from odoo import api, fields, models, _ from odoo.exceptions import UserError class HrPayslipRun(models.Model): """Реестр зарплаты (зарплатный проект банка). Расширяет штатный «Пакет расчётных листков» до реестра на выплату: банк одним платежом зачисляет общую сумму сотрудникам (в выписке одна строка от банка), а реестр раскладывает её на выплаты по каждому сотруднику (Дт 70 — Кт 51) одной кнопкой. """ _inherit = 'hr.payslip.run' payment_bank_partner_id = fields.Many2one( 'res.partner', string='Банк (зарплатный проект)', help='Контрагент-банк зарплатного проекта (напр. Т-Банк), от которого ' 'приходит единый платёж по реестру.') payment_date = fields.Date( string='Дата выплаты', default=fields.Date.context_today) bank_fee_amount = fields.Monetary( string='Комиссия банка', currency_field='currency_id', default=0.0) currency_id = fields.Many2one( 'res.currency', related='company_id.currency_id', readonly=True) total_net = fields.Monetary( string='Итого к перечислению', currency_field='currency_id', compute='_compute_total_net', store=False, help='Сумма «к выплате» (правило NET) по проведённым расчётным ' 'листкам реестра.') rsbu_payout_move_id = fields.Many2one( 'rsbu.move', string='РСБУ-проводка выплаты', copy=False, readonly=True) payout_state = fields.Selection( [('to_pay', 'К выплате'), ('paid', 'Выплачено')], string='Статус выплаты', default='to_pay', copy=False, readonly=True) @api.depends('slip_ids.line_ids.total', 'slip_ids.state') def _compute_total_net(self): for run in self: total = 0.0 for slip in run.slip_ids.filtered(lambda s: s.state == 'done'): total += run._slip_net(slip) run.total_net = total @staticmethod def _slip_net(slip): """Сумма «к выплате» по расчётному листку (правило NET).""" net_lines = slip.line_ids.filtered(lambda l: l.code == 'NET') return sum(net_lines.mapped('total')) # ------------------------------------------------------------------ # Проводки выплаты # ------------------------------------------------------------------ def _rsbu_account(self, code): """Найти счёт РСБУ по коду в разрезе компании реестра.""" self.ensure_one() account = self.env['rsbu.account'].search([ ('code', '=', code), ('company_id', '=', self.company_id.id), ], limit=1) if not account: raise UserError(_( 'В плане счетов РСБУ компании «%(company)s» не найден счёт %(code)s.', company=self.company_id.display_name, code=code)) return account def action_rsbu_pay(self): """Сформировать РСБУ-проводку выплаты по реестру (Дт 70 — Кт 51).""" for run in self: if run.rsbu_payout_move_id: # Идемпотентность: повторное нажатие не дублирует. continue done_slips = run.slip_ids.filtered(lambda s: s.state == 'done') if not done_slips: raise UserError(_( 'В реестре «%s» нет проведённых расчётных листков.') % run.display_name) acc_70 = run._rsbu_account('70') acc_51 = run._rsbu_account('51') lines = [] total_pay = 0.0 for slip in done_slips: net = run._slip_net(slip) if not net: continue if not slip.employee_id: raise UserError(_( 'В расчётном листке %s не указан сотрудник.') % slip.number) # Дт 70 — закрытие задолженности перед сотрудником. lines.append({ 'account_id': acc_70.id, 'name': _('Выплата зарплаты: %s') % slip.employee_id.name, 'debit': net, 'employee_id': slip.employee_id.id, }) total_pay += net if not lines: raise UserError(_( 'Нет сумм к выплате по реестру «%s».') % run.display_name) fee = run.bank_fee_amount or 0.0 # Кт 51 — единый платёж банка (сумма выплат + комиссия). lines.append({ 'account_id': acc_51.id, 'name': _('Перечисление зарплаты (реестр %s)') % run.name, 'credit': total_pay + fee, }) if fee: acc_fee = run._rsbu_account('91') lines.append({ 'account_id': acc_fee.id, 'name': _('Комиссия банка по реестру %s') % run.name, 'debit': fee, }) usn_recognition = ( 'expense' if run.company_id.rsbu_usn_mode == 'usn_income_expense' else 'none') move = self.env['rsbu.move'].sudo().create({ 'date': run.payment_date or fields.Date.context_today(run), 'company_id': run.company_id.id, 'ref': _('Выплата зарплаты по реестру %s') % run.name, 'source_model': run._name, 'source_res_id': run.id, 'usn_recognition': usn_recognition, 'line_ids': [(0, 0, line) for line in lines], }) move.action_post() run.write({ 'rsbu_payout_move_id': move.id, 'payout_state': 'paid', }) return True def action_rsbu_pay_cancel(self): """Отменить проводку выплаты (сторно/удаление).""" for run in self: move = run.rsbu_payout_move_id.sudo() if move: run.rsbu_payout_move_id = False run.payout_state = 'to_pay' if move.state == 'posted': move.action_draft() move.unlink() return True def action_view_rsbu_payout_move(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': _('РСБУ-проводка выплаты'), 'res_model': 'rsbu.move', 'view_mode': 'form', 'res_id': self.rsbu_payout_move_id.id, }