[FIX] account,*: remove manual use of check_move_validity

TLDR: the context key `check_move_validity` should never be used
explicitly.

`check_move_validity` is a context key used to disable some important
integrity check in accounting: `debit=credit`, which is one of the most
fundamental rule.
It is possible to disable this in some cases, when creating and updating
lines one by one, since the constraint cannot be satisfied between each
line if the operations are not atomic.

A mechanism has been created for this, with `_check_balanced`. It is a
context manager using `_disable_recursion`, which will taint all
the contexts while inside of the context manager.
Putting any code changing lines one by one inside of that context
manager will then allow having a transient invalid state, but still
always check the validity at the end, when exiting the context.

That context key has been kept only in `point_of_sale` because a special
wizard is used there to be able to validate a session with an unbalanced
move, but a wizard is then in charge of balancing it. The check is then
done at that time.

closes odoo/odoo#146824

Signed-off-by: Brice Bartoletti (bib) <bib@odoo.com>
This commit is contained in:
william-andre
2024-01-18 13:46:49 +00:00
parent 21927695bf
commit b4de10fdc5
8 changed files with 113 additions and 104 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ class AccountChartTemplate(models.AbstractModel):
+ self.ref('demo_move_auto_reconcile_7')
+ self.ref('demo_move_auto_reconcile_8')
+ self.ref('demo_move_auto_reconcile_9')
).with_context(check_move_validity=False)
)
# the invoice_extract acts like a placeholder for the OCR to be ran and doesn't contain
# any lines yet
+66 -48
View File
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _, tools
from contextlib import nullcontext
from odoo import api, fields, models, _, tools, Command
from odoo.osv import expression
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_is_zero
@@ -478,44 +480,52 @@ class AccountAccount(models.Model):
opening_move = self.company_id.account_opening_move_id
if opening_move.state == 'draft':
# check whether we should create a new move line or modify an existing one
account_op_lines = self.env['account.move.line'].search([('account_id', '=', self.id),
('move_id','=', opening_move.id),
(field,'!=', False),
(field,'!=', 0.0)]) # 0.0 condition important for import
with self.env['account.move']._check_balanced({'records': opening_move}):
# check whether we should create a new move line or modify an existing one
account_op_lines = self.env['account.move.line'].search([
('account_id', '=', self.id),
('move_id', '=', opening_move.id),
(field, '!=', False),
(field, '!=', 0.0), # 0.0 condition important for import
])
if account_op_lines:
op_aml_debit = sum(account_op_lines.mapped('debit'))
op_aml_credit = sum(account_op_lines.mapped('credit'))
if account_op_lines:
op_aml_debit = sum(account_op_lines.mapped('debit'))
op_aml_credit = sum(account_op_lines.mapped('credit'))
# There might be more than one line on this account if the opening entry was manually edited
# If so, we need to merge all those lines into one before modifying its balance
opening_move_line = account_op_lines[0]
if len(account_op_lines) > 1:
merge_write_cmd = [(1, opening_move_line.id, {'debit': op_aml_debit, 'credit': op_aml_credit, 'partner_id': None ,'name': _("Opening balance")})]
unlink_write_cmd = [(2, line.id) for line in account_op_lines[1:]]
opening_move.write({'line_ids': merge_write_cmd + unlink_write_cmd})
# There might be more than one line on this account if the opening entry was manually edited
# If so, we need to merge all those lines into one before modifying its balance
opening_move_line = account_op_lines[0]
if len(account_op_lines) > 1:
merge_write_cmd = [Command.update(opening_move_line.id, {
'debit': op_aml_debit,
'credit': op_aml_credit,
'partner_id': False,
'name': _("Opening balance"),
})]
unlink_write_cmd = [Command.unlink(line.id) for line in account_op_lines[1:]]
opening_move.write({'line_ids': merge_write_cmd + unlink_write_cmd})
if amount:
# modify the line
opening_move_line.with_context(check_move_validity=False)[field] = amount
else:
# delete the line (no need to keep a line with value = 0)
opening_move_line.with_context(check_move_validity=False).unlink()
if amount:
# modify the line
opening_move_line[field] = amount
else:
# delete the line (no need to keep a line with value = 0)
opening_move_line.unlink()
elif amount:
# create a new line, as none existed before
self.env['account.move.line'].with_context(check_move_validity=False).create({
'name': _('Opening balance'),
field: amount,
'move_id': opening_move.id,
'account_id': self.id,
})
elif amount:
# create a new line, as none existed before
self.env['account.move.line'].create({
'name': _('Opening balance'),
field: amount,
'move_id': opening_move.id,
'account_id': self.id,
})
# Then, we automatically balance the opening move, to make sure it stays valid
if not 'import_file' in self.env.context:
# When importing a file, avoid recomputing the opening move for each account and do it at the end, for better performances
self.company_id._auto_balance_opening_move()
# Then, we automatically balance the opening move, to make sure it stays valid
if not 'import_file' in self.env.context:
# When importing a file, avoid recomputing the opening move for each account and do it at the end, for better performances
self.company_id._auto_balance_opening_move()
@api.model
def default_get(self, default_fields):
@@ -656,20 +666,28 @@ class AccountAccount(models.Model):
with opening debit/credit. In that case, the auto-balance is postpone
until the whole file has been imported.
"""
rslt = super(AccountAccount, self).load(fields, data)
if 'import_file' in self.env.context and 'opening_balance' in fields:
companies = self.search([('id', 'in', rslt['ids'])]).mapped('company_id')
for company in companies:
if company.account_opening_move_id.filtered(lambda m: m.state == "posted"):
raise UserError(
_('You cannot import the "openning_balance" if the opening move (%s) is already posted. \
If you are absolutely sure you want to modify the opening balance of your accounts, reset the move to draft.',
company.account_opening_move_id.name))
company._auto_balance_opening_move()
# the current_balance of the account only includes posted moves and
# would always amount to 0 after the import if we didn't post the opening move
company.account_opening_move_id.action_post()
importing = 'import_file' in self.env.context and 'opening_balance' in fields
if importing:
container = {'records': self.env['account.move']}
manager = self.env['account.move']._check_balanced(container)
else:
manager = nullcontext
with manager:
rslt = super(AccountAccount, self).load(fields, data)
if importing:
companies = self.search([('id', 'in', rslt['ids'])]).mapped('company_id')
container['records'] = companies.account_opening_move_id
for company in companies:
if company.account_opening_move_id.filtered(lambda m: m.state == "posted"):
raise UserError(_(
'You cannot import the "openning_balance" if the opening move (%s) is already posted. '
'If you are absolutely sure you want to modify the opening balance of your accounts, reset the move to draft.',
company.account_opening_move_id.name,
))
company._auto_balance_opening_move()
# the current_balance of the account only includes posted moves and
# would always amount to 0 after the import if we didn't post the opening move
companies.account_opening_move_id.action_post()
return rslt
def _toggle_reconcile_to_true(self):
@@ -2514,7 +2514,6 @@ class AccountMoveLine(models.Model):
skip_invoice_sync=True,
skip_invoice_line_sync=True,
skip_account_move_synchronization=True,
check_move_validity=False,
)\
.create(full_reconcile_values_list)
+1 -1
View File
@@ -494,7 +494,7 @@ class ResCompany(models.Model):
balancing_move_line = self.account_opening_move_id.line_ids.filtered(lambda x: x.account_id == balancing_account)
# There could be multiple lines if we imported the balance from unaffected earnings account too
if len(balancing_move_line) > 1:
self.with_context(check_move_validity=False).account_opening_move_id.line_ids -= balancing_move_line[1:]
self.account_opening_move_id.line_ids -= balancing_move_line[1:]
balancing_move_line = balancing_move_line[0]
debit_diff, credit_diff = self.get_opening_move_differences(self.account_opening_move_id.line_ids)
@@ -668,7 +668,6 @@ class HrExpenseSheet(models.Model):
'skip_invoice_sync': True,
'skip_invoice_line_sync': True,
'skip_account_move_synchronization': True,
'check_move_validity': False,
}
own_account_sheets = self.filtered(lambda sheet: sheet.payment_mode == 'own_account')
company_account_sheets = self - own_account_sheets
@@ -17,7 +17,7 @@ class AccountPayment(models.Model):
return
for pay in self.with_context(
skip_account_move_synchronization=True, check_move_validity=False, skip_invoice_sync=True, dynamic_unlink=True):
skip_account_move_synchronization=True, skip_invoice_sync=True, dynamic_unlink=True):
pay.line_ids.filtered(lambda x: x.account_id == pay.company_id.l10n_ar_tax_base_account_id or x.tax_line_id.l10n_ar_withholding_payment_type).unlink()
res = super()._synchronize_to_moves(changed_fields)
return res
+43 -50
View File
@@ -567,61 +567,54 @@ class PosOrder(models.Model):
new_move.message_post(body=message)
if self.config_id.cash_rounding:
rounding_applied = float_round(self.amount_paid - self.amount_total,
precision_rounding=new_move.currency_id.rounding)
rounding_line = new_move.line_ids.filtered(lambda line: line.display_type == 'rounding')
if rounding_line and rounding_line.debit > 0:
rounding_line_difference = rounding_line.debit + rounding_applied
elif rounding_line and rounding_line.credit > 0:
rounding_line_difference = -rounding_line.credit + rounding_applied
else:
rounding_line_difference = rounding_applied
if rounding_applied:
if rounding_applied > 0.0:
account_id = new_move.invoice_cash_rounding_id.loss_account_id.id
with self.env['account.move']._check_balanced({'records': new_move}):
rounding_applied = float_round(self.amount_paid - self.amount_total,
precision_rounding=new_move.currency_id.rounding)
rounding_line = new_move.line_ids.filtered(lambda line: line.display_type == 'rounding')
if rounding_line and rounding_line.debit > 0:
rounding_line_difference = rounding_line.debit + rounding_applied
elif rounding_line and rounding_line.credit > 0:
rounding_line_difference = -rounding_line.credit + rounding_applied
else:
account_id = new_move.invoice_cash_rounding_id.profit_account_id.id
if rounding_line:
if rounding_line_difference:
rounding_line.with_context(skip_invoice_sync=True, check_move_validity=False).write({
'debit': rounding_applied < 0.0 and -rounding_applied or 0.0,
'credit': rounding_applied > 0.0 and rounding_applied or 0.0,
'account_id': account_id,
'price_unit': rounding_applied,
})
rounding_line_difference = rounding_applied
if rounding_applied:
if rounding_applied > 0.0:
account_id = new_move.invoice_cash_rounding_id.loss_account_id.id
else:
account_id = new_move.invoice_cash_rounding_id.profit_account_id.id
if rounding_line:
if rounding_line_difference:
rounding_line.with_context(skip_invoice_sync=True).write({
'debit': rounding_applied < 0.0 and -rounding_applied or 0.0,
'credit': rounding_applied > 0.0 and rounding_applied or 0.0,
'account_id': account_id,
'price_unit': rounding_applied,
})
else:
self.env['account.move.line'].with_context(skip_invoice_sync=True).create({
'balance': -rounding_applied,
'quantity': 1.0,
'partner_id': new_move.partner_id.id,
'move_id': new_move.id,
'currency_id': new_move.currency_id.id,
'company_id': new_move.company_id.id,
'company_currency_id': new_move.company_id.currency_id.id,
'display_type': 'rounding',
'sequence': 9999,
'name': self.config_id.rounding_method.name,
'account_id': account_id,
})
else:
self.env['account.move.line'].with_context(skip_invoice_sync=True, check_move_validity=False).create({
'balance': -rounding_applied,
'quantity': 1.0,
'partner_id': new_move.partner_id.id,
'move_id': new_move.id,
'currency_id': new_move.currency_id.id,
'company_id': new_move.company_id.id,
'company_currency_id': new_move.company_id.currency_id.id,
'display_type': 'rounding',
'sequence': 9999,
'name': self.config_id.rounding_method.name,
'account_id': account_id,
})
else:
if rounding_line:
rounding_line.with_context(skip_invoice_sync=True, check_move_validity=False).unlink()
if rounding_line_difference:
existing_terms_line = new_move.line_ids.filtered(
lambda line: line.account_id.account_type in ('asset_receivable', 'liability_payable'))
if existing_terms_line.debit > 0:
if rounding_line:
rounding_line.with_context(skip_invoice_sync=True).unlink()
if rounding_line_difference:
existing_terms_line = new_move.line_ids.filtered(
lambda line: line.account_id.account_type in ('asset_receivable', 'liability_payable'))
existing_terms_line_new_val = float_round(
existing_terms_line.debit + rounding_line_difference,
existing_terms_line.balance + rounding_line_difference,
precision_rounding=new_move.currency_id.rounding)
else:
existing_terms_line_new_val = float_round(
-existing_terms_line.credit + rounding_line_difference,
precision_rounding=new_move.currency_id.rounding)
existing_terms_line.with_context(skip_invoice_sync=True).write({
'debit': existing_terms_line_new_val > 0.0 and existing_terms_line_new_val or 0.0,
'credit': existing_terms_line_new_val < 0.0 and -existing_terms_line_new_val or 0.0,
})
existing_terms_line.with_context(skip_invoice_sync=True).balance = existing_terms_line_new_val
return new_move
def action_pos_order_paid(self):
+1 -1
View File
@@ -91,6 +91,6 @@ class PosPayment(models.Model):
'account_id': pos_session.company_id.account_default_pos_receivable_account_id.id,
'move_id': payment_move.id,
}, amounts['amount'], amounts['amount_converted'])
self.env['account.move.line'].with_context(check_move_validity=False).create([credit_line_vals, debit_line_vals])
self.env['account.move.line'].create([credit_line_vals, debit_line_vals])
payment_move._post()
return result