vendor_payment_diff_amount/models/account_payment.py

208 lines
10 KiB
Python

# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class AccountPayment(models.Model):
_inherit = 'account.payment'
# Flag to prevent infinite recursion during synchronization
_skip_amount_sync = False
deduction_line_ids = fields.One2many(
'payment.deduction.line',
'payment_id',
string='Deductions',
help='Payment deductions (e.g., withholding tax, fees)',
readonly=False,
)
amount_substract = fields.Monetary(
string='Total Deductions',
currency_field='currency_id',
compute='_compute_amount_substract',
store=True,
help='Total amount to be deducted from the payment',
)
final_payment_amount = fields.Monetary(
string='Final Payment Amount',
currency_field='currency_id',
compute='_compute_final_payment_amount',
store=True,
help='Actual amount to be paid after deductions',
)
@api.depends('deduction_line_ids.amount_substract')
def _compute_amount_substract(self):
for payment in self:
payment.amount_substract = sum(payment.deduction_line_ids.mapped('amount_substract'))
@api.depends('amount', 'amount_substract', 'currency_id')
def _compute_final_payment_amount(self):
for payment in self:
amount_substract = payment.amount_substract or 0.0
currency = payment.currency_id or payment.company_id.currency_id
payment.final_payment_amount = currency.round(payment.amount - amount_substract)
@api.constrains('amount', 'amount_substract', 'expense_account_id', 'deduction_line_ids')
def _check_amount_substract(self):
for payment in self:
if payment.amount_substract and payment.amount_substract < 0:
raise ValidationError(_("Total deductions cannot be negative."))
if payment.amount_substract and payment.amount_substract > payment.amount:
raise ValidationError(_("Total deductions cannot be greater than the payment amount."))
# Require expense account when using deductions
if payment.deduction_line_ids and not payment.expense_account_id:
raise ValidationError(_(
"Expense Account is required when using payment deductions.\n\n"
"Please set the Expense Account field before adding deductions."
))
def _synchronize_from_moves(self, changed_fields):
"""
Override to prevent amount synchronization when we have a substract 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).
Also handles the case where expense_account_id is used (from vendor_batch_payment_merge),
which replaces the payable account with an expense account.
"""
# Handle multiple records - process each payment individually
for payment in self:
# When expense_account_id is used with substract amount, the journal entry doesn't have
# a payable/receivable account. This causes Odoo's validation to fail.
# We need to skip the validation in this case.
if payment.expense_account_id and payment.amount_substract and payment.amount_substract > 0:
try:
result = super(AccountPayment, payment)._synchronize_from_moves(changed_fields)
except Exception as e:
# If validation fails due to missing payable/receivable account, it's expected
if 'receivable/payable account' in str(e):
# This is expected - just continue to next payment
continue
else:
# Re-raise other exceptions
raise
continue
# If we have a substract amount (but no expense_account_id), we need to handle the sync differently
if payment.amount_substract and payment.amount_substract > 0:
# Store the original amount before sync
original_amount = payment.amount
original_substract = payment.amount_substract
# Call parent sync
result = super(AccountPayment, payment)._synchronize_from_moves(changed_fields)
# Restore the original amount if it was changed by sync
if payment.amount != original_amount:
# Use write to update without triggering another sync
super(AccountPayment, payment).write({
'amount': original_amount,
'amount_substract': original_substract,
})
# Force recomputation of final_payment_amount
payment._compute_final_payment_amount()
else:
super(AccountPayment, payment)._synchronize_from_moves(changed_fields)
def _prepare_move_line_default_vals(self, write_off_line_vals=None, force_balance=None):
"""
Override to add deduction lines when deductions exist.
This method modifies the journal entry to:
1. Keep the payable/expense debit line at the original amount
2. Add credit lines for each deduction account
3. Reduce the bank credit line to final_payment_amount
The resulting entry for outbound payment (amount=2000, deductions=150):
- Expense/Payable: debit 2000 (original amount)
- PPh 21: credit 100 (deduction)
- PPh 29: credit 50 (deduction)
- Bank: credit 1850 (final_payment_amount)
Total: debit 2000 = credit 2000 (balanced)
"""
# Get standard line values from parent
line_vals_list = super()._prepare_move_line_default_vals(write_off_line_vals, force_balance)
# Only modify if we have deductions
if self.amount_substract and self.amount_substract > 0 and self.deduction_line_ids:
if self.payment_type == 'outbound':
# Get existing deduction account IDs to prevent duplicates
existing_deduction_accounts = {
line.get('account_id')
for line in line_vals_list
if line.get('account_id') in self.deduction_line_ids.mapped('substract_account_id').ids
}
if not existing_deduction_accounts:
# The liquidity line is the first line (index 0) - this is the bank account
# The counterpart line is the second line (index 1) - this is the payable/expense account
# Adjust the bank (liquidity) line - reduce the credit to final_payment_amount
liquidity_line = line_vals_list[0]
final_balance = self.currency_id._convert(
self.final_payment_amount,
self.company_id.currency_id,
self.company_id,
self.date,
)
liquidity_line['amount_currency'] = -self.final_payment_amount
liquidity_line['credit'] = final_balance
# The counterpart line should remain at the original amount
# Ensure it has all required fields (partner_id, account_id, etc.)
if len(line_vals_list) < 2:
# Something is wrong with the parent method, skip modifications
return line_vals_list
counterpart_line = line_vals_list[1]
# CRITICAL: Ensure the counterpart line has proper account and partner
# Get the account to check its type
account_id = counterpart_line.get('account_id')
if account_id:
account = self.env['account.account'].browse(account_id)
# Only set partner_id if the account requires it (payable/receivable)
if account.account_type in ('asset_receivable', 'liability_payable'):
counterpart_line['partner_id'] = self.partner_id.id
else:
# No account_id set, use destination_account_id
counterpart_line['account_id'] = self.destination_account_id.id
# Check if destination account requires partner
if self.destination_account_id.account_type in ('asset_receivable', 'liability_payable'):
counterpart_line['partner_id'] = self.partner_id.id
# Create a deduction line for each deduction (CREDIT)
for deduction in self.deduction_line_ids:
# Convert deduction amount to company currency
deduction_balance = self.currency_id._convert(
deduction.amount_substract,
self.company_id.currency_id,
self.company_id,
self.date,
)
# Create the deduction line (CREDIT)
# Note: Deduction accounts should be tax/expense accounts, not payable/receivable
# Therefore, we don't add partner_id to these lines
deduction_line_name = deduction.name or _('Payment Deduction: %s') % deduction.substract_account_id.name
deduction_line = {
'name': deduction_line_name,
'date_maturity': self.date,
'amount_currency': -deduction.amount_substract, # Negative for credit
'currency_id': self.currency_id.id,
'debit': 0.0,
'credit': deduction_balance,
'account_id': deduction.substract_account_id.id,
# No partner_id - deduction accounts are typically tax/expense accounts
}
# Add the deduction line to the list
line_vals_list.append(deduction_line)
return line_vals_list