[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 <las@odoo.com>
This commit is contained in:
Camille Spiritus
2023-03-03 10:15:46 +01:00
committed by Laurent Smet
parent 27fb535d9b
commit 01cd352a9a
4 changed files with 120 additions and 13 deletions
+24 -11
View File
@@ -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
@@ -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);
+63
View File
@@ -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],
)
@@ -62,9 +62,9 @@
</group>
<group>
<group string="Due Terms">
<field name="line_ids" nolabel="1" colspan="2">
<field name="line_ids" widget="payment_term_line_ids" nolabel="1" colspan="2">
<tree string="Payment Terms" editable="bottom" no_open="True">
<field name="value_amount" attrs="{'invisible': [('value', '=', 'balance')]}"/>
<field name="value_amount"/>
<field name="value" nolabel="1"/>
<field name="nb_days" string="After"/>
<field name="delay_type" nolabel="1"/>