Files

97 lines
5.8 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
from odoo.exceptions import AccessError, UserError
class AccountMove(models.Model):
_inherit = 'account.move'
def _post(self, soft=True):
# EXTENDS account
# In the case where an expense is reimbursed through an employee's payslip, we need to reconcile the move generated
# for the expense and the one generated for the payslip, so the expense can be set to 'paid'
# and only one payment shall be made, the payslip one. If the moves have been altered, the automatic reconciliation may not be done,
# it will be done manually by an accountant. We will keep the temporary matching data to simplify that case.
lines_to_reconcile = self.env['account.move.line']
if not self.env.context.get('skip_reconcile_expense_with_payslip') and self.sudo().payslip_ids.expense_ids:
self = self.with_context(skip_reconcile_expense_with_payslip=True) # noqa: PLW0642 Avoid entering here recursively
lines_to_reconcile = self._hr_payroll_expense_prepare_move_lines_to_reconcile()
res = super()._post(soft=soft) # Posting will automatically reconcile same-account-same-matching lines
lines_to_reconcile = lines_to_reconcile.filtered(lambda line: not line.reconciled)
if lines_to_reconcile:
# Tries to reconcile move lines automatically (to mark the expense as paid)
wizard = self.env['account.reconcile.wizard'].with_context(
active_model='account.move.line',
active_ids=lines_to_reconcile.ids,
).new({})
if not (wizard.is_write_off_required or wizard.force_partials): # Only reconcile if there are no issue that requires user-input
wizard.reconcile()
return res
def unlink(self):
# EXTENDS account
moves_to_unlink_ids = self.sudo().payslip_ids.expense_ids.account_move_id.ids
if moves_to_unlink_ids:
self.browse(moves_to_unlink_ids)._unlink_or_reverse()
return super().unlink()
def _hr_payroll_expense_prepare_move_lines_to_reconcile(self):
"""
(Create if None &) Post the expense's move and prepare it to be reconciled with the payslip move,
as the expense is reimbursed to the employee through the payslip.
This ensures the payment state of the expense, its report, and move are set to 'paid'.
"""
if not self.env.is_superuser() and not self.env.user.has_group('account.group_account_invoice'):
raise AccessError(_("You don't have the access rights to post an invoice."))
payslips_sudo = self.sudo().payslip_ids
all_payslip_expenses_sudo = payslips_sudo.expense_ids
if not all_payslip_expenses_sudo:
return self.env['account.move.line'] # No payslip or expenses, no reason to continue
expenses_without_moves = all_payslip_expenses_sudo.filtered(lambda expense: not expense.account_move_id)
if expenses_without_moves:
expenses_without_moves._post_without_wizard() # Only valid case to bypass the wizard
# Prepare the reconciliation by grouping expense moves per payslip move
payslip_move_to_expense_move_map = {}
# Handles the case where an account move linked to an expense is already paid, ignoring the fact that it was flagged to be paid here
# (They will get paid twice, but it's the user's responsibility)
valid_expenses_sudo = all_payslip_expenses_sudo.filtered(lambda expense: expense.state == 'posted')
for payslip_sudo, expenses_sudo in valid_expenses_sudo.grouped('payslip_id').items():
payslip_move_to_expense_move_map.setdefault(payslip_sudo.move_id, self.env['account.move'].sudo())
payslip_move_to_expense_move_map[payslip_sudo.move_id] |= expenses_sudo.sorted().account_move_id
# Prepare and collect the reconciliation lines
lines_to_reconcile_sudo = self.env['account.move.line']
rule_account_id_per_struct_id = payslips_sudo.struct_id._get_expense_rule_account_id_map(payslips_sudo.company_id)
for idx, (payslip_move_sudo, expense_moves_sudo) in enumerate(payslip_move_to_expense_move_map.items()):
# Get the payslip move lines generated for the expenses
payslip_account_id = rule_account_id_per_struct_id[payslip_move_sudo.payslip_ids.struct_id.id] # Only one structure per move
lines_to_match_sudo = payslip_move_sudo.line_ids.filtered(
lambda aml: aml.account_id.id == payslip_account_id and aml.display_type != 'payment_term'
)
# Get the corresponding payable lines for the expenses
lines_to_match_sudo |= expense_moves_sudo.line_ids.filtered(lambda aml: aml.account_id.account_type == 'liability_payable')
# Use the matching number system to simplify reconciliation, the number itself is just made to be unique
lines_to_match_sudo.matching_number = f'{idx:0>10}-{payslip_move_sudo.id}-{min(expense_moves_sudo.ids)}'
lines_to_reconcile_sudo |= lines_to_match_sudo
# Post all expense moves that aren't posted already
expense_moves_to_post_sudo = all_payslip_expenses_sudo.account_move_id.filtered(lambda move: move.state == 'draft')
if expense_moves_to_post_sudo:
expense_moves_to_post_sudo._post(soft=False)
# If we have more than 2 accounts, we will not be able to reconcile
if len(lines_to_reconcile_sudo.account_id) > 2:
return self.env['account.move.line']
# Return all the account move lines to be reconciled in the previous sudo state, sorted to facilitate the pairing
return lines_to_reconcile_sudo.sorted(lambda line: abs(line.balance)).sudo(self.env.is_superuser())