303 lines
15 KiB
Python
303 lines
15 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
|
|
calculated_final = payment.amount - amount_substract
|
|
|
|
# Debug logging
|
|
import logging
|
|
_logger = logging.getLogger(__name__)
|
|
_logger.info(f"Computing final payment amount for payment {payment.id}: amount={payment.amount}, substract={amount_substract}, final={calculated_final}")
|
|
|
|
payment.final_payment_amount = currency.round(calculated_final)
|
|
|
|
@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 write(self, vals):
|
|
"""
|
|
Override write to handle amount field changes when we have deductions.
|
|
This prevents Odoo's synchronization from incorrectly changing the amount based on journal entries.
|
|
"""
|
|
# If amount is being changed and we have deductions, we need to be careful
|
|
if 'amount' in vals:
|
|
for payment in self:
|
|
if payment.amount_substract and payment.amount_substract > 0:
|
|
# Log the attempted change for debugging
|
|
import logging
|
|
_logger = logging.getLogger(__name__)
|
|
_logger.info(f"Amount change attempted from {payment.amount} to {vals['amount']} for payment {payment.id} with deductions {payment.amount_substract}")
|
|
|
|
# If the payment is posted, don't allow amount changes
|
|
# This prevents synchronization from messing up the amount
|
|
if payment.state == 'posted':
|
|
_logger.info(f"Preventing amount change for posted payment {payment.id}")
|
|
vals = vals.copy()
|
|
del vals['amount']
|
|
break
|
|
|
|
return super().write(vals)
|
|
|
|
def action_fix_amount_calculation(self):
|
|
"""
|
|
Action to fix amount calculation for payments with deductions.
|
|
This can be used to correct payments that have incorrect amounts due to synchronization issues.
|
|
"""
|
|
for payment in self:
|
|
if payment.amount_substract and payment.amount_substract > 0:
|
|
# Get the correct amount from the journal entry
|
|
if payment.move_id:
|
|
# Find the counterpart line (payable/expense line)
|
|
counterpart_lines = payment.move_id.line_ids.filtered(
|
|
lambda l: l.account_id.account_type in ('liability_payable', 'expense') and l.debit > 0
|
|
)
|
|
if counterpart_lines:
|
|
correct_amount = counterpart_lines[0].debit
|
|
if abs(payment.amount - correct_amount) > 0.01: # Allow for rounding differences
|
|
import logging
|
|
_logger = logging.getLogger(__name__)
|
|
_logger.info(f"Fixing amount for payment {payment.id}: {payment.amount} -> {correct_amount}")
|
|
|
|
# Use SQL to update directly to avoid triggering computed fields
|
|
payment.env.cr.execute(
|
|
"UPDATE account_payment SET amount = %s WHERE id = %s",
|
|
(correct_amount, payment.id)
|
|
)
|
|
payment.invalidate_recordset(['amount'])
|
|
payment._compute_final_payment_amount()
|
|
|
|
@api.model
|
|
def fix_all_payment_amounts(self):
|
|
"""
|
|
Model method to fix all payments with deduction amount calculation issues.
|
|
Can be called from code or through RPC.
|
|
"""
|
|
payments = self.search([
|
|
('amount_substract', '>', 0),
|
|
('state', '=', 'posted'),
|
|
])
|
|
|
|
fixed_count = 0
|
|
for payment in payments:
|
|
if payment.move_id:
|
|
# Find the counterpart line (payable/expense line with debit)
|
|
counterpart_lines = payment.move_id.line_ids.filtered(
|
|
lambda l: l.debit > 0 and l.account_id.account_type in ('liability_payable', 'expense')
|
|
)
|
|
|
|
if counterpart_lines:
|
|
correct_amount = counterpart_lines[0].debit
|
|
if abs(payment.amount - correct_amount) > 0.01:
|
|
# Fix using SQL to avoid sync issues
|
|
payment.env.cr.execute(
|
|
"UPDATE account_payment SET amount = %s WHERE id = %s",
|
|
(correct_amount, payment.id)
|
|
)
|
|
payment.invalidate_recordset(['amount'])
|
|
payment._compute_final_payment_amount()
|
|
fixed_count += 1
|
|
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'title': _('Amount Fix Complete'),
|
|
'message': _('Fixed %d payments with incorrect amounts.') % fixed_count,
|
|
'type': 'success',
|
|
}
|
|
}
|
|
|
|
def _synchronize_from_moves(self, changed_fields):
|
|
"""
|
|
Override to handle synchronization when we have deductions.
|
|
|
|
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).
|
|
"""
|
|
# For payments with deductions, we need to handle synchronization carefully
|
|
for payment in self:
|
|
if payment.amount_substract and payment.amount_substract > 0:
|
|
# Store the original amount before any synchronization
|
|
original_amount = payment.amount
|
|
original_substract = payment.amount_substract
|
|
|
|
# Try to call parent sync but handle any errors
|
|
try:
|
|
super(AccountPayment, payment)._synchronize_from_moves(changed_fields)
|
|
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
|
|
_logger = logging.getLogger(__name__)
|
|
_logger.info(f"Sync error for payment {payment.id} (expected with deductions): {e}")
|
|
|
|
# After sync, ensure the amount is still correct
|
|
# The sync might have changed it based on journal entry lines
|
|
if payment.amount != original_amount:
|
|
import logging
|
|
_logger = logging.getLogger(__name__)
|
|
_logger.info(f"Restoring amount for payment {payment.id}: {payment.amount} -> {original_amount}")
|
|
|
|
# Use SQL to restore the original amount without triggering more syncs
|
|
payment.env.cr.execute(
|
|
"UPDATE account_payment SET amount = %s WHERE id = %s",
|
|
(original_amount, payment.id)
|
|
)
|
|
payment.invalidate_recordset(['amount'])
|
|
|
|
# Ensure final_payment_amount is recalculated correctly
|
|
payment._compute_final_payment_amount()
|
|
else:
|
|
# No deductions - use standard synchronization
|
|
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
|