310 lines
15 KiB
Python
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,
|
|
}
|