# -*- 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