fix the calculation for residual

This commit is contained in:
admin.suherdy 2025-11-21 17:41:28 +07:00
parent 9b5deb2f87
commit 0bdbede031
4 changed files with 71 additions and 117 deletions

28
CHANGELOG.md Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)