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: The module extends the `account.payment` model with a computed field that:
1. Retrieves the journal entry lines created by the payment 1. Retrieves the journal entry lines created by the payment
2. Identifies the counterpart lines (receivable/payable accounts) and write-off lines 2. Identifies only the counterpart lines (receivable/payable accounts)
3. Sums up their `amount_residual` or `amount_residual_currency` values 3. Excludes liquidity account lines (bank/cash) to avoid false residuals when matched with bank statements
4. Displays the total unreconciled amount 4. Sums up their `amount_residual` or `amount_residual_currency` values based on currency
5. Displays the total unreconciled amount
## Fields Added ## Fields Added
@ -57,11 +58,13 @@ The module extends the `account.payment` model with a computed field that:
- `account.view_account_payment_form` - `account.view_account_payment_form`
### Computation Logic ### Computation Logic
The residual is computed using the same approach as Odoo's built-in `is_reconciled` field: The residual is computed by:
- Uses `_seek_for_lines()` to identify counterpart and write-off lines - Filtering only receivable/payable account lines (excludes liquidity accounts)
- Filters for reconcilable accounts - This prevents showing residuals when payments are matched with bank statements
- Sums the `amount_residual` or `amount_residual_currency` based on currency matching - Sums the `amount_residual` or `amount_residual_currency` based on payment currency
- This ensures consistency with Odoo's core reconciliation logic - 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 ## 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 from . import account_payment

View File

@ -23,64 +23,48 @@ class AccountPayment(models.Model):
@api.depends('move_id.line_ids.amount_residual', @api.depends('move_id.line_ids.amount_residual',
'move_id.line_ids.amount_residual_currency', 'move_id.line_ids.amount_residual_currency',
'move_id.line_ids.reconciled') 'move_id.line_ids.reconciled',
'is_matched')
def _compute_payment_residual(self): def _compute_payment_residual(self):
"""Compute the residual amount from payment journal items """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: 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: if not pay.move_id:
pay.payment_residual = 0.0 pay.payment_residual = 0.0
pay.payment_residual_currency = 0.0 pay.payment_residual_currency = 0.0
continue continue
# Get ALL move lines - let's see what's there # Get all reconcilable lines except liquidity accounts
all_lines = pay.move_id.line_ids # 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 # Calculate residual based on currency
total_residual = sum(all_lines.mapped('amount_residual')) residual = 0.0
total_residual_currency = sum(all_lines.mapped('amount_residual_currency')) 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
# Approach 1: All lines residual # For foreign currency, use amount_residual_currency
#approach1_residual = total_residual if line.currency_id and line.currency_id != pay.company_id.currency_id:
#approach1_residual_currency = total_residual_currency residual_currency += line.amount_residual_currency
else:
residual_currency += line.amount_residual
# Approach 2: Only reconcilable account lines # Store absolute values
reconcilable_lines = all_lines.filtered(lambda line: line.account_id.reconcile) pay.payment_residual = abs(residual)
approach2_residual = sum(reconcilable_lines.mapped('amount_residual')) pay.payment_residual_currency = abs(residual_currency)
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)