[FIX] account: Fix rounding error when saving invoice

- Activate multi-currency
  Company currency: USD
  EUR rate: 0.14
- Set the decimal accuracy of Product Price to 5
- Create the following invoice in EUR:
  Name   Qty    Price Unit     Subtotal
  A     38.0      38.73553      1471.95
  B    222.0    4083.19000    906468.18
  C     35.0      49.45257      1730.84
  D      1.0      17.99000        17.99
- Save the invoice
- Edit the invoice and set a payment term, e.g. 30 days
- Save

There is a difference in debit and credit of 0.01 coming from the
following:
  1471.95   / 0.14 =   10513.93
  906468.18 / 0.14 = 6474772.71
  1730.84   / 0.14 =   12363.14
  17.99     / 0.14 =     128.50
              SUM  = 6497778.28

But the receivable account:
  909688.96 / 0.14 = 6497778.285714286 => 6497778.29

When changing the payment term, all invoice lines are recomputed.
`price_unit` and `tax_ids` are in the list of the updated fields, which
triggers the synchronization of accounting and business fields. During
this synchronization, `_get_fields_onchange_subtotal_model` is called
and recomputes the debit amount, leading to the difference.

We only synchronize if the value of the business fields have changed.

opw-2209543

closes odoo/odoo#49605

X-original-commit: 70bd2c9c7a7c8ec832edd2c158f4f61b78cdeb37
Signed-off-by: Nicolas Martinelli (nim) <nim@odoo.com>
This commit is contained in:
Laurent Smet
2020-04-16 07:11:07 +00:00
committed by Nicolas Martinelli
parent 5a99ddf88b
commit 47e6afc3e3
3 changed files with 65 additions and 7 deletions
+15 -6
View File
@@ -317,6 +317,10 @@ class AccountMove(models.Model):
return True
if field.type == 'monetary' and record[field.currency_field]:
return not record[field.currency_field].is_zero(record[field_name] - vals[field_name])
if field.type == 'float':
record_value = field.convert_to_cache(record[field_name], record)
to_write_value = field.convert_to_cache(vals[field_name], record)
return record_value != to_write_value
return record[field_name] != vals[field_name]
@api.model
@@ -3594,8 +3598,6 @@ class AccountMoveLine(models.Model):
move_initial_values[line.move_id.id] = {}
move_initial_values[line.move_id.id].update({field: line[field]})
result = super(AccountMoveLine, self).write(vals)
# Create the dict for the message post
tracking_values = {} # Tracking values to write in the message post
for move_id, modified_lines in move_initial_values.items():
@@ -3628,7 +3630,14 @@ class AccountMoveLine(models.Model):
msg = self._get_tracking_field_string(tracking_values.get(move.id))
move.message_post(body=msg) # Write for each concerned move the message in the chatter
result = True
for line in self:
cleaned_vals = line.move_id._cleanup_write_orm_values(line, vals)
if not cleaned_vals:
continue
result |= super(AccountMoveLine, line).write(cleaned_vals)
if not line.move_id.is_invoice(include_receipts=True):
continue
@@ -3636,7 +3645,7 @@ class AccountMoveLine(models.Model):
# As we can't express such synchronization as computed fields without cycling, we need to do it both
# in onchange and in create/write. So, if something changed in accounting [resp. business] fields,
# business [resp. accounting] fields are recomputed.
if any(field in vals for field in ACCOUNTING_FIELDS):
if any(field in cleaned_vals for field in ACCOUNTING_FIELDS):
balance = line.currency_id and line.amount_currency or line.debit - line.credit
price_subtotal = line._get_price_total_and_subtotal().get('price_subtotal', 0.0)
to_write = line._get_fields_onchange_balance(
@@ -3648,13 +3657,13 @@ class AccountMoveLine(models.Model):
quantity=to_write.get('quantity', line.quantity),
discount=to_write.get('discount', line.discount),
))
super(AccountMoveLine, line).write(to_write)
elif any(field in vals for field in BUSINESS_FIELDS):
result |= super(AccountMoveLine, line).write(to_write)
elif any(field in cleaned_vals for field in BUSINESS_FIELDS):
to_write = line._get_price_total_and_subtotal()
to_write.update(line._get_fields_onchange_subtotal(
price_subtotal=to_write['price_subtotal'],
))
super(AccountMoveLine, line).write(to_write)
result |= super(AccountMoveLine, line).write(to_write)
# Check total_debit == total_credit in the related moves.
if self._context.get('check_move_validity', True):
+1 -1
View File
@@ -510,7 +510,7 @@ class ResPartner(models.Model):
def _increase_rank(self, field):
if self.ids and field in ['customer_rank', 'supplier_rank']:
try:
with self.env.cr.savepoint():
with self.env.cr.savepoint(flush=False):
query = sql.SQL("""
SELECT {field} FROM res_partner WHERE ID IN %(partner_ids)s FOR UPDATE NOWAIT;
UPDATE res_partner SET {field} = {field} + 1
@@ -2449,3 +2449,52 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
self.assertEqual(len(invoice.invoice_line_ids), 1)
self.assertEqual(len(invoice.line_ids), 2)
def test_out_invoice_recomputation_receivable_lines(self):
''' Test a tricky specific case caused by some framework limitations. Indeed, when
saving a record, some fields are written to the records even if the value is the same
as the previous one. It could lead to an unbalanced journal entry when the recomputed
line is the receivable/payable one.
For example, the computed price_subtotal are the following:
1471.95 / 0.14 = 10513.93
906468.18 / 0.14 = 6474772.71
1730.84 / 0.14 = 12363.14
17.99 / 0.14 = 128.50
SUM = 6497778.28
But when recomputing the receivable line:
909688.96 / 0.14 = 6497778.285714286 => 6497778.29
This recomputation was made because the framework was writing the same 'price_unit'
as the previous value leading to a recomputation of the debit/credit.
'''
self.env['decimal.precision'].search([
('name', '=', self.env['account.move.line']._fields['price_unit']._digits),
]).digits = 5
self.env['res.currency.rate'].create({
'name': '2019-01-01',
'rate': 0.14,
'currency_id': self.currency_data['currency'].id,
'company_id': self.company_data['company'].id,
})
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'partner_id': self.partner_a.id,
'currency_id': self.currency_data['currency'].id,
'invoice_payment_term_id': self.env.ref('account.account_payment_term_immediate').id,
'invoice_line_ids': [
(0, 0, {'name': 'line1', 'price_unit': 38.73553, 'quantity': 38.0}),
(0, 0, {'name': 'line2', 'price_unit': 4083.19000, 'quantity': 222.0}),
(0, 0, {'name': 'line3', 'price_unit': 49.45257, 'quantity': 35.0}),
(0, 0, {'name': 'line4', 'price_unit': 17.99000, 'quantity': 1.0}),
],
})
# assertNotUnbalancedEntryWhenSaving
with Form(invoice) as move_form:
move_form.invoice_payment_term_id = self.env.ref('account.account_payment_term_30days')