diff --git a/models/account_payment.py b/models/account_payment.py index e54d137..f4c143b 100644 --- a/models/account_payment.py +++ b/models/account_payment.py @@ -9,19 +9,26 @@ class AccountPayment(models.Model): realization_id = fields.Many2one('hr.expense.realization', string='Originating Realization', readonly=True) + def _get_hr_expense_base_class(self): + """ Returns the hr_expense class in the MRO to jump over it. """ + mro = type(self).mro() + # The module for Odoo's hr_expense override is usually 'odoo.addons.hr_expense' + return next((c for c in mro if c.__module__.startswith('odoo.addons.hr_expense')), None) + def action_post(self): """ Confirmation bypass. Calls standard post with skip flag and FORCES a sync to ensure deductions are applied to the Journal Entry. """ - # 1. Force a synchronization of the moves right before posting. - # We pass 'amount' and 'deduction_line_ids' to trigger vendor_payment_diff_amount. self_bypass = self.with_context(skip_expense_lock=True) for payment in self_bypass: if payment.state == 'draft': payment._synchronize_to_moves({'amount', 'deduction_line_ids', 'amount_substract'}) - # 2. Standard confirmation logic with bypass flag for internal writes + # JUMPER: Skip hr_expense's action_post if it exists, or just ensure skip_expense_lock is used globally + hr_expense_class = self._get_hr_expense_base_class() + if hr_expense_class: + return super(hr_expense_class, self.with_context(skip_expense_lock=True)).action_post() return super(AccountPayment, self.with_context(skip_expense_lock=True)).action_post() def _synchronize_to_moves(self, changed_fields): @@ -31,25 +38,23 @@ class AccountPayment(models.Model): if 'amount' not in changed_fields: changed_fields = set(changed_fields) | {'amount'} - # SURGICAL JUMPER: If we have the bypass flag, we jump over the hr_expense method. - if self._context.get('skip_expense_lock'): - mro = type(self).mro() - hr_expense_class = next((c for c in mro if c.__module__.startswith('odoo.addons.hr_expense')), None) - if hr_expense_class: - super(hr_expense_class, self)._synchronize_to_moves(changed_fields) - else: - super()._synchronize_to_moves(changed_fields) + # SURGICAL JUMPER: Bypass the hr_expense method's locking check + hr_expense_class = self._get_hr_expense_base_class() + if self._context.get('skip_expense_lock') and hr_expense_class: + super(hr_expense_class, self)._synchronize_to_moves(changed_fields) else: super()._synchronize_to_moves(changed_fields) # 2. Custom Deduction Synchronization # After base sync, we "fix" the lines if deductions are present - for payment in self.with_context(skip_account_move_synchronization=True): + # We use with_context(skip_expense_lock=True) for EVERY operation on lines + bypass_ctx = {'skip_expense_lock': True, 'skip_account_move_synchronization': True} + for payment in self.with_context(**bypass_ctx): if payment.amount_substract and payment.amount_substract > 0 and payment.move_id: liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines() # Adjust liquidity line to final_payment_amount - for line in liquidity_lines: + for line in liquidity_lines.with_context(skip_expense_lock=True): final_amount = payment.final_payment_amount line_vals = { 'amount_currency': -final_amount if payment.payment_type == 'outbound' else final_amount, @@ -59,15 +64,12 @@ class AccountPayment(models.Model): line.write(line_vals) # Deduction lines management - # Remove existing deduction lines that are no longer present or need refresh existing_deduction_accounts = payment.deduction_line_ids.mapped('substract_account_id') - - # We identify deduction lines as those not being liquidity or counterpart - # and having an account from the deduction list other_lines = payment.move_id.line_ids - liquidity_lines - counterpart_lines - writeoff_lines to_delete = other_lines.filtered(lambda l: l.account_id.id in existing_deduction_accounts.ids) if to_delete: - to_delete.unlink() + # Pass context to unlink as well + to_delete.with_context(skip_expense_lock=True).unlink() # Re-add deduction lines for deduction in payment.deduction_line_ids: @@ -79,8 +81,6 @@ class AccountPayment(models.Model): payment.date, ) - # For outbound (Send Money): Deductions are CREDITS - # For inbound (Receive Money): Deductions are DEBITS (rare but possible) line_vals = { 'name': deduction.name or _('Payment Deduction: %s') % deduction.substract_account_id.name, 'move_id': payment.move_id.id, @@ -91,33 +91,25 @@ class AccountPayment(models.Model): 'credit': deduction_balance if payment.payment_type == 'outbound' else 0.0, 'account_id': deduction.substract_account_id.id, } - payment.env['account.move.line'].with_context(check_move_validity=False).create(line_vals) + payment.env['account.move.line'].with_context(skip_expense_lock=True, check_move_validity=False).create(line_vals) # Force re-balance check - payment.move_id._check_balanced() - + payment.move_id.with_context(skip_expense_lock=True)._check_balanced() def _synchronize_from_moves(self, changed_fields): # 1. Standard sync with jumper support - if self._context.get('skip_expense_lock'): - mro = type(self).mro() - hr_expense_class = next((c for c in mro if c.__module__.startswith('odoo.addons.hr_expense')), None) - if hr_expense_class: - super(hr_expense_class, self)._synchronize_from_moves(changed_fields) - else: - super()._synchronize_from_moves(changed_fields) + hr_expense_class = self._get_hr_expense_base_class() + if self._context.get('skip_expense_lock') and hr_expense_class: + super(hr_expense_class, self)._synchronize_from_moves(changed_fields) else: super()._synchronize_from_moves(changed_fields) # 2. Custom Deduction Restoration - # If we have deductions, we must restore the gross amount from the move counterpart lines, - # because the bank line (which base Odoo syncs from) only contains final_payment_amount. - for payment in self.with_context(skip_account_move_synchronization=True): + for payment in self.with_context(skip_expense_lock=True, skip_account_move_synchronization=True): if payment.amount_substract and payment.amount_substract > 0 and payment.move_id: liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines() non_liquidity_lines = counterpart_lines + writeoff_lines - # Gross amount is sum of counterparts that balance the liquidity if payment.payment_type == 'outbound': gross_lines = non_liquidity_lines.filtered(lambda l: l.debit > 0) else: @@ -132,18 +124,27 @@ class AccountPayment(models.Model): (correct_gross_amount, payment.id) ) payment.invalidate_recordset(['amount']) - # Ensure computed fields are re-evaluated payment._compute_final_payment_amount() - def write(self, vals): - # Propagate bypass flag during writes to avoid locked checks + # Propagate bypass flag during writes + # If we have the flag, we jump over hr_expense to avoid its 'linked to expense' error + hr_expense_class = self._get_hr_expense_base_class() + if hr_expense_class and self._context.get('skip_expense_lock'): + return super(hr_expense_class, self).write(vals) + if self.expense_sheet_id and self.state == 'draft': return super(AccountPayment, self.with_context(skip_expense_lock=True)).write(vals) return super().write(vals) def action_cancel(self): - res = super().action_cancel() + # Bypass locks for cancellations as well + hr_expense_class = self._get_hr_expense_base_class() + if hr_expense_class: + res = super(hr_expense_class, self.with_context(skip_expense_lock=True)).action_cancel() + else: + res = super().action_cancel() + for payment in self: if payment.expense_sheet_id: payment.expense_sheet_id.invalidate_recordset(['state']) @@ -152,10 +153,17 @@ class AccountPayment(models.Model): return res def action_draft(self): - res = super().action_draft() + # Bypass locks for resetting to draft + hr_expense_class = self._get_hr_expense_base_class() + if hr_expense_class: + res = super(hr_expense_class, self.with_context(skip_expense_lock=True)).action_draft() + else: + res = super().action_draft() + for payment in self: if payment.expense_sheet_id: payment.expense_sheet_id.invalidate_recordset(['state']) if payment.realization_id and payment.realization_id.expense_sheet_id: payment.realization_id.expense_sheet_id.invalidate_recordset(['state']) return res +