From 586b72e0c9740e0640e8271219ccea02ea286c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nshimiyimana=20S=C3=A9na?= Date: Fri, 14 Apr 2023 07:08:12 +0000 Subject: [PATCH] [FIX] account: fix interaction between early payment discount and intra-community taxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: Currently, creating an intra-community bill and reconciling it with an early payment can break the tax report. Steps to reproduce: 1. install the Austrian localization (l10n_at) 2. set `Cash Discount Tax Reduction` to `On early payment` 3. create a €1000 intra-community bill (you can just use the tax called `IGE 20%`) 4. set the payment term to`2/7 Net 30` and confirm 5. reconcile the bill with a payment within the discount period (2% discount: €980). 6. check the Tax Report: line 5.4 of the report should be €196. If you reconciled the bill with a back statement directly, sections `Innergemeinschaftliche Erwerb` and `Bemessungsgrundlage` will be wrong as well. Cause: Since the intra-community applies here, the bill will produce two tax lines. And because `Cash Discount Tax Reduction` is set to `On early payment`, those two tax lines will be reduced when an early payment is made. However, because of the way the `is_refund` property of `account.move.line` field is computed on moves of type `entry`, one of those *tax reduction line* will be considered a refund and the other will not. Furthermore, the computed tags are correct only because the taxes are recomputed again when creating the payment. After removing this extra taxes computation, both tax_tag_ids/tax_tag_invert are invalid. So the solution is to fix the method computing the taxes for cash discount lines. Note: This commit also fixes the cash discount engine not working with multiple tax repartition lines since: https://github.com/odoo/odoo/commit/9c134ec379819053e3fdd1e252e3a7ae8a949f42 opw-3112197 Enterprise PR: odoo/enterprise#39181 closes odoo/odoo#118588 X-original-commit: de0db2429f7c47d52c1bad37b9d35d425c8d5b74 Related: odoo/enterprise#39775 Signed-off-by: Laurent Smet Signed-off-by: Nshimiyimana Serge Séna (sesn) Co-authored-by: Laurent Smet --- addons/account/models/account_move.py | 56 +++++++++------ addons/account/models/account_payment.py | 14 ++-- .../tests/test_early_payment_discount.py | 72 +++++++++++++++++++ .../wizard/account_payment_register.py | 4 +- 4 files changed, 118 insertions(+), 28 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 6585b3ac91b..d61c6fddd40 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -2948,9 +2948,14 @@ class AccountMove(models.Model): def grouping_key_generator(base_line, tax_values): return self.env['account.tax']._get_generation_dict_from_base_line(base_line, tax_values) + def inverse_tax_rep(tax_rep): + tax = tax_rep.tax_id + index = list(tax.invoice_repartition_line_ids).index(tax_rep) + return tax.refund_repartition_line_ids[index] + # Get the current tax amounts in the current invoice. tax_amounts = { - line.tax_repartition_line_id.id: { + inverse_tax_rep(line.tax_repartition_line_id).id: { 'amount_currency': line.amount_currency, 'balance': line.balance, } @@ -2958,7 +2963,13 @@ class AccountMove(models.Model): } product_lines = self.line_ids.filtered(lambda x: x.display_type == 'product') - base_lines = [x._convert_to_tax_base_line_dict() for x in product_lines] + base_lines = [ + { + **x._convert_to_tax_base_line_dict(), + 'is_refund': True, + } + for x in product_lines + ] for base_line in base_lines: base_line['taxes'] = base_line['taxes'].filtered(lambda t: t.amount_type != 'fixed') @@ -2982,8 +2993,9 @@ class AccountMove(models.Model): term_amount_currency = payment_term_line.amount_currency - payment_term_line.discount_amount_currency term_balance = payment_term_line.balance - payment_term_line.discount_balance if early_pay_discount_computation == 'included' and product_lines.tax_ids: - # Compute the base amounts. + # Compute the base amounts. resulting_delta_base_details = {} + resulting_delta_tax_details = {} to_process = [] for base_line in base_lines: invoice_line = base_line['record'] @@ -3023,7 +3035,6 @@ class AccountMove(models.Model): grouping_key_generator=grouping_key_generator, ) - resulting_delta_tax_details = {} for tax_detail in tax_details_with_epd['tax_details'].values(): tax_amount_without_epd = tax_amounts.get(tax_detail['tax_repartition_line_id']) if not tax_amount_without_epd: @@ -3045,25 +3056,28 @@ class AccountMove(models.Model): # Multiply the amount by the percentage percentage_paid = abs(payment_term_line.amount_residual_currency / self.amount_total) - tax_rep = self.env['account.tax.repartition.line'].browse(tax_detail['tax_repartition_line_id']) - tax = tax_rep.tax_id + for tax_detail in resulting_delta_tax_details.values(): + tax_rep = self.env['account.tax.repartition.line'].browse(tax_detail['tax_repartition_line_id']) + tax = tax_rep.tax_id - grouping_dict = { - 'account_id': tax_detail['account_id'], - 'partner_id': tax_detail['partner_id'], - 'currency_id': tax_detail['currency_id'], - 'analytic_distribution': tax_detail['analytic_distribution'], - 'tax_repartition_line_id': tax_rep.id, - 'tax_ids': tax_detail['tax_ids'], - 'tax_tag_ids': tax_detail['tax_tag_ids'], - 'group_tax_id': tax_detail['tax_id'] if tax_detail['tax_id'] != tax.id else None, - } + grouping_dict = { + 'account_id': tax_detail['account_id'], + 'partner_id': tax_detail['partner_id'], + 'currency_id': tax_detail['currency_id'], + 'analytic_distribution': tax_detail['analytic_distribution'], + 'tax_repartition_line_id': tax_rep.id, + 'tax_ids': tax_detail['tax_ids'], + 'tax_tag_ids': tax_detail['tax_tag_ids'], + 'group_tax_id': tax_detail['tax_id'] if tax_detail['tax_id'] != tax.id else None, + } + + res['tax_lines'][payment_term_line][frozendict(grouping_dict)] = { + 'name': _("Early Payment Discount (%s)", tax.name), + 'amount_currency': payment_term_line.currency_id.round(tax_detail['amount_currency'] * percentage_paid), + 'balance': payment_term_line.company_currency_id.round(tax_detail['balance'] * percentage_paid), + 'tax_tag_invert': True, + } - res['tax_lines'][payment_term_line][frozendict(grouping_dict)] = { - 'name': _("Early Payment Discount (%s)", tax.name), - 'amount_currency': payment_term_line.currency_id.round(tax_detail['tax_amount_currency'] * percentage_paid), - 'balance': payment_term_line.company_currency_id.round(tax_detail['tax_amount'] * percentage_paid), - } for grouping_dict, base_detail in bases_details.items(): res['base_lines'][payment_term_line][grouping_dict] = { 'name': _("Early Payment Discount"), diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index c91d49dbfd7..a2b208ab0c0 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -873,12 +873,14 @@ class AccountPayment(models.Model): # Update the existing journal items. # If dealing with multiple write-off lines, they are dropped and a new one is generated. - pay.move_id.write({ - 'partner_id': pay.partner_id.id, - 'currency_id': pay.currency_id.id, - 'partner_bank_id': pay.partner_bank_id.id, - 'line_ids': line_ids_commands, - }) + pay.move_id\ + .with_context(skip_invoice_sync=True)\ + .write({ + 'partner_id': pay.partner_id.id, + 'currency_id': pay.currency_id.id, + 'partner_bank_id': pay.partner_bank_id.id, + 'line_ids': line_ids_commands, + }) def _create_paired_internal_transfer_payment(self): ''' When an internal transfer is posted, a paired payment is created diff --git a/addons/account/tests/test_early_payment_discount.py b/addons/account/tests/test_early_payment_discount.py index 7a475c85a56..f83ea1d83bc 100644 --- a/addons/account/tests/test_early_payment_discount.py +++ b/addons/account/tests/test_early_payment_discount.py @@ -375,3 +375,75 @@ class TestAccountEarlyPaymentDiscount(AccountTestInvoicingCommon): } ], }) + + def test_intracomm_bill_with_early_payment_included(self): + tax_tags = self.env['account.account.tag'].create({ + 'name': f'tax_tag_{i}', + 'applicability': 'taxes', + 'country_id': self.env.company.account_fiscal_country_id.id, + } for i in range(6)) + + intracomm_tax = self.env['account.tax'].create({ + 'name': 'tax20', + 'amount_type': 'percent', + 'amount': 20, + 'type_tax_use': 'purchase', + 'invoice_repartition_line_ids': [ + # pylint: disable=bad-whitespace + Command.create({'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[0].ids)]}), + Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[1].ids)]}), + Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'tag_ids': [Command.set(tax_tags[2].ids)]}), + ], + 'refund_repartition_line_ids': [ + # pylint: disable=bad-whitespace + Command.create({'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[3].ids)]}), + Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[4].ids)]}), + Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'tag_ids': [Command.set(tax_tags[5].ids)]}), + ], + }) + + early_payment_term = self.env['account.payment.term'].create({ + 'name': "early_payment_term", + 'company_id': self.company_data['company'].id, + 'early_pay_discount_computation': 'included', + 'early_discount': True, + 'discount_percentage': 2, + 'discount_days': 7, + 'line_ids': [ + Command.create({ + 'value': 'percent', + 'value_amount': 100.0, + 'nb_days': 30, + }), + ], + }) + + bill = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'partner_id': self.partner_a.id, + 'invoice_payment_term_id': early_payment_term.id, + 'invoice_date': '2019-01-01', + 'date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line', + 'price_unit': 1000.0, + 'tax_ids': [Command.set(intracomm_tax.ids)], + }), + ], + }) + bill.action_post() + + payment = self.env['account.payment.register']\ + .with_context(active_model='account.move', active_ids=bill.ids)\ + .create({'payment_date': '2019-01-01'})\ + ._create_payments() + + self.assertRecordValues(payment.line_ids.sorted('balance'), [ + # pylint: disable=bad-whitespace + {'amount_currency': -980.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_tag_invert': False}, + {'amount_currency': -20.0, 'tax_ids': intracomm_tax.ids, 'tax_tag_ids': tax_tags[3].ids, 'tax_tag_invert': True}, + {'amount_currency': -4.0, 'tax_ids': [], 'tax_tag_ids': tax_tags[4].ids, 'tax_tag_invert': True}, + {'amount_currency': 4.0, 'tax_ids': [], 'tax_tag_ids': tax_tags[5].ids, 'tax_tag_invert': True}, + {'amount_currency': 1000.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_tag_invert': False}, + ]) diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py index 31180b08d0b..33661d55657 100644 --- a/addons/account/wizard/account_payment_register.py +++ b/addons/account/wizard/account_payment_register.py @@ -774,7 +774,9 @@ class AccountPaymentRegister(models.TransientModel): :param edit_mode: Is the wizard in edition mode. """ - payments = self.env['account.payment'].create([x['create_vals'] for x in to_process]) + payments = self.env['account.payment']\ + .with_context(skip_invoice_sync=True)\ + .create([x['create_vals'] for x in to_process]) for payment, vals in zip(payments, to_process): vals['payment'] = payment