Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
160
rsbu_payroll/models/hr_payslip_run.py
Normal file
160
rsbu_payroll/models/hr_payslip_run.py
Normal file
@@ -0,0 +1,160 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user