From 02911abaa5e7c76da14c68873de6882bb0d71590 Mon Sep 17 00:00:00 2001 From: sergiocorato Date: Wed, 26 Jun 2024 22:32:05 +0200 Subject: [PATCH] [FIX] l10n_it_withholding_tax: pay multiple invoices with expenses Co-authored-by: Simone Rubino --- l10n_it_withholding_tax/models/account.py | 101 +++++++++-- .../models/withholding_tax.py | 9 +- .../tests/test_withholding_tax.py | 171 +++++++++++++++--- .../views/withholding_tax.xml | 2 - .../wizards/account_payment_register.py | 49 ++++- 5 files changed, 282 insertions(+), 50 deletions(-) diff --git a/l10n_it_withholding_tax/models/account.py b/l10n_it_withholding_tax/models/account.py index eac54d14c55d..933fa06ddde3 100644 --- a/l10n_it_withholding_tax/models/account.py +++ b/l10n_it_withholding_tax/models/account.py @@ -54,10 +54,7 @@ def _wt_get_paying_invoice(self, move_lines): # If we are reconciling a vendor bill and its refund, # we do not need to generate Withholding Tax Moves # or change the reconciliation amount - in_refunding = len(invoices) == 2 and set(invoices.mapped("move_type")) == { - "in_invoice", - "in_refund", - } + in_refunding = invoices._wt_in_refunding() if not in_refunding: paying_invoice = first(invoices) else: @@ -72,30 +69,50 @@ def create(self, vals): debit_move_line, credit_move_line = self._wt_get_move_lines(vals) move_lines = debit_move_line | credit_move_line paying_invoice = self._wt_get_paying_invoice(move_lines) + reconcile_existing = False # Limit value of reconciliation if ( paying_invoice and paying_invoice.withholding_tax - and paying_invoice.amount_net_pay + and paying_invoice.amount_net_pay_residual ): # We must consider amount in foreign currency, if present # Note that this is always executed, for every reconciliation. # Thus, we must not change amount when not in withholding tax case amount = vals.get("amount_currency") or vals.get("amount") digits_rounding_precision = paying_invoice.company_id.currency_id.rounding + if amount == 0.0: + # it's a reconciliation with an existing move line + if ( + float_compare( + abs(debit_move_line.amount_residual), + abs(credit_move_line.amount_residual), + precision_rounding=digits_rounding_precision, + ) + == 0 + ): + amount = abs(move_lines[0].amount_residual) + vals.update( + { + "amount": amount, + "credit_amount_currency": amount, + "debit_amount_currency": amount, + } + ) + reconcile_existing = True if ( float_compare( amount, - paying_invoice.amount_net_pay, + paying_invoice.amount_net_pay_residual, precision_rounding=digits_rounding_precision, ) == 1 ): vals.update( { - "amount": paying_invoice.amount_net_pay, - "credit_amount_currency": paying_invoice.amount_net_pay, - "debit_amount_currency": paying_invoice.amount_net_pay, + "amount": paying_invoice.amount_net_pay_residual, + "credit_amount_currency": paying_invoice.amount_net_pay_residual, + "debit_amount_currency": paying_invoice.amount_net_pay_residual, } ) @@ -106,11 +123,26 @@ def create(self, vals): moves = move_lines.move_id lines = self.env["account.move.line"].search( - [("withholding_tax_generated_by_move_id", "in", moves.ids)] + [ + ("withholding_tax_generated_by_move_id", "in", moves.ids), + ("balance", "=", abs(vals.get("amount"))), + ] ) + if not lines: + for move in moves.filtered(lambda x: x.withholding_tax_amount): + lines = self.env["account.move.line"].search( + [ + ("withholding_tax_generated_by_move_id", "in", moves.ids), + ("balance", "=", abs(move.withholding_tax_amount)), + ("name", "=ilike", move.name), + ] + ) + if lines: + reconcile_existing = True if lines: is_wt_move = True - reconcile.generate_wt_moves(is_wt_move, lines) + if not reconcile_existing: + reconcile.generate_wt_moves(is_wt_move, lines) else: is_wt_move = False @@ -127,7 +159,8 @@ def create(self, vals): ) ): # and not wt_existing_moves\ - reconcile.generate_wt_moves(is_wt_move) + if not reconcile_existing: + reconcile.generate_wt_moves(is_wt_move) return reconcile @@ -152,7 +185,7 @@ def generate_wt_moves(self, is_wt_move, lines=None): wt_statements = wt_statement_obj.browse() rec_line_statement = rec_line_model.browse() for rec_line in rec_lines: - domain = [("move_id", "=", rec_line.move_id.id)] + domain = [("invoice_id", "=", rec_line.move_id.id)] wt_statements = wt_statement_obj.search(domain) if wt_statements: rec_line_statement = rec_line @@ -470,7 +503,6 @@ def create_wt_statement(self): val = { "wt_type": "", "date": self.date, - "move_id": self.id, "invoice_id": self.id, "partner_id": self.partner_id.id, "withholding_tax_id": inv_wt.withholding_tax_id.id, @@ -507,7 +539,7 @@ def _wt_unlink_statements(self): if posted_moves: statements = self.env["withholding.tax.statement"].search( [ - ("move_id", "in", posted_moves.ids), + ("invoice_id", "in", posted_moves.ids), ], ) statements.unlink() @@ -518,6 +550,12 @@ def write(self, vals): self._wt_unlink_statements() return super().write(vals) + def _wt_in_refunding(self): + return len(self) == 2 and set(self.mapped("move_type")) == { + "in_invoice", + "in_refund", + } + class AccountMoveLine(models.Model): _inherit = "account.move.line" @@ -558,6 +596,39 @@ def remove_move_reconcile(self): return super(AccountMoveLine, self).remove_move_reconcile() + def _prepare_reconciliation_partials(self): + wt_move_lines = self.filtered(lambda x: x.withholding_tax_amount != 0) + if not wt_move_lines: + return super()._prepare_reconciliation_partials() + credit_lines = self.filtered(lambda line: line.credit) + debit_line_ids = (self - credit_lines).ids + partials_vals_list = [] + for credit_line in credit_lines: + next_debit_line_id = debit_line_ids.pop(0) if debit_line_ids else False + if next_debit_line_id: + debit_line = self.browse(next_debit_line_id) + total_amount_payment = debit_line.balance + invoices = (credit_line | debit_line).move_id.filtered( + lambda move: move.is_invoice() + ) + in_refunding = invoices._wt_in_refunding() + residual_amount = credit_line.move_id.amount_net_pay_residual + if not in_refunding and total_amount_payment > residual_amount: + total_amount_payment -= residual_amount + else: + residual_amount = total_amount_payment + partials_vals_list.append( + { + "amount": residual_amount, + "debit_amount_currency": residual_amount, + "credit_amount_currency": residual_amount, + "debit_move_id": debit_line.id, + "credit_move_id": credit_line.id, + } + ) + + return partials_vals_list + @api.model def _default_withholding_tax(self): result = [] diff --git a/l10n_it_withholding_tax/models/withholding_tax.py b/l10n_it_withholding_tax/models/withholding_tax.py index ff083b57da39..c837822a906c 100644 --- a/l10n_it_withholding_tax/models/withholding_tax.py +++ b/l10n_it_withholding_tax/models/withholding_tax.py @@ -226,7 +226,6 @@ def _compute_total(self): store=True, compute="_compute_type", ) - move_id = fields.Many2one("account.move", "Account move", ondelete="cascade") invoice_id = fields.Many2one("account.move", "Invoice", ondelete="cascade") partner_id = fields.Many2one("res.partner", "Partner") withholding_tax_id = fields.Many2one("withholding.tax", string="Withholding Tax") @@ -243,12 +242,12 @@ def _compute_total(self): ) move_ids = fields.One2many("withholding.tax.move", "statement_id", "Moves") - @api.depends("move_id.line_ids.account_id.user_type_id.type") + @api.depends("invoice_id.line_ids.account_id.user_type_id.type") def _compute_type(self): for st in self: - if st.move_id: + if st.invoice_id: domain = [ - ("move_id", "=", st.move_id.id), + ("move_id", "=", st.invoice_id.id), ("account_id.user_type_id.type", "=", "payable"), ] lines = self.env["account.move.line"].search(domain) @@ -279,7 +278,7 @@ def get_wt_competence(self, amount_reconcile): ) if st.invoice_id.move_type in ["in_refund", "out_refund"]: amount_wt = -1 * amount_wt - elif st.move_id: + elif st.invoice_id: tax_data = st.withholding_tax_id.compute_tax(amount_reconcile) amount_wt = tax_data["tax"] return amount_wt diff --git a/l10n_it_withholding_tax/tests/test_withholding_tax.py b/l10n_it_withholding_tax/tests/test_withholding_tax.py index 638b57509c26..a383180d0d5b 100644 --- a/l10n_it_withholding_tax/tests/test_withholding_tax.py +++ b/l10n_it_withholding_tax/tests/test_withholding_tax.py @@ -54,6 +54,19 @@ def setUp(self): } self.payment_term_15 = self.env["account.payment.term"].create(vals_payment) + self.account_expense, self.account_expense1 = self.env[ + "account.account" + ].search( + [ + ( + "user_type_id", + "=", + self.env.ref("account.data_account_type_expenses").id, + ) + ], + limit=2, + ) + # Withholding tax wt_vals = { "name": "Code 1040", @@ -83,18 +96,7 @@ def setUp(self): 0, { "quantity": 1.0, - "account_id": self.env["account.account"] - .search( - [ - ( - "user_type_id", - "=", - self.env.ref("account.data_account_type_expenses").id, - ) - ], - limit=1, - ) - .id, + "account_id": self.account_expense.id, "name": "Advice", "price_unit": 1000.00, "invoice_line_tax_wt_ids": [(6, 0, [self.wt1040.id])], @@ -241,18 +243,7 @@ def test_keep_selected_wt(self): 0, { "quantity": 1.0, - "account_id": self.env["account.account"] - .search( - [ - ( - "user_type_id", - "=", - self.env.ref("account.data_account_type_expenses").id, - ) - ], - limit=1, - ) - .id, + "account_id": self.account_expense.id, "name": "Advice", "price_unit": 1000.00, "tax_ids": False, @@ -303,7 +294,7 @@ def _get_statements(self, move): """Get statements linked to `move`.""" statements = self.env["withholding.tax.statement"].search( [ - ("move_id", "=", move.id), + ("invoice_id", "=", move.id), ], ) return statements @@ -445,7 +436,7 @@ def test_wt_after_repost(self): self.assertEqual(self.invoice.amount_residual, 250) self.assertEqual(self.invoice.state, "posted") - def _create_bill(self): + def _create_bill(self, price_unit=1000.0): bill_model = self.env["account.move"].with_context( default_move_type="in_invoice", ) @@ -454,11 +445,21 @@ def _create_bill(self): bill_form.partner_id = self.env.ref("base.res_partner_12") with bill_form.invoice_line_ids.new() as line: line.name = "Advice" - line.price_unit = 1000 + line.price_unit = price_unit line.invoice_line_tax_wt_ids.clear() line.invoice_line_tax_wt_ids.add(self.wt1040) + line.tax_ids.clear() bill = bill_form.save() bill.action_post() + + wt_statement_ids = self.env["withholding.tax.statement"].search( + [ + ("invoice_id", "=", bill.id), + ("withholding_tax_id", "=", self.wt1040.id), + ] + ) + self.assertEqual(len(wt_statement_ids), 1) + return bill def _get_refund(self, bill): @@ -536,3 +537,119 @@ def test_refund_wt_moves(self): ] ) self.assertFalse(withholding_tax_moves) + + def test_multi_invoice_with_payment(self): + invoice = self._create_bill(price_unit=477.19) # wt 95.44 net 486.73 + invoice1 = self._create_bill(price_unit=13.10) # wt 2.62 net 13.36 + invoice2 = self._create_bill(price_unit=100.00) # wt 20.00 net 102.00 + invoice3 = self._create_bill(price_unit=48.40) # wt 9.68 net 49.37 + invoice4 = self._create_bill(price_unit=48.40) # wt 9.68 net 49.37 + # we add 0.50 to the total paid for bank expenses + invoices = invoice | invoice1 | invoice2 | invoice3 | invoice4 + ctx = { + "active_model": "account.move", + "active_ids": invoices.ids, + } + register_payments = ( + self.env["account.payment.register"] + .with_context(ctx) + .create( + { + "payment_date": fields.Date.today().replace(month=7, day=15), + "amount": 486.73 + 13.36 + 102 + 49.37 + 49.37 + 0.50, + "group_payment": True, + "payment_difference_handling": "reconcile", + "writeoff_account_id": self.account_expense1.id, + "writeoff_label": "Bank expense", + "journal_id": self.journal_bank.id, + "payment_method_id": self.env.ref( + "account.account_payment_method_manual_out" + ).id, + } + ) + ) + payment_action = register_payments.action_create_payments() + payment_id = payment_action["res_id"] + payment = self.env["account.payment"].browse(payment_id) + self.assertEqual(payment.reconciled_bill_ids.ids, invoices.ids) + statements = self.env["withholding.tax.statement"].search( + [ + ("invoice_id", "in", invoices.ids), + ], + ) + self.assertEqual(len(statements), len(invoices)) + self.assertAlmostEqual( + sum(x.tax for x in statements), 95.44 + 2.62 + 20 + 9.68 + 9.68 + ) + wh_move_ids = statements.mapped("move_ids.wt_account_move_id") + self.assertEqual(len(wh_move_ids), len(statements)) + + def test_multi_invoice_with_partial_payment(self): + invoice = self._create_bill(price_unit=100) # wt 20 net 80 + invoice1 = self._create_bill(price_unit=150) # wt 30 net 120 + invoice2 = self._create_bill(price_unit=2000) # wt 400 net 1600 + self.assertAlmostEqual(invoice.amount_net_pay_residual, 80) + # pay partially the first invoice, it's impossible to register bank expenses + ctx = { + "active_model": "account.move", + "active_ids": invoice.id, + } + register_payments = ( + self.env["account.payment.register"] + .with_context(ctx) + .create( + { + "payment_date": fields.Date.today().replace(month=7, day=15), + "amount": 10, + "group_payment": True, + "payment_difference_handling": "open", + "journal_id": self.journal_bank.id, + "payment_method_id": self.env.ref( + "account.account_payment_method_manual_out" + ).id, + } + ) + ) + payment_action = register_payments.action_create_payments() + payment_id = payment_action["res_id"] + payment = self.env["account.payment"].browse(payment_id) + self.assertEqual(payment.reconciled_bill_ids.ids, invoice.ids) + self.assertAlmostEqual(invoice.amount_net_pay_residual, 70) + # Payment the residual of the first invoice and the others, with 0.50 + # for bank expenses + invoices = invoice | invoice1 | invoice2 + ctx = { + "active_model": "account.move", + "active_ids": invoices.ids, + } + register_payments = ( + self.env["account.payment.register"] + .with_context(ctx) + .create( + { + "payment_date": fields.Date.today().replace(month=7, day=15), + "amount": 970 + 130 + 1600 + 0.50, + "group_payment": True, + "payment_difference_handling": "reconcile", + "writeoff_account_id": self.account_expense1.id, + "writeoff_label": "Bank expense", + "journal_id": self.journal_bank.id, + "payment_method_id": self.env.ref( + "account.account_payment_method_manual_out" + ).id, + } + ) + ) + payment_action = register_payments.action_create_payments() + payment_id = payment_action["res_id"] + payment = self.env["account.payment"].browse(payment_id) + self.assertEqual(payment.reconciled_bill_ids.ids, invoices.ids) + statements = self.env["withholding.tax.statement"].search( + [ + ("invoice_id", "in", invoices.ids), + ], + ) + self.assertEqual(len(statements), len(invoices)) + self.assertAlmostEqual(sum(x.tax for x in statements), 1.96 + 18.04 + 30 + 400) + wh_move_ids = statements.mapped("move_ids.wt_account_move_id") + self.assertEqual(len(wh_move_ids), 4) diff --git a/l10n_it_withholding_tax/views/withholding_tax.xml b/l10n_it_withholding_tax/views/withholding_tax.xml index d67806142f7e..8d2a55eb5464 100644 --- a/l10n_it_withholding_tax/views/withholding_tax.xml +++ b/l10n_it_withholding_tax/views/withholding_tax.xml @@ -119,7 +119,6 @@ - @@ -152,7 +151,6 @@ -