From 0bdbede031fd90f06403378c2a2166d02c5203e0 Mon Sep 17 00:00:00 2001 From: "admin.suherdy" Date: Fri, 21 Nov 2025 17:41:28 +0700 Subject: [PATCH] fix the calculation for residual --- CHANGELOG.md | 28 ++++++++++++++ README.md | 19 ++++++---- models/__init__.py | 61 ----------------------------- models/account_payment.py | 80 ++++++++++++++++----------------------- 4 files changed, 71 insertions(+), 117 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d46aebe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## [17.0.1.0.1] - 2025-11-21 + +### Fixed +- **Bank Statement Matching Issue**: Fixed residual amounts showing incorrectly when payments are already reconciled/matched with bank statements +- **Unmatched Payment Issue**: Fixed unmatched payments showing 0 residual when they should show actual residual amounts + +### Changed +- Modified `_compute_payment_residual()` to check `is_matched` status only +- If payment is matched with bank statement (`is_matched=True`), residual is always 0 +- For unmatched payments, calculates residual from receivable/payable lines only +- Excluded liquidity account lines (bank/cash accounts) from residual calculation +- Removed `is_reconciled` check as it was causing false positives + +### Technical Details +The issues occurred because: +1. When a payment is matched with a bank statement, the `is_matched` flag is set to True +2. The previous implementation was calculating residuals regardless of matching status +3. Even though the payment was matched, the receivable/payable line could still show a residual if not applied to an invoice +4. Using `is_reconciled` check caused unmatched payments to show 0 residual incorrectly + +The fix: +- Only checks `is_matched` flag - if True, returns 0 residual +- This ensures matched payments always show 0 residual +- For unmatched payments, calculates residual from receivable/payable lines +- Excludes liquidity account lines to focus on actual customer/vendor balances +- This correctly shows residuals for unmatched payments while hiding them for matched ones diff --git a/README.md b/README.md index 9968bc3..c7a199e 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,10 @@ This module adds residual amount display to customer and vendor payments in Odoo The module extends the `account.payment` model with a computed field that: 1. Retrieves the journal entry lines created by the payment -2. Identifies the counterpart lines (receivable/payable accounts) and write-off lines -3. Sums up their `amount_residual` or `amount_residual_currency` values -4. Displays the total unreconciled amount +2. Identifies only the counterpart lines (receivable/payable accounts) +3. Excludes liquidity account lines (bank/cash) to avoid false residuals when matched with bank statements +4. Sums up their `amount_residual` or `amount_residual_currency` values based on currency +5. Displays the total unreconciled amount ## Fields Added @@ -57,11 +58,13 @@ The module extends the `account.payment` model with a computed field that: - `account.view_account_payment_form` ### Computation Logic -The residual is computed using the same approach as Odoo's built-in `is_reconciled` field: -- Uses `_seek_for_lines()` to identify counterpart and write-off lines -- Filters for reconcilable accounts -- Sums the `amount_residual` or `amount_residual_currency` based on currency matching -- This ensures consistency with Odoo's core reconciliation logic +The residual is computed by: +- Filtering only receivable/payable account lines (excludes liquidity accounts) +- This prevents showing residuals when payments are matched with bank statements +- Sums the `amount_residual` or `amount_residual_currency` based on payment currency +- Uses company currency residual for same-currency payments +- Uses foreign currency residual for multi-currency payments +- This ensures accurate residual display regardless of bank statement matching ## Installation diff --git a/models/__init__.py b/models/__init__.py index e2e41b7..d357b42 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,62 +1 @@ -from odoo import api, fields, models - - -class AccountPayment(models.Model): - _inherit = 'account.payment' - - # Computed residual amount field - payment_residual = fields.Monetary( - string='Payment Residual', - compute='_compute_payment_residual', - currency_field='currency_id', - help="Residual amount of this payment (amount not yet reconciled)", - readonly=True - ) - - # One2many to show journal items with residual amounts - payment_move_line_ids = fields.One2many( - 'account.move.line', - 'payment_id', - string='Payment Journal Items', - domain=lambda self: [ - ('account_id.reconcile', '=', True), - ('account_id.account_type', 'in', ['asset_receivable', 'liability_payable']) - ], - readonly=True - ) - - @api.depends('payment_move_line_ids.amount_residual', - 'payment_move_line_ids.account_id') - def _compute_payment_residual(self): - """Compute the residual amount from payment journal items""" - for payment in self: - if payment.state in ['draft', 'cancel'] or not payment.payment_move_line_ids: - payment.payment_residual = 0.0 - else: - # Get all reconcilable journal items for this payment - reconcilable_lines = payment.payment_move_line_ids.filtered( - lambda l: l.account_id.reconcile and - l.account_id.account_type in ['asset_receivable', 'liability_payable'] - ) - - # Sum up the residual amounts - total_residual = sum(reconcilable_lines.mapped('amount_residual')) - - # For display purposes, show absolute value with proper sign - payment.payment_residual = total_residual - - def action_view_journal_items(self): - """Action to view the payment's journal items""" - self.ensure_one() - return { - 'name': _('Payment Journal Items'), - 'view_mode': 'tree,form', - 'res_model': 'account.move.line', - 'domain': [('payment_id', '=', self.id)], - 'context': { - 'default_payment_id': self.id, - 'search_default_payment_id': self.id, - }, - 'type': 'ir.actions.act_window', - } from . import account_payment \ No newline at end of file diff --git a/models/account_payment.py b/models/account_payment.py index fc3169f..155d5e7 100644 --- a/models/account_payment.py +++ b/models/account_payment.py @@ -23,64 +23,48 @@ class AccountPayment(models.Model): @api.depends('move_id.line_ids.amount_residual', 'move_id.line_ids.amount_residual_currency', - 'move_id.line_ids.reconciled') + 'move_id.line_ids.reconciled', + 'is_matched') def _compute_payment_residual(self): """Compute the residual amount from payment journal items - For testing: show all residual amounts regardless of reconciliation status + Shows 0 residual when payment is matched with bank statement. + Otherwise shows the residual from all reconcilable lines except liquidity accounts. """ for pay in self: + # If payment is matched with bank statement, no residual to show + if pay.is_matched: + pay.payment_residual = 0.0 + pay.payment_residual_currency = 0.0 + continue + if not pay.move_id: pay.payment_residual = 0.0 pay.payment_residual_currency = 0.0 continue - # Get ALL move lines - let's see what's there - all_lines = pay.move_id.line_ids + # Get all reconcilable lines except liquidity accounts + # This includes receivable, payable, and other reconcilable accounts + # but excludes bank/cash accounts that get matched with statements + reconcilable_lines = pay.move_id.line_ids.filtered( + lambda line: line.account_id.reconcile and + line.account_id.account_type not in ('asset_cash', 'liability_credit_card') + ) - # Sum ALL residual amounts to see what's happening - total_residual = sum(all_lines.mapped('amount_residual')) - total_residual_currency = sum(all_lines.mapped('amount_residual_currency')) + # Calculate residual based on currency + residual = 0.0 + residual_currency = 0.0 - # For debugging - let's try multiple approaches: + for line in reconcilable_lines: + # Always add to company currency residual + residual += line.amount_residual + + # For foreign currency, use amount_residual_currency + if line.currency_id and line.currency_id != pay.company_id.currency_id: + residual_currency += line.amount_residual_currency + else: + residual_currency += line.amount_residual - # Approach 1: All lines residual - #approach1_residual = total_residual - #approach1_residual_currency = total_residual_currency - - # Approach 2: Only reconcilable account lines - reconcilable_lines = all_lines.filtered(lambda line: line.account_id.reconcile) - approach2_residual = sum(reconcilable_lines.mapped('amount_residual')) - approach2_residual_currency = sum(reconcilable_lines.mapped('amount_residual_currency')) - - # Approach 3: Receivable/Payable account lines only - rec_pay_lines = all_lines.filtered(lambda line: line.account_id.account_type in ('asset_receivable', 'liability_payable')) - approach3_residual = sum(rec_pay_lines.mapped('amount_residual')) - approach3_residual_currency = sum(rec_pay_lines.mapped('amount_residual_currency')) - - # For now, let's use the approach that gives us the largest non-zero value - # This will help us identify which approach works - candidates = [ - #abs(approach1_residual), - abs(approach2_residual), - abs(approach3_residual), - #abs(approach1_residual_currency), - abs(approach2_residual_currency), - abs(approach3_residual_currency) - ] - - max_value = max(candidates) if candidates else 0.0 - - #if abs(approach1_residual) == max_value: - # pay.payment_residual = abs(approach1_residual) - # pay.payment_residual_currency = abs(approach1_residual_currency) - if abs(approach2_residual) == max_value: - pay.payment_residual = abs(approach2_residual) - pay.payment_residual_currency = abs(approach2_residual_currency) - elif abs(approach3_residual) == max_value: - pay.payment_residual = abs(approach3_residual) - pay.payment_residual_currency = abs(approach3_residual_currency) - #else: - # Fallback to currency residuals - #pay.payment_residual = abs(approach1_residual_currency) - #pay.payment_residual_currency = abs(approach1_residual_currency) + # Store absolute values + pay.payment_residual = abs(residual) + pay.payment_residual_currency = abs(residual_currency)