# -*- 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 amount_substract = fields.Monetary( string='Amount Substract', currency_field='currency_id', help='Amount to be deducted from the payment (e.g., withholding tax, fees)', readonly=False, ) substract_account_id = fields.Many2one( 'account.account', string='Substract Account', domain="[('account_type', 'not in', ['asset_cash', 'asset_cash_bank']), ('deprecated', '=', False), ('company_id', '=', company_id)]", help='Account where the deduction will be recorded', readonly=False, ) 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('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') def _check_amount_substract(self): for payment in self: if payment.amount_substract and payment.amount_substract < 0: raise ValidationError(_("Amount Substract cannot be negative.")) if payment.amount_substract and payment.amount_substract > payment.amount: raise ValidationError(_("Amount Substract cannot be greater than the payment amount.")) @api.constrains('amount_substract', 'substract_account_id') def _check_substract_account(self): for payment in self: if payment.amount_substract > 0 and not payment.substract_account_id: raise ValidationError(_("Please select a Substract Account when Amount Substract is specified.")) 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 substract account line when amount_substract > 0. This method modifies the journal entry to: 1. Reduce the payable debit line to final_payment_amount 2. Add a new debit line for the substract account 3. Keep the bank credit line at the original amount The resulting entry for outbound payment (amount=1000, substract=100): - Payable: debit 900 (final_payment_amount) - Substract: debit 100 (amount_substract) - Bank: credit 1000 (original amount) Total: debit 1000 = credit 1000 (balanced) Requirements: 4.1, 4.2, 4.3, 4.4, 4.5 """ # 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 a deduction amount and account if self.amount_substract and self.amount_substract > 0 and self.substract_account_id: # For outbound payments, we need to: # - Keep the payable debit (counterpart line) at the original amount # - Add a credit line for the substract account (reduction) # - Reduce the bank credit (liquidity line) to final_payment_amount if self.payment_type == 'outbound': # Check if substract line already exists (to prevent duplicates) has_substract_line = any( line.get('account_id') == self.substract_account_id.id for line in line_vals_list ) if not has_substract_line: # 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 account liquidity_line = line_vals_list[0] # Convert amount_substract to company currency for the journal entry substract_balance = self.currency_id._convert( self.amount_substract, self.company_id.currency_id, self.company_id, self.date, ) # Don't adjust the liquidity (bank) line - keep it at the original amount # The bank credit should be the original amount (requirement 4.4) # Adjust the counterpart (payable) line - reduce the debit to final_payment_amount # For outbound payment: # - Original: amount_currency = amount, debit = amount # - Modified: amount_currency = final_payment_amount, debit = final_payment_amount counterpart_line = line_vals_list[1] final_balance = self.currency_id._convert( self.final_payment_amount, self.company_id.currency_id, self.company_id, self.date, ) counterpart_line['amount_currency'] = self.final_payment_amount counterpart_line['debit'] = final_balance # Create the substract account line (DEBIT - requirement 4.3) substract_line_name = _('Payment Deduction: %s') % self.substract_account_id.name substract_line = { 'name': substract_line_name, 'date_maturity': self.date, 'amount_currency': self.amount_substract, # Positive because it's a debit 'currency_id': self.currency_id.id, 'debit': substract_balance, 'credit': 0.0, 'partner_id': self.partner_id.id, 'account_id': self.substract_account_id.id, } # Add the substract line to the list line_vals_list.append(substract_line) return line_vals_list