From 47e6afc3e33d9d1be765541bf570410804bb4a4a Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Tue, 31 Mar 2020 12:24:04 +0000 Subject: [PATCH] [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) --- addons/account/models/account_move.py | 21 +++++--- addons/account/models/partner.py | 2 +- .../tests/test_account_move_out_invoice.py | 49 +++++++++++++++++++ 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 9531a72baee..663ad11a4f9 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -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): diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 53b3eaa9bf4..115514677ca 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -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 diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index d09adc1e7ff..dd38027ef68 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -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')