forked from Mapan/odoo17e
230 lines
12 KiB
Python
230 lines
12 KiB
Python
#-*- coding:utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
from markupsafe import Markup
|
|
|
|
from odoo import 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')
|
|
batch_payroll_move_lines = fields.Boolean(related='company_id.batch_payroll_move_lines')
|
|
|
|
def action_payslip_cancel(self):
|
|
moves = self.mapped('move_id')
|
|
moves._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.
|
|
"""
|
|
res = super().action_payslip_done()
|
|
self._action_create_account_move()
|
|
return res
|
|
|
|
def _action_create_account_move(self):
|
|
precision = self.env['decimal.precision'].precision_get('Payroll')
|
|
|
|
# Add payslip without run
|
|
payslips_to_post = self.filtered(lambda slip: not slip.payslip_run_id)
|
|
|
|
# Adding pay slips from a batch and deleting pay slips with a batch that is not ready for validation.
|
|
payslip_runs = (self - payslips_to_post).payslip_run_id
|
|
for run in payslip_runs:
|
|
if run._are_payslips_ready():
|
|
payslips_to_post |= run.slip_ids
|
|
|
|
# A payslip need to have a done state and not an accounting move.
|
|
payslips_to_post = payslips_to_post.filtered(lambda slip: slip.state == 'done' and not slip.move_id)
|
|
|
|
# Check that a journal exists on all the structures
|
|
if any(not payslip.struct_id for payslip in payslips_to_post):
|
|
raise ValidationError(_('One of the contract for these payslips has no structure type.'))
|
|
if any(not structure.journal_id for structure in payslips_to_post.mapped('struct_id')):
|
|
raise ValidationError(_('One of the payroll structures has no account journal defined on it.'))
|
|
|
|
# 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]
|
|
|
|
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(slip.number 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]
|
|
move = self._create_account_move(move_dict)
|
|
for slip in slip_mapped_data[journal_id][slip_date]:
|
|
slip.write({'move_id': move.id, 'date': date})
|
|
return True
|
|
|
|
def _prepare_line_values(self, line, account_id, date, debit, credit):
|
|
if not self.company_id.batch_payroll_move_lines and line.code == "NET":
|
|
partner = self.employee_id.work_contact_id
|
|
else:
|
|
partner = line.partner_id
|
|
return {
|
|
'name': line.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_account_id and {line.salary_rule_id.analytic_account_id.id: 100}) or
|
|
(line.slip_id.contract_id.analytic_account_id.id and {line.slip_id.contract_id.analytic_account_id.id: 100})
|
|
}
|
|
|
|
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_id = line.salary_rule_id.account_debit.id
|
|
credit_account_id = line.salary_rule_id.account_credit.id
|
|
if debit_account_id: # If the rule has a debit account.
|
|
debit = amount if amount > 0.0 else 0.0
|
|
credit = -amount if amount < 0.0 else 0.0
|
|
|
|
debit_line = self._get_existing_lines(
|
|
line_ids + new_lines, line, debit_account_id, debit, credit)
|
|
|
|
if not debit_line:
|
|
debit_line = self._prepare_line_values(line, debit_account_id, date, debit, credit)
|
|
debit_line['tax_ids'] = [(4, tax_id) for tax_id in line.salary_rule_id.account_debit.tax_ids.ids]
|
|
new_lines.append(debit_line)
|
|
else:
|
|
debit_line['debit'] += debit
|
|
debit_line['credit'] += credit
|
|
|
|
if credit_account_id: # If the rule has a credit account.
|
|
debit = -amount if amount < 0.0 else 0.0
|
|
credit = amount if amount > 0.0 else 0.0
|
|
credit_line = self._get_existing_lines(
|
|
line_ids + new_lines, line, credit_account_id, debit, credit)
|
|
|
|
if not credit_line:
|
|
credit_line = self._prepare_line_values(line, credit_account_id, date, debit, credit)
|
|
credit_line['tax_ids'] = [(4, tax_id) for tax_id in line.salary_rule_id.account_credit.tax_ids.ids]
|
|
new_lines.append(credit_line)
|
|
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'] == _('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 _get_existing_lines(self, line_ids, line, account_id, debit, credit):
|
|
existing_lines = (
|
|
line_id for line_id in line_ids if
|
|
line_id['name'] == line.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 (
|
|
(
|
|
not line_id['analytic_distribution'] and
|
|
not line.salary_rule_id.analytic_account_id.id and
|
|
not line.slip_id.contract_id.analytic_account_id.id
|
|
)
|
|
or line_id['analytic_distribution'] and line.salary_rule_id.analytic_account_id.id in line_id['analytic_distribution']
|
|
or line_id['analytic_distribution'] and line.slip_id.contract_id.analytic_account_id.id in line_id['analytic_distribution']
|
|
|
|
)
|
|
)
|
|
return next(existing_lines, False)
|
|
|
|
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 not self.struct_id.rule_ids.filtered(lambda r: r.code == "NET").account_credit.reconcile:
|
|
raise UserError(_('The credit account on the NET salary rule is not reconciliable'))
|
|
bank_account = self.employee_id.sudo().bank_account_id
|
|
if not bank_account.allow_out_payment:
|
|
raise UserError(_('The employee bank account is untrusted'))
|
|
return self.move_id.with_context(
|
|
default_partner_id=self.employee_id.work_contact_id.id,
|
|
default_partner_bank_id=bank_account.id
|
|
).action_register_payment()
|