Compare commits

...

3 Commits
main ... 19.0

27 changed files with 105 additions and 69 deletions

0
.gitignore vendored Normal file → Executable file
View File

0
AMOUNT_FIX_GUIDE.md Normal file → Executable file
View File

0
CHANGELOG.md Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

0
__init__.py Normal file → Executable file
View File

2
__manifest__.py Normal file → Executable file
View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
{ {
'name': 'Vendor Payment Diff Amount', 'name': 'Vendor Payment Diff Amount',
'version': '17.0.2.1.0', 'version': '19.0.2.1.0',
'category': 'Accounting/Accounting', 'category': 'Accounting/Accounting',
'summary': 'Support multiple payment deductions for vendor payments (withholding tax, fees, etc.)', 'summary': 'Support multiple payment deductions for vendor payments (withholding tax, fees, etc.)',
'description': """ 'description': """

Binary file not shown.

20
fix_amount_issue.py Normal file → Executable file
View File

@ -24,23 +24,25 @@ def fix_payment_amounts():
fixed_count = 0 fixed_count = 0
for payment in payments: for payment in payments:
if payment.move_id: if payment.move_id:
# Find the counterpart line (payable/expense line with debit) # Robust logic to find gross amount from moves
counterpart_lines = payment.move_id.line_ids.filtered( liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
lambda l: l.debit > 0 and l.account_id.account_type in ('liability_payable', 'expense') non_liquidity_lines = counterpart_lines + writeoff_lines
)
if counterpart_lines: if payment.payment_type == 'outbound':
correct_amount = counterpart_lines[0].debit 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_amount = sum(abs(l.amount_currency) for l in gross_lines)
current_amount = payment.amount current_amount = payment.amount
# Check if amount needs fixing (allow for small rounding differences) # Check if amount needs fixing (allow for small rounding differences)
if abs(current_amount - correct_amount) > 0.01: if abs(current_amount - correct_amount) > 0.001:
print(f"Payment {payment.name} (ID: {payment.id}):") print(f"Payment {payment.name} (ID: {payment.id}):")
print(f" Current amount: {current_amount}") print(f" Current amount: {current_amount}")
print(f" Correct amount: {correct_amount}") print(f" Correct amount: {correct_amount}")
print(f" Deductions: {payment.amount_substract}") print(f" Deductions: {payment.amount_substract}")
print(f" Current final: {payment.final_payment_amount}")
print(f" Expected final: {correct_amount - payment.amount_substract}")
# Fix the amount using SQL to avoid triggering computed fields # Fix the amount using SQL to avoid triggering computed fields
env.cr.execute( env.cr.execute(

0
models/__init__.py Normal file → Executable file
View File

0
models/account_batch_payment.py Normal file → Executable file
View File

77
models/account_payment.py Normal file → Executable file
View File

@ -100,13 +100,18 @@ class AccountPayment(models.Model):
if payment.amount_substract and payment.amount_substract > 0: if payment.amount_substract and payment.amount_substract > 0:
# Get the correct amount from the journal entry # Get the correct amount from the journal entry
if payment.move_id: if payment.move_id:
# Find the counterpart line (payable/expense line) liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
counterpart_lines = payment.move_id.line_ids.filtered( non_liquidity_lines = counterpart_lines + writeoff_lines
lambda l: l.account_id.account_type in ('liability_payable', 'expense') and l.debit > 0
) # Find gross amount lines (Debits for outbound, Credits for inbound)
if counterpart_lines: if payment.payment_type == 'outbound':
correct_amount = counterpart_lines[0].debit gross_lines = non_liquidity_lines.filtered(lambda l: l.debit > 0)
if abs(payment.amount - correct_amount) > 0.01: # Allow for rounding differences else:
gross_lines = non_liquidity_lines.filtered(lambda l: l.credit > 0)
if gross_lines:
correct_amount = sum(abs(l.amount_currency) for l in gross_lines)
if abs(payment.amount - correct_amount) > 0.001:
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
_logger.info(f"Fixing amount for payment {payment.id}: {payment.amount} -> {correct_amount}") _logger.info(f"Fixing amount for payment {payment.id}: {payment.amount} -> {correct_amount}")
@ -133,14 +138,19 @@ class AccountPayment(models.Model):
fixed_count = 0 fixed_count = 0
for payment in payments: for payment in payments:
if payment.move_id: if payment.move_id:
# Find the counterpart line (payable/expense line with debit) # Robust logic to find gross amount from moves
counterpart_lines = payment.move_id.line_ids.filtered( liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
lambda l: l.debit > 0 and l.account_id.account_type in ('liability_payable', 'expense') non_liquidity_lines = counterpart_lines + writeoff_lines
)
if counterpart_lines: # Find gross amount lines (Debits for outbound, Credits for inbound)
correct_amount = counterpart_lines[0].debit if payment.payment_type == 'outbound':
if abs(payment.amount - correct_amount) > 0.01: 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_amount = sum(abs(l.amount_currency) for l in gross_lines)
if abs(payment.amount - correct_amount) > 0.001:
# Fix using SQL to avoid sync issues # Fix using SQL to avoid sync issues
payment.env.cr.execute( payment.env.cr.execute(
"UPDATE account_payment SET amount = %s WHERE id = %s", "UPDATE account_payment SET amount = %s WHERE id = %s",
@ -165,36 +175,49 @@ class AccountPayment(models.Model):
Override to handle synchronization when we have deductions. Override to handle synchronization when we have deductions.
When we have a substract amount, the bank credit line is reduced to final_payment_amount, When we have a substract amount, the bank credit line is reduced to final_payment_amount,
but we want to keep the payment amount at the original value (not sync it down). but we want to keep the payment amount at the original gross value.
""" """
# For payments with deductions, we need to handle synchronization carefully # For payments with deductions, we need to handle synchronization carefully
for payment in self: for payment in self:
if payment.amount_substract and payment.amount_substract > 0: if payment.amount_substract and payment.amount_substract > 0:
# Store the original amount before any synchronization # Store potential original values
original_amount = payment.amount original_amount = payment.amount
original_substract = payment.amount_substract
# Try to call parent sync but handle any errors # Call parent sync
try: try:
super(AccountPayment, payment)._synchronize_from_moves(changed_fields) super(AccountPayment, payment)._synchronize_from_moves(changed_fields)
except Exception as e: except Exception as e:
# If there's an error (like missing payable account when using expense_account_id),
# that's expected, so we just continue
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
_logger.info(f"Sync error for payment {payment.id} (expected with deductions): {e}") _logger.debug(f"Sync info for payment {payment.id} (handling deductions): {e}")
# After sync, ensure the amount is still correct # Restore gross amount from the move's counterpart lines
# The sync might have changed it based on journal entry lines if payment.move_id:
if payment.amount != original_amount: liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
# The gross amount is the sum of counterpart/writeoff lines that balance the liquidity line
# For outbound (Send Money): Gross = Sum of Debits (excluding liquidity)
# For inbound (Receive Money): Gross = Sum of Credits (excluding liquidity)
non_liquidity_lines = counterpart_lines + writeoff_lines
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:
# Use amount_currency because it represents the amount in payment currency
correct_gross_amount = sum(abs(l.amount_currency) for l in gross_lines)
if abs(payment.amount - correct_gross_amount) > 0.001:
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
_logger.info(f"Restoring amount for payment {payment.id}: {payment.amount} -> {original_amount}") _logger.info(f"Restoring gross amount for payment {payment.id}: {payment.amount} -> {correct_gross_amount}")
# Use SQL to restore the original amount without triggering more syncs # Use SQL to restore to avoid triggering more syncs
payment.env.cr.execute( payment.env.cr.execute(
"UPDATE account_payment SET amount = %s WHERE id = %s", "UPDATE account_payment SET amount = %s WHERE id = %s",
(original_amount, payment.id) (correct_gross_amount, payment.id)
) )
payment.invalidate_recordset(['amount']) payment.invalidate_recordset(['amount'])

2
models/payment_deduction_line.py Normal file → Executable file
View File

@ -58,7 +58,7 @@ class PaymentDeductionLine(models.Model):
'account.account', 'account.account',
string='Deduction Account', string='Deduction Account',
required=True, required=True,
domain="[('account_type', 'not in', ['asset_cash', 'asset_cash_bank', 'asset_receivable', 'liability_payable']), ('deprecated', '=', False)]", domain="[('account_type', 'not in', ['asset_cash', 'asset_cash_bank', 'asset_receivable', 'liability_payable']), ('active', '=', True)]",
help='Account where the deduction will be recorded (use tax payable or expense accounts, NOT payable/receivable accounts)', help='Account where the deduction will be recorded (use tax payable or expense accounts, NOT payable/receivable accounts)',
) )
name = fields.Char( name = fields.Char(

0
security/ir.model.access.csv Normal file → Executable file
View File

0
tests/__init__.py Normal file → Executable file
View File

5
tests/test_account_payment.py Normal file → Executable file
View File

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from odoo import fields from odoo import fields
from odoo.tests import TransactionCase from odoo.tests import TransactionCase, tagged
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from hypothesis import given, strategies as st, settings from hypothesis import given, strategies as st, settings
@tagged('post_install', '-at_install')
class TestAccountPayment(TransactionCase): class TestAccountPayment(TransactionCase):
"""Test cases for vendor payment deduction functionality""" """Test cases for vendor payment deduction functionality"""
@ -37,7 +38,7 @@ class TestAccountPayment(TransactionCase):
'name': 'Withholding Tax Account', 'name': 'Withholding Tax Account',
'code': 'WHT001', 'code': 'WHT001',
'account_type': 'expense', 'account_type': 'expense',
'company_id': self.env.company.id, 'company_ids': [self.env.company.id],
}) })
@given( @given(

3
tests/test_batch_payment_integration.py Normal file → Executable file
View File

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from odoo import fields from odoo import fields
from odoo.tests import TransactionCase from odoo.tests import TransactionCase, tagged
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
@tagged('post_install', '-at_install')
class TestBatchPaymentIntegration(TransactionCase): class TestBatchPaymentIntegration(TransactionCase):
"""Test cases for batch payment integration with deduction functionality""" """Test cases for batch payment integration with deduction functionality"""

12
views/account_batch_payment_views.xml Normal file → Executable file
View File

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Add deduction lines field to batch payment line tree view --> <!-- Add deduction lines field to batch payment line list view -->
<record id="view_batch_payment_form_inherit_diff_amount" model="ir.ui.view"> <record id="view_batch_payment_form_inherit_diff_amount" model="ir.ui.view">
<field name="name">account.batch.payment.form.inherit.diff.amount</field> <field name="name">account.batch.payment.form.inherit.diff.amount</field>
<field name="model">account.batch.payment</field> <field name="model">account.batch.payment</field>
<field name="inherit_id" ref="vendor_batch_payment_merge.view_batch_payment_form_inherit"/> <field name="inherit_id" ref="vendor_batch_payment_merge.view_batch_payment_form_inherit"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<!-- Replace the tree view with one that includes a form view for deduction lines --> <!-- Replace the list view with one that includes a form view for deduction lines -->
<xpath expr="//field[@name='direct_payment_line_ids']" position="replace"> <xpath expr="//field[@name='direct_payment_line_ids']" position="replace">
<field name="direct_payment_line_ids"> <field name="direct_payment_line_ids">
<tree editable="bottom"> <list editable="bottom">
<field name="partner_id" domain="parent.batch_type == 'outbound' and [('supplier_rank', '>', 0)] or [('customer_rank', '>', 0)]" options="{'no_create': True}"/> <field name="partner_id" domain="parent.batch_type == 'outbound' and [('supplier_rank', '>', 0)] or [('customer_rank', '>', 0)]" options="{'no_create': True}"/>
<field name="amount" sum="Total"/> <field name="amount" sum="Total"/>
<field name="expense_account_id"/> <field name="expense_account_id"/>
@ -17,7 +17,7 @@
<field name="memo"/> <field name="memo"/>
<field name="date"/> <field name="date"/>
<field name="payment_id" readonly="1"/> <field name="payment_id" readonly="1"/>
</tree> </list>
<form> <form>
<group> <group>
<group name="payment_info"> <group name="payment_info">
@ -32,13 +32,13 @@
</group> </group>
<group string="Deductions" name="deductions"> <group string="Deductions" name="deductions">
<field name="deduction_line_ids" nolabel="1" context="{'default_currency_id': currency_id}"> <field name="deduction_line_ids" nolabel="1" context="{'default_currency_id': currency_id}">
<tree editable="bottom"> <list editable="bottom">
<field name="sequence" widget="handle"/> <field name="sequence" widget="handle"/>
<field name="substract_account_id" required="1"/> <field name="substract_account_id" required="1"/>
<field name="name" placeholder="Description (optional)"/> <field name="name" placeholder="Description (optional)"/>
<field name="amount_substract" required="1" sum="Total"/> <field name="amount_substract" required="1" sum="Total"/>
<field name="currency_id" column_invisible="1"/> <field name="currency_id" column_invisible="1"/>
</tree> </list>
</field> </field>
<field name="amount_substract" readonly="1"/> <field name="amount_substract" readonly="1"/>
</group> </group>

4
views/account_payment_views.xml Normal file → Executable file
View File

@ -12,14 +12,14 @@
invisible="payment_type != 'outbound'" invisible="payment_type != 'outbound'"
readonly="state != 'draft'" readonly="state != 'draft'"
nolabel="1"> nolabel="1">
<tree editable="bottom"> <list editable="bottom">
<field name="sequence" widget="handle"/> <field name="sequence" widget="handle"/>
<field name="substract_account_id" required="1"/> <field name="substract_account_id" required="1"/>
<field name="name" placeholder="Description (optional)"/> <field name="name" placeholder="Description (optional)"/>
<field name="amount_substract" required="1" sum="Total Deductions"/> <field name="amount_substract" required="1" sum="Total Deductions"/>
<field name="currency_id" column_invisible="1"/> <field name="currency_id" column_invisible="1"/>
<field name="company_id" column_invisible="1"/> <field name="company_id" column_invisible="1"/>
</tree> </list>
</field> </field>
<label for="amount_substract" string="Total Deductions" <label for="amount_substract" string="Total Deductions"

0
wizard/__init__.py Normal file → Executable file
View File

33
wizard/payment_amount_fix_wizard.py Normal file → Executable file
View File

@ -28,12 +28,17 @@ class PaymentAmountFixWizard(models.TransientModel):
payments_to_fix = [] payments_to_fix = []
for payment in payments: for payment in payments:
if payment.move_id: if payment.move_id:
counterpart_lines = payment.move_id.line_ids.filtered( liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
lambda l: l.debit > 0 and l.account_id.account_type in ('liability_payable', 'expense') non_liquidity_lines = counterpart_lines + writeoff_lines
)
if counterpart_lines: if payment.payment_type == 'outbound':
correct_amount = counterpart_lines[0].debit gross_lines = non_liquidity_lines.filtered(lambda l: l.debit > 0)
if abs(payment.amount - correct_amount) > 0.01: else:
gross_lines = non_liquidity_lines.filtered(lambda l: l.credit > 0)
if gross_lines:
correct_amount = sum(abs(l.amount_currency) for l in gross_lines)
if abs(payment.amount - correct_amount) > 0.001:
payments_to_fix.append(payment.id) payments_to_fix.append(payment.id)
res['payment_ids'] = [(6, 0, payments_to_fix)] res['payment_ids'] = [(6, 0, payments_to_fix)]
@ -45,13 +50,17 @@ class PaymentAmountFixWizard(models.TransientModel):
for payment in self.payment_ids: for payment in self.payment_ids:
if payment.move_id: if payment.move_id:
counterpart_lines = payment.move_id.line_ids.filtered( liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
lambda l: l.debit > 0 and l.account_id.account_type in ('liability_payable', 'expense') non_liquidity_lines = counterpart_lines + writeoff_lines
)
if counterpart_lines: if payment.payment_type == 'outbound':
correct_amount = counterpart_lines[0].debit gross_lines = non_liquidity_lines.filtered(lambda l: l.debit > 0)
if abs(payment.amount - correct_amount) > 0.01: else:
gross_lines = non_liquidity_lines.filtered(lambda l: l.credit > 0)
if gross_lines:
correct_amount = sum(abs(l.amount_currency) for l in gross_lines)
if abs(payment.amount - correct_amount) > 0.001:
# Fix using SQL to avoid sync issues # Fix using SQL to avoid sync issues
payment.env.cr.execute( payment.env.cr.execute(
"UPDATE account_payment SET amount = %s WHERE id = %s", "UPDATE account_payment SET amount = %s WHERE id = %s",

4
wizard/payment_amount_fix_wizard_views.xml Normal file → Executable file
View File

@ -12,14 +12,14 @@
</p> </p>
<group> <group>
<field name="payment_ids" nolabel="1"> <field name="payment_ids" nolabel="1">
<tree> <list>
<field name="name"/> <field name="name"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="amount"/> <field name="amount"/>
<field name="amount_substract"/> <field name="amount_substract"/>
<field name="final_payment_amount"/> <field name="final_payment_amount"/>
<field name="date"/> <field name="date"/>
</tree> </list>
</field> </field>
</group> </group>
<footer> <footer>