243 lines
8.6 KiB
Python
243 lines
8.6 KiB
Python
import logging
|
|
from collections import defaultdict
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HrPayslip(models.Model):
|
|
_inherit = 'hr.payslip'
|
|
|
|
move_id = fields.Many2one(
|
|
'account.move',
|
|
string='Accounting Entry',
|
|
readonly=True,
|
|
copy=False,
|
|
index='btree_not_null',
|
|
)
|
|
move_state = fields.Selection(
|
|
related='move_id.state',
|
|
string='Move State',
|
|
export_string_translation=False,
|
|
)
|
|
journal_id = fields.Many2one(
|
|
'account.journal',
|
|
string='Salary Journal',
|
|
domain="[('type', '=', 'general')]",
|
|
)
|
|
|
|
@api.model
|
|
def _fusion_enterprise_bridge_active(self):
|
|
"""Return True when the Enterprise hr_payroll_account module is the
|
|
authoritative payslip - GL bridge on this database. Used to avoid
|
|
duplicate move creation while both modules coexist."""
|
|
module = self.env['ir.module.module'].sudo().search(
|
|
[('name', '=', 'hr_payroll_account')], limit=1,
|
|
)
|
|
return bool(module) and module.state == 'installed'
|
|
|
|
def _fusion_resolve_journal(self):
|
|
"""Pick the journal for this payslip's bridge move."""
|
|
self.ensure_one()
|
|
if self.journal_id:
|
|
return self.journal_id
|
|
struct = self.struct_id
|
|
if struct and 'journal_id' in struct._fields and struct.journal_id:
|
|
return struct.journal_id
|
|
company = self.company_id or self.env.company
|
|
return company.fusion_payroll_journal_id or False
|
|
|
|
def _fusion_resolve_partner(self):
|
|
"""Pick the best partner reference for the move lines of this payslip."""
|
|
self.ensure_one()
|
|
employee = self.employee_id
|
|
if not employee:
|
|
return False
|
|
if 'work_contact_id' in employee._fields and employee.work_contact_id:
|
|
return employee.work_contact_id.id
|
|
if 'address_home_id' in employee._fields and employee.address_home_id:
|
|
return employee.address_home_id.id
|
|
return False
|
|
|
|
def _fusion_get_line_amount(self, line):
|
|
"""Hook so a localisation can override which payslip-line value is
|
|
posted. Defaults to ``line.total``."""
|
|
return line.total or 0.0
|
|
|
|
def action_payslip_done(self):
|
|
res = super().action_payslip_done()
|
|
if self._fusion_enterprise_bridge_active():
|
|
return res
|
|
for slip in self:
|
|
if slip.move_id:
|
|
continue
|
|
journal = slip._fusion_resolve_journal()
|
|
if not journal:
|
|
continue
|
|
try:
|
|
slip._fusion_create_account_move(journal=journal)
|
|
except UserError as err:
|
|
_logger.warning(
|
|
"Fusion payroll bridge: GL move skipped for slip %s: %s",
|
|
slip.id, err,
|
|
)
|
|
slip.message_post(body=_(
|
|
"Fusion Payroll bridge could not create the journal "
|
|
"entry: %s",
|
|
) % err)
|
|
except Exception:
|
|
_logger.exception(
|
|
"Fusion payroll bridge: unexpected failure for slip %s",
|
|
slip.id,
|
|
)
|
|
return res
|
|
|
|
def action_payslip_cancel(self):
|
|
if hasattr(super(), 'action_payslip_cancel'):
|
|
res = super().action_payslip_cancel()
|
|
else:
|
|
res = True
|
|
if self._fusion_enterprise_bridge_active():
|
|
return res
|
|
for slip in self:
|
|
move = slip.move_id
|
|
if not move:
|
|
continue
|
|
try:
|
|
if move.state == 'posted':
|
|
move.button_draft()
|
|
move.with_context(force_delete=True).unlink()
|
|
except Exception:
|
|
_logger.exception(
|
|
"Fusion payroll bridge: cannot reverse move %s for slip %s",
|
|
move.id, slip.id,
|
|
)
|
|
return res
|
|
|
|
def _fusion_create_account_move(self, journal=None):
|
|
"""Build a balanced ``account.move`` from this payslip using the
|
|
``account_debit`` / ``account_credit`` mapping on each salary rule.
|
|
Returns the created move (or False if there is nothing to post)."""
|
|
self.ensure_one()
|
|
if not self.line_ids:
|
|
return False
|
|
journal = journal or self._fusion_resolve_journal()
|
|
if not journal:
|
|
raise UserError(_(
|
|
"No salary journal configured for company %s. "
|
|
"Set a fallback journal under Accounting Settings - "
|
|
"Fusion Payroll Bridge.",
|
|
) % (self.company_id.display_name if self.company_id else ''))
|
|
|
|
debit_per_account = defaultdict(float)
|
|
credit_per_account = defaultdict(float)
|
|
analytic_per_account = {}
|
|
|
|
for line in self.line_ids:
|
|
rule = line.salary_rule_id
|
|
amount = self._fusion_get_line_amount(line)
|
|
if not amount:
|
|
continue
|
|
debit_account = rule.account_debit
|
|
credit_account = rule.account_credit
|
|
analytic = (
|
|
rule.fusion_analytic_account_id
|
|
if 'fusion_analytic_account_id' in rule._fields
|
|
else False
|
|
)
|
|
|
|
if amount > 0:
|
|
if debit_account:
|
|
debit_per_account[debit_account.id] += amount
|
|
if credit_account:
|
|
credit_per_account[credit_account.id] += amount
|
|
else:
|
|
pos = -amount
|
|
if debit_account:
|
|
credit_per_account[debit_account.id] += pos
|
|
if credit_account:
|
|
debit_per_account[credit_account.id] += pos
|
|
|
|
if analytic:
|
|
for acc in (debit_account, credit_account):
|
|
if acc and acc.id not in analytic_per_account:
|
|
analytic_per_account[acc.id] = analytic.id
|
|
|
|
partner_id = self._fusion_resolve_partner()
|
|
line_label = self.display_name or self.number or _('Payslip')
|
|
move_lines = []
|
|
all_accounts = set(debit_per_account) | set(credit_per_account)
|
|
|
|
for account_id in all_accounts:
|
|
net = (
|
|
debit_per_account.get(account_id, 0.0)
|
|
- credit_per_account.get(account_id, 0.0)
|
|
)
|
|
if abs(net) < 0.005:
|
|
continue
|
|
vals = {
|
|
'account_id': account_id,
|
|
'name': line_label,
|
|
'partner_id': partner_id,
|
|
}
|
|
if net > 0:
|
|
vals['debit'] = round(net, 2)
|
|
vals['credit'] = 0.0
|
|
else:
|
|
vals['debit'] = 0.0
|
|
vals['credit'] = round(-net, 2)
|
|
analytic_id = analytic_per_account.get(account_id)
|
|
if analytic_id:
|
|
vals['analytic_distribution'] = {str(analytic_id): 100.0}
|
|
move_lines.append((0, 0, vals))
|
|
|
|
if not move_lines:
|
|
return False
|
|
|
|
total_debit = sum(vals[2]['debit'] for vals in move_lines)
|
|
total_credit = sum(vals[2]['credit'] for vals in move_lines)
|
|
if abs(total_debit - total_credit) > 0.01:
|
|
raise UserError(_(
|
|
"Payroll move not balanced: debit=%(d).2f, credit=%(c).2f. "
|
|
"Check the account_debit / account_credit mapping on the "
|
|
"salary rules of payslip %(name)s.",
|
|
) % {
|
|
'd': total_debit,
|
|
'c': total_credit,
|
|
'name': self.display_name,
|
|
})
|
|
|
|
move_vals = {
|
|
'journal_id': journal.id,
|
|
'date': self.date_to or fields.Date.context_today(self),
|
|
'ref': self.number or self.display_name,
|
|
'line_ids': move_lines,
|
|
'move_type': 'entry',
|
|
}
|
|
move = self.env['account.move'].sudo().create(move_vals)
|
|
if self.company_id and self.company_id.fusion_payroll_auto_post:
|
|
try:
|
|
move.action_post()
|
|
except Exception:
|
|
_logger.exception(
|
|
"Fusion payroll bridge: auto-post failed for move %s; "
|
|
"leaving in draft.",
|
|
move.id,
|
|
)
|
|
self.move_id = move.id
|
|
return move
|
|
|
|
def action_open_move(self):
|
|
self.ensure_one()
|
|
if not self.move_id:
|
|
return False
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _('Journal Entry'),
|
|
'res_model': 'account.move',
|
|
'view_mode': 'form',
|
|
'res_id': self.move_id.id,
|
|
}
|