Files
enterprise-addons/hr_payroll_account/models/hr_payslip.py
T

310 lines
15 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare, float_is_zero, plaintext2html
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
date = fields.Date('Date Account',
help="Keep empty to use the period of the validation(Payslip) date.")
journal_id = fields.Many2one('account.journal', 'Salary Journal', related="struct_id.journal_id", check_company=True)
move_id = fields.Many2one('account.move', '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)
batch_payroll_move_lines = fields.Boolean(related='company_id.batch_payroll_move_lines')
@api.model
def _issues_dependencies(self):
return super()._issues_dependencies() + ['struct_id.journal_id']
def _get_warnings_by_slip(self):
warnings_by_slip = super()._get_warnings_by_slip()
for slip in self.filtered(
lambda slip: slip.state in ['draft', 'validated'] and slip.struct_id and not slip.with_company(slip.company_id).journal_id
):
warnings_by_slip[slip].append({
'message': _("Account Journal not configured on Structure"),
'action_text': _("Structure"),
'action': slip.struct_id._get_records_action(target='new'),
'level': 'warning',
})
return warnings_by_slip
def action_payslip_cancel(self):
moves = self.mapped('move_id')
moves.sudo()._unlink_or_reverse()
return super().action_payslip_cancel()
def action_payslip_done(self):
"""
Generate the accounting entries related to the selected payslips
A move is created for each journal and for each month.
"""
if any(slip.state == 'paid' for slip in self):
raise ValidationError(_("You can't create a journal entry for a paid payslip."))
res = super().action_payslip_done()
self.filtered('journal_id')._action_create_account_move()
return res
def _action_create_account_move(self):
precision = self.env['decimal.precision'].precision_get('Payroll')
all_payslips = self
# Adding pay slips from a batch.
for run in self.payslip_run_id:
if run._are_payslips_ready():
all_payslips |= run.slip_ids
# A payslip need to have a validated state and not an accounting move.
payslips_to_post = all_payslips.filtered(lambda slip: slip.state == 'validated' and not slip.move_id and slip.struct_id.journal_id)
# Map all payslips by structure journal and pay slips month.
# Case 1: Batch all the payslips together -> {'journal_id': {'month': slips}}
# Case 2: Generate account move separately -> [{'journal_id': {'month': slip}}]
if self.company_id.batch_payroll_move_lines:
all_slip_mapped_data = defaultdict(lambda: defaultdict(lambda: self.env['hr.payslip']))
for slip in payslips_to_post:
all_slip_mapped_data[slip.struct_id.journal_id.id][slip.date or fields.Date.end_of(slip.date_to, 'month')] |= slip
all_slip_mapped_data = [all_slip_mapped_data]
else:
all_slip_mapped_data = [{
slip.struct_id.journal_id.id: {
slip.date or fields.Date.end_of(slip.date_to, 'month'): slip
}
} for slip in payslips_to_post]
account_move_vals = []
slips_date_tuples = []
for slip_mapped_data in all_slip_mapped_data:
for journal_id in slip_mapped_data: # For each journal_id.
for slip_date in slip_mapped_data[journal_id]: # For each month.
line_ids = []
debit_sum = 0.0
credit_sum = 0.0
date = slip_date
move_dict = {
'narration': '',
'ref': fields.Date.end_of(slip_mapped_data[journal_id][slip_date][0].date_to, 'month').strftime('%B %Y'),
'journal_id': journal_id,
'date': date,
}
for slip in slip_mapped_data[journal_id][slip_date]:
move_dict['narration'] += plaintext2html(f'{slip.id or ""} - {slip.employee_id.name or ""}')
move_dict['narration'] += Markup('<br/>')
slip_lines = slip._prepare_slip_lines(date, line_ids)
line_ids.extend(slip_lines)
for line_id in line_ids: # Get the debit and credit sum.
debit_sum += line_id['debit']
credit_sum += line_id['credit']
# The code below is called if there is an error in the balance between credit and debit sum.
if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1:
slip._prepare_adjust_line(line_ids, 'credit', debit_sum, credit_sum, date)
elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1:
slip._prepare_adjust_line(line_ids, 'debit', debit_sum, credit_sum, date)
# Add accounting lines in the move
move_dict['line_ids'] = [(0, 0, line_vals) for line_vals in line_ids]
account_move_vals.append(move_dict)
slips_date_tuples.append((slip_mapped_data[journal_id][slip_date], date))
moves = self._create_account_move(account_move_vals)
for move, (slips, date) in zip(moves, slips_date_tuples):
slips.write({'move_id': move.id, 'date': date})
if slips.company_id.batch_payroll_move_lines:
slips.payslip_run_id.write({'move_id': move.id})
return True
def _prepare_line_values(self, line, account, date, debit, credit):
batch_lines = self.company_id.batch_payroll_move_lines
if not batch_lines and line.salary_rule_id.employee_move_line:
partner = self.employee_id.work_contact_id
else:
partner = line.partner_id
line_vals = []
if not batch_lines and line.salary_rule_id.employee_move_line and self.employee_id.has_multiple_bank_accounts:
debit_allocations = self.compute_salary_allocations(debit)
credit_allocations = self.compute_salary_allocations(credit)
for ba in self.employee_id.bank_account_ids:
subdebit = debit_allocations.get(str(ba.id), 0)
subcredit = credit_allocations.get(str(ba.id), 0)
line_vals.append({
'name': line.name if line.salary_rule_id.split_move_lines else line.salary_rule_id.name,
'partner_id': partner.id,
'account_id': account.id,
'employee_bank_account_id': ba.id,
'journal_id': line.slip_id.struct_id.journal_id.id,
'date': date,
'debit': subdebit,
'credit': subcredit,
'analytic_distribution': line.salary_rule_id.analytic_distribution or line.slip_id.version_id.analytic_distribution,
'tax_tag_ids': line.debit_tag_ids.ids if account.id == line.salary_rule_id.account_debit.id else line.credit_tag_ids.ids,
'tax_ids': [(4, tax_id) for tax_id in account.tax_ids.ids],
})
return line_vals
return [{
'name': line.name if line.salary_rule_id.split_move_lines else line.salary_rule_id.name,
'partner_id': partner.id,
'account_id': account.id,
'journal_id': line.slip_id.struct_id.journal_id.id,
'date': date,
'debit': debit,
'credit': credit,
'analytic_distribution': line.salary_rule_id.analytic_distribution or line.slip_id.version_id.analytic_distribution,
'tax_tag_ids': line.debit_tag_ids.ids if account.id == line.salary_rule_id.account_debit.id else line.credit_tag_ids.ids,
'tax_ids': [(4, tax_id) for tax_id in account.tax_ids.ids],
}]
def _prepare_slip_lines(self, date, line_ids):
self.ensure_one()
precision = self.env['decimal.precision'].precision_get('Payroll')
new_lines = []
for line in self.line_ids.filtered(lambda line: line.category_id):
amount = line.total
if line.code == 'NET': # Check if the line is the 'Net Salary'.
for tmp_line in self.line_ids.filtered(lambda line: line.category_id):
if tmp_line.salary_rule_id.not_computed_in_net: # Check if the rule must be computed in the 'Net Salary' or not.
if amount > 0:
amount -= abs(tmp_line.total)
elif amount < 0:
amount += abs(tmp_line.total)
if float_is_zero(amount, precision_digits=precision):
continue
debit_account = line.salary_rule_id.account_debit
credit_account = line.salary_rule_id.account_credit
merge_amounts = self.company_id.batch_payroll_move_lines or not line.salary_rule_id.employee_move_line
if debit_account.id: # If the rule has a debit account.
if self.company_id.account_storno and self.credit_note:
debit = amount if amount < 0.0 else 0.0
credit = -amount if amount > 0.0 else 0.0
else:
debit = amount if amount > 0.0 else 0.0
credit = -amount if amount < 0.0 else 0.0
debit_line = merge_amounts and next(self._get_existing_lines(
line_ids + new_lines, line, debit_account.id, debit, credit), False)
if not debit_line:
debit_lines = self._prepare_line_values(line, debit_account, date, debit, credit)
new_lines.extend(debit_lines)
else:
debit_line['debit'] += debit
debit_line['credit'] += credit
if credit_account.id: # If the rule has a credit account.
if self.company_id.account_storno and self.credit_note:
debit = -amount if amount > 0.0 else 0.0
credit = amount if amount < 0.0 else 0.0
else:
debit = -amount if amount < 0.0 else 0.0
credit = amount if amount > 0.0 else 0.0
credit_line = merge_amounts and next(self._get_existing_lines(
line_ids + new_lines, line, credit_account.id, debit, credit), False)
if not credit_line:
credit_lines = self._prepare_line_values(line, credit_account, date, debit, credit)
new_lines.extend(credit_lines)
else:
credit_line['debit'] += debit
credit_line['credit'] += credit
return new_lines
def _prepare_adjust_line(self, line_ids, adjust_type, debit_sum, credit_sum, date):
acc_id = self.sudo().journal_id.default_account_id.id
if not acc_id:
raise UserError(_('The Expense Journal "%s" has not properly configured the default Account!', self.journal_id.name))
existing_adjustment_line = (
line_id for line_id in line_ids if line_id['name'] == self.env._('Adjustment Entry')
)
adjust_credit = next(existing_adjustment_line, False)
if not adjust_credit:
adjust_credit = {
'name': _('Adjustment Entry'),
'partner_id': False,
'account_id': acc_id,
'journal_id': self.journal_id.id,
'date': date,
'debit': 0.0 if adjust_type == 'credit' else credit_sum - debit_sum,
'credit': debit_sum - credit_sum if adjust_type == 'credit' else 0.0,
}
line_ids.append(adjust_credit)
else:
adjust_credit['credit'] = debit_sum - credit_sum
def _check_debit_credit_tags(self, line_vals, line, account_id):
return (
account_id != line.salary_rule_id.account_debit.id
or sorted(line_vals['tax_tag_ids']) == sorted(line.debit_tag_ids.ids)
) and (
account_id != line.salary_rule_id.account_credit.id
or sorted(line_vals['tax_tag_ids']) == sorted(line.credit_tag_ids.ids)
)
def _get_existing_lines(self, line_ids, line, account_id, debit, credit):
distribution = line.salary_rule_id.analytic_distribution or line.slip_id.version_id.analytic_distribution
return (
line_id for line_id in line_ids if
line_id['name'] == (line.name if line.salary_rule_id.split_move_lines else line.salary_rule_id.name)
and line_id['account_id'] == account_id
and ((line_id['debit'] > 0 and credit <= 0) or (line_id['credit'] > 0 and debit <= 0))
and line_id['analytic_distribution'] == distribution
and self._check_debit_credit_tags(line_id, line, account_id)
)
# TODO master: remove this method no longer used
def _check_partially_matching_accounts(self, line_id, line):
keys = line_id['analytic_distribution']
unravelled_keys = []
for key in keys:
ids = key.split(',')
unravelled_keys += ids
result = any(str(acc_id.id) in unravelled_keys for acc_id in line.salary_rule_id.distribution_analytic_account_ids) \
or any(str(acc_id.id) in unravelled_keys for acc_id in line.slip_id.version_id.distribution_analytic_account_ids)
return result
def _create_account_move(self, values):
return self.env['account.move'].sudo().create(values)
def action_register_payment(self):
''' Open the account.payment.register wizard to pay the selected journal entries.
:return: An action opening the account.payment.register wizard.
'''
if any(state == 'paid' for state in self.mapped('state')):
raise UserError(self.env._("You can only register payments for unpaid documents."))
if not self.struct_id.rule_ids.filtered(lambda r: r.code == "NET").account_credit.reconcile:
raise UserError(self.env._('The credit account on the NET salary rule is not reconciliable'))
bank_accounts = self.employee_id.sudo().bank_account_ids
if any(not bank.allow_out_payment for bank in bank_accounts):
raise UserError(self.env._('An employee bank account is untrusted'))
if any(m.state != 'posted' for m in self.move_id):
raise UserError(self.env._("You can only register payment for posted journal entries."))
return self.move_id.line_ids.action_register_payment(
ctx={
"default_partner_id": self.employee_id.work_contact_id.id,
"default_partner_bank_id": self.employee_id.primary_bank_account_id.id,
"default_company_id": self.company_id.id,
"payment_consider_partner": True,
})
def action_open_move(self):
return {
'type': 'ir.actions.act_window',
'name': _('Journal Entry'),
'res_model': 'account.move',
'view_mode': 'form',
'res_id': self.move_id.id,
}