first commit
This commit is contained in:
commit
4012e6f4c9
75
README.md
Normal file
75
README.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Payment Residual Display
|
||||||
|
|
||||||
|
This module adds residual amount display to customer and vendor payments in Odoo 17.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Residual Amount Field**: Shows the unreconciled amount remaining on each payment
|
||||||
|
- **List View Integration**: Adds "Residual" column to payment list views (hidden by default)
|
||||||
|
- **Form View Integration**: Displays residual amount in payment form view
|
||||||
|
- **Multi-Currency Support**: Handles both company currency and payment currency residuals
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Fields Added
|
||||||
|
|
||||||
|
### `payment_residual`
|
||||||
|
- **Type**: Monetary (computed)
|
||||||
|
- **Currency**: Payment currency
|
||||||
|
- **Purpose**: Shows the residual amount not yet reconciled
|
||||||
|
- **Computation**: Based on `move_id.line_ids.amount_residual` and `amount_residual_currency`
|
||||||
|
|
||||||
|
### `payment_residual_currency`
|
||||||
|
- **Type**: Monetary (computed)
|
||||||
|
- **Currency**: Payment currency
|
||||||
|
- **Purpose**: Shows the residual amount in the payment's currency
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### In List View
|
||||||
|
1. Go to **Accounting → Customers → Payments** (or **Vendors → Payments**)
|
||||||
|
2. Click the column selector (☰ icon)
|
||||||
|
3. Enable the "Residual" column
|
||||||
|
4. You'll see the unreconciled amount for each payment
|
||||||
|
|
||||||
|
### In Form View
|
||||||
|
1. Open any customer or vendor payment
|
||||||
|
2. The "Residual Amount" field appears after the payment amount
|
||||||
|
3. Shows 0.00 for fully reconciled payments
|
||||||
|
4. Shows the remaining amount for partially reconciled payments
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- `account` - Core Accounting module
|
||||||
|
|
||||||
|
### Inheritance
|
||||||
|
- Extends: `account.payment`
|
||||||
|
- Views inherited:
|
||||||
|
- `account.view_account_payment_tree`
|
||||||
|
- `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
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Copy the module to your Odoo addons directory
|
||||||
|
2. Update the apps list
|
||||||
|
3. Install "Payment Residual Display"
|
||||||
|
4. No additional configuration needed
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Created for Odoo 17 accounting workflow enhancement.
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
26
__manifest__.py
Normal file
26
__manifest__.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
'name': 'Payment Residual Display',
|
||||||
|
'version': '17.0.1.0.0',
|
||||||
|
'category': 'Accounting',
|
||||||
|
'summary': 'Display residual amounts in customer payment lines',
|
||||||
|
'description': """
|
||||||
|
This module adds residual amount display to customer payments.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Shows residual amount in payment list view
|
||||||
|
- Shows residual amount in payment form view
|
||||||
|
- Computes residual from journal items automatically
|
||||||
|
- Helps identify partially reconciled payments
|
||||||
|
|
||||||
|
See README.md for more details.
|
||||||
|
""",
|
||||||
|
'author': 'Suherdy Yacob',
|
||||||
|
'depends': ['account'],
|
||||||
|
'data': [
|
||||||
|
'views/account_payment_views.xml',
|
||||||
|
],
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
'application': False,
|
||||||
|
}
|
||||||
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
62
models/__init__.py
Normal file
62
models/__init__.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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
|
||||||
BIN
models/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/account_payment.cpython-310.pyc
Normal file
BIN
models/__pycache__/account_payment.cpython-310.pyc
Normal file
Binary file not shown.
86
models/account_payment.py
Normal file
86
models/account_payment.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
payment_residual_currency = fields.Monetary(
|
||||||
|
string='Payment Residual Currency',
|
||||||
|
compute='_compute_payment_residual',
|
||||||
|
currency_field='currency_id',
|
||||||
|
help="Residual amount in payment currency",
|
||||||
|
readonly=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('move_id.line_ids.amount_residual',
|
||||||
|
'move_id.line_ids.amount_residual_currency',
|
||||||
|
'move_id.line_ids.reconciled')
|
||||||
|
def _compute_payment_residual(self):
|
||||||
|
"""Compute the residual amount from payment journal items
|
||||||
|
|
||||||
|
For testing: show all residual amounts regardless of reconciliation status
|
||||||
|
"""
|
||||||
|
for pay in self:
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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'))
|
||||||
|
|
||||||
|
# For debugging - let's try multiple approaches:
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
elif 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)
|
||||||
41
views/account_payment_views.xml
Normal file
41
views/account_payment_views.xml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Extend the customer payment list view to show residual amount -->
|
||||||
|
<record id="account_payment_view_tree_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">account.payment.tree.inherit</field>
|
||||||
|
<field name="model">account.payment</field>
|
||||||
|
<field name="inherit_id" ref="account.view_account_payment_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<!-- Add residual field after amount field -->
|
||||||
|
<field name="amount_company_currency_signed" position="after">
|
||||||
|
<field name="payment_residual"
|
||||||
|
widget="monetary"
|
||||||
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
string="Residual"
|
||||||
|
optional="hide"/>
|
||||||
|
<field name="payment_residual_currency"
|
||||||
|
widget="monetary"
|
||||||
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
string="Residual Currency"
|
||||||
|
optional="hide"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Extend the customer payment form view to show residual amount -->
|
||||||
|
<record id="account_payment_view_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">account.payment.form.inherit</field>
|
||||||
|
<field name="model">account.payment</field>
|
||||||
|
<field name="inherit_id" ref="account.view_account_payment_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<!-- Add residual field after amount in the amount_div -->
|
||||||
|
<field name="amount" position="after">
|
||||||
|
<field name="payment_residual"
|
||||||
|
widget="monetary"
|
||||||
|
options="{'currency_field': 'currency_id'}"
|
||||||
|
string="Residual Amount"
|
||||||
|
readonly="1"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user