[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:
committed by
Nicolas Martinelli
parent
5a99ddf88b
commit
47e6afc3e3
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user