From beedab36e0daa248e7c243bb07bea5b48963f8a8 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 6 Apr 2026 13:37:33 +0700 Subject: [PATCH] feat: integrate vendor_payment_diff_amount and implement robust synchronization for payment deductions and gross amounts --- __manifest__.py | 2 +- models/account_payment.py | 103 +++++++++++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/__manifest__.py b/__manifest__.py index 1fac19b..c5e25a1 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -4,7 +4,7 @@ 'summary': 'Set different expense accounts for Employee paid vs Company paid expenses on Expense Categories.', 'category': 'Human Resources/Expenses', 'author': 'Suherdy Yacob', - 'depends': ['hr_expense', 'account'], + 'depends': ['hr_expense', 'account', 'vendor_payment_diff_amount'], 'data': [ 'security/ir.model.access.csv', 'data/ir_sequence_data.xml', diff --git a/models/account_payment.py b/models/account_payment.py index b489e01..e54d137 100644 --- a/models/account_payment.py +++ b/models/account_payment.py @@ -25,7 +25,8 @@ class AccountPayment(models.Model): return super(AccountPayment, self.with_context(skip_expense_lock=True)).action_post() def _synchronize_to_moves(self, changed_fields): - # Force the refresh by ensuring 'amount' is in fields if deductions are involved + # 1. Standard synchronization first + # We ensure 'amount' is in changed_fields if deductions are involved to trigger base sync if 'deduction_line_ids' in changed_fields or 'amount_substract' in changed_fields: if 'amount' not in changed_fields: changed_fields = set(changed_fields) | {'amount'} @@ -35,19 +36,105 @@ class AccountPayment(models.Model): 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: - return super(hr_expense_class, self)._synchronize_to_moves(changed_fields) - - return super()._synchronize_to_moves(changed_fields) + super(hr_expense_class, self)._synchronize_to_moves(changed_fields) + else: + super()._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): + 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: + final_amount = payment.final_payment_amount + line_vals = { + 'amount_currency': -final_amount if payment.payment_type == 'outbound' else final_amount, + 'debit': payment.currency_id._convert(final_amount, payment.company_id.currency_id, payment.company_id, payment.date) if payment.payment_type == 'inbound' else 0.0, + 'credit': payment.currency_id._convert(final_amount, payment.company_id.currency_id, payment.company_id, payment.date) if payment.payment_type == 'outbound' else 0.0, + } + 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() + + # Re-add deduction lines + for deduction in payment.deduction_line_ids: + deduction_amount = deduction.amount_substract + deduction_balance = payment.currency_id._convert( + deduction_amount, + payment.company_id.currency_id, + payment.company_id, + 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, + 'date_maturity': payment.date, + 'amount_currency': -deduction_amount if payment.payment_type == 'outbound' else deduction_amount, + 'currency_id': payment.currency_id.id, + 'debit': deduction_balance if payment.payment_type == 'inbound' else 0.0, + '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) + + # Force re-balance check + payment.move_id._check_balanced() + def _synchronize_from_moves(self, changed_fields): - # Same jumper for synchronization back from moves + # 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: - return super(hr_expense_class, self)._synchronize_from_moves(changed_fields) - - return super()._synchronize_from_moves(changed_fields) + super(hr_expense_class, self)._synchronize_from_moves(changed_fields) + else: + super()._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): + 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: + gross_lines = non_liquidity_lines.filtered(lambda l: l.credit > 0) + + if gross_lines: + correct_gross_amount = sum(abs(l.amount_currency) for l in gross_lines) + if abs(payment.amount - correct_gross_amount) > 0.001: + # Use SQL to avoid triggering more syncs, then invalidate cache + payment.env.cr.execute( + "UPDATE account_payment SET amount = %s WHERE id = %s", + (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