From 01cd352a9af9a6b4a93c450b6ea17deea34ec20c Mon Sep 17 00:00:00 2001 From: Camille Spiritus Date: Wed, 1 Mar 2023 14:02:27 +0000 Subject: [PATCH] [FIX] account: Fix payment terms computation + usability - Compute the fixed amounts first before managing the percentage. Suppose a fixed line of 100 plus a percentage line of 100%. For 1000, the previous behavior was 100 + 1000 = 1100 != 1000 - Manage fixed amounts in multi-currency. With multi-currency, apply the rate to compute the company amount. - Ensure the full amount is dispatched exactly with percentages. Suppose 2 percent lines of 50%. To dispatch 0.03, the previous behavior was computing 0.02 + 0.02 = 0.04 != 0.03 leading to a not balanced journal entry. - Don't discard the last new line when clicking outside the sheet. Click on "add new line", then click on the empty space near the form view. => The last added line is removed. closes odoo/odoo#114025 Signed-off-by: Laurent Smet --- addons/account/models/account_payment_term.py | 35 +++++++---- .../payment_term_line_ids.js | 31 +++++++++ addons/account/tests/test_payment_term.py | 63 +++++++++++++++++++ .../views/account_payment_term_views.xml | 4 +- 4 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 addons/account/static/src/components/account_payment_term_form/payment_term_line_ids.js diff --git a/addons/account/models/account_payment_term.py b/addons/account/models/account_payment_term.py index ec631ecd39a..dcdf3a71b51 100644 --- a/addons/account/models/account_payment_term.py +++ b/addons/account/models/account_payment_term.py @@ -138,7 +138,7 @@ class AccountPaymentTerm(models.Model): @api.constrains('line_ids') def _check_lines(self): for terms in self: - if len(terms.line_ids.filtered(lambda r: r.value == 'percent')) > 0 and not terms._get_sum_line_ids(): + if sum(line.value_amount for line in terms.line_ids if line.value == 'percent') != 100: raise ValidationError(_('The Payment Term must have at least one percent line and the sum of the percent must be 100%.')) if len(terms.line_ids) > 1 and terms.early_discount: raise ValidationError( @@ -148,11 +148,6 @@ class AccountPaymentTerm(models.Model): if terms.early_discount and terms.discount_days <= 0: raise ValidationError(_("The Early Payment Discount days must be strictly positive.")) - @api.model - def _get_sum_line_ids(self): - self.ensure_one() - return sum(line.value_amount for line in self.line_ids if line.value == 'percent') == 100 - def _compute_terms(self, date_ref, currency, company, tax_amount, tax_amount_currency, sign, untaxed_amount, untaxed_amount_currency): """Get the distribution of this payment term. :param date_ref: The move date to take into account @@ -180,6 +175,7 @@ class AccountPaymentTerm(models.Model): } if self.early_discount: + # Early discount is only available on single line, 100% payment terms. discount_percentage = self.discount_percentage / 100.0 if self.early_pay_discount_computation in ('excluded', 'mixed'): pay_term['discount_balance'] = company_currency.round(total_amount - untaxed_amount * discount_percentage) @@ -188,21 +184,38 @@ class AccountPaymentTerm(models.Model): pay_term['discount_balance'] = company_currency.round(total_amount * (1 - discount_percentage)) pay_term['discount_amount_currency'] = currency.round(total_amount_currency * (1 - discount_percentage)) + # Remove all fixed amounts. + rate = abs(total_amount_currency / total_amount) if total_amount else 0.0 + for line in self.line_ids: + if line.value == 'fixed': + total_amount_currency -= line.value_amount + total_amount -= currency.round(line.value_amount / rate) if rate else 0.0 + + residual_amount = total_amount + residual_amount_currency = total_amount_currency + #We sort by the last line of a term + term_lines = self.line_ids.sorted(lambda l: l == self.line_ids[-1]) + last_percent_line = term_lines.filtered(lambda l: l.value == 'percent')[-1] for line in self.line_ids.sorted(lambda l: l == self.line_ids[-1]): term_vals = { 'date': line._get_due_date(date_ref), 'company_amount': 0, 'foreign_amount': 0, } - if line.value == 'fixed': - term_vals['company_amount'] = sign * company_currency.round(line.value_amount) + term_vals['company_amount'] = sign * company_currency.round(line.value_amount / rate) if rate else 0.0 term_vals['foreign_amount'] = sign * currency.round(line.value_amount) + elif line == last_percent_line: + term_vals['company_amount'] = residual_amount + term_vals['foreign_amount'] = residual_amount_currency else: - term_vals['company_amount'] = company_currency.round(total_amount * (line.value_amount / 100.0)) - term_vals['foreign_amount'] = currency.round(total_amount_currency * (line.value_amount / 100.0)) - + line_amount = company_currency.round(total_amount * (line.value_amount / 100.0)) + line_amount_currency = currency.round(total_amount_currency * (line.value_amount / 100.0)) + term_vals['company_amount'] = line_amount + term_vals['foreign_amount'] = line_amount_currency + residual_amount -= line_amount + residual_amount_currency -= line_amount_currency pay_term["line_ids"].append(term_vals) return pay_term diff --git a/addons/account/static/src/components/account_payment_term_form/payment_term_line_ids.js b/addons/account/static/src/components/account_payment_term_form/payment_term_line_ids.js new file mode 100644 index 00000000000..59866f07368 --- /dev/null +++ b/addons/account/static/src/components/account_payment_term_form/payment_term_line_ids.js @@ -0,0 +1,31 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; + +import { ListRenderer } from "@web/views/list/list_renderer"; +import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field"; + +export class PaymentTermLineIdsRenderer extends ListRenderer { + + /* override */ + onGlobalClick(ev) { + // Prevent the discard of new records when clicking outside of the sheet. + // This is needed because the user is not forced to edit something on the newly + // created record. Therefore, there is no reason to remove this record when he + // attempt to save the form. + this.props.list.editedRecord = null; + super.onGlobalClick(ev); + } + +} + +export class PaymentTermLineIdsOne2Many extends X2ManyField { + static components = {...X2ManyField.components, ListRenderer: PaymentTermLineIdsRenderer} +} + +export const PaymentTermLineIds = { + ...x2ManyField, + component: PaymentTermLineIdsOne2Many, +} + +registry.category("fields").add("payment_term_line_ids", PaymentTermLineIds); diff --git a/addons/account/tests/test_payment_term.py b/addons/account/tests/test_payment_term.py index e9415b1cb7b..158333e40fe 100644 --- a/addons/account/tests/test_payment_term.py +++ b/addons/account/tests/test_payment_term.py @@ -205,3 +205,66 @@ class TestAccountPaymentTerms(AccountTestInvoicingCommon): }], }, ) + + def test_payment_term_residual_amount_on_last_line_with_fixed_amount_multi_currency(self): + pay_term = self.env['account.payment.term'].create({ + 'name': "test_payment_term_residual_amount_on_last_line", + 'line_ids': [ + Command.create({ + 'value_amount': 50, + 'value': 'percent', + 'nb_days': 0, + }), + Command.create({ + 'value_amount': 50, + 'value': 'percent', + 'nb_days': 0, + }), + Command.create({ + 'value_amount': 0.02, + 'value': 'fixed', + 'nb_days': 0, + }), + ], + }) + + computed_term = pay_term._compute_terms( + fields.Date.from_string('2016-01-01'), self.currency_data['currency'], self.env.company, + 0.0, 0.0, 1.0, 0.04, 0.09, + ) + self.assertEqual( + [ + ( + self.currency_data['currency'].round(l['foreign_amount']), + self.company_data['currency'].round(l['company_amount']), + ) + for l in computed_term['line_ids'] + ], + [(0.035, 0.02), (0.035, 0.01), (0.02, 0.01)], + ) + + def test_payment_term_residual_amount_on_last_line(self): + pay_term = self.env['account.payment.term'].create({ + 'name': "test_payment_term_residual_amount_on_last_line", + 'line_ids': [ + Command.create({ + 'value_amount': 50, + 'value': 'percent', + 'nb_days': 0, + }), + Command.create({ + 'value_amount': 50, + 'value': 'percent', + 'nb_days': 0, + }), + ], + }) + + computed_term = pay_term._compute_terms( + fields.Date.from_string('2016-01-01'), self.env.company.currency_id, self.env.company, + 0.0, 0.0, 1.0, 0.03, 0.03, + ) + self.assertEqual( + [self.env.company.currency_id.round(l['foreign_amount']) for l in computed_term['line_ids']], + [0.02, 0.01], + ) diff --git a/addons/account/views/account_payment_term_views.xml b/addons/account/views/account_payment_term_views.xml index 4c752cc3932..b75c814754d 100644 --- a/addons/account/views/account_payment_term_views.xml +++ b/addons/account/views/account_payment_term_views.xml @@ -62,9 +62,9 @@ - + - +