From d5bfc839df77ed6474969282c1cf734ce071a9d2 Mon Sep 17 00:00:00 2001 From: "John Laterre (jol)" Date: Wed, 28 Sep 2022 14:46:34 +0000 Subject: [PATCH] [FIX] account: automatically create balancing lines on entries Currently, in a Journal Entry, applying a tax on any journal item doesn't compute the corresponding tax line. We end up in a situation where the move appears balanced but doesn't take into account the taxes applied. Moreover, the move cannot be posted. The solution involves several steps: - Changing a tax on a line now triggers a save. - Changing the debit or credit on a line with taxes will also trigger a save. - The company's suspense account is used for the balancing line. During the save, a check is added to see if the move is unbalanced. In that case, a balancing line will automatically be created to ensure the move can be saved. X-original-commit: a3e9ca9586cba9d6161d4c05a7bfff9beeeddea7 Part-of: odoo/odoo#102487 --- addons/account/models/account_move.py | 65 +++++++++++++++---- addons/account/models/account_move_line.py | 2 +- .../autosave_many2many_tags.js | 42 ++++++++++++ .../account/tests/test_account_move_entry.py | 18 ----- .../account/views/account_journal_views.xml | 5 ++ addons/account/views/account_move_views.xml | 4 +- 6 files changed, 101 insertions(+), 35 deletions(-) create mode 100644 addons/account/static/src/components/autosave_many2many_tags/autosave_many2many_tags.js diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 2f9bba28fe7..f0ff6abc9e6 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1668,19 +1668,33 @@ class AccountMove(models.Model): if disabled: return + unbalanced_moves = self._get_unbalanced_moves(container) + if unbalanced_moves: + error_msg = _("There was a problem with the following move(s):\n") + for id_, balance, _debit, _credit in unbalanced_moves: + error_msg += _("- Move with id %i\n", id_) + if balance != 0.0: + error_msg += _("\tCannot create unbalanced journal entry. The balance is equal to %s\n", + format_amount(self.env, balance, self.env['account.move'].browse(id_).currency_id)) + raise UserError(error_msg) + + def _get_unbalanced_moves(self, container): + moves = container['records'].filtered(lambda move: move.line_ids) if not moves: return # /!\ As this method is called in create / write, we can't make the assumption the computed stored fields - # are already done. Then, this query MUST NOT depend of computed stored fields (e.g. balance). - # It happens as the ORM makes the create with the 'no_recompute' statement. - self.env['account.move.line'].flush_model(['balance', 'currency_id', 'move_id']) + # are already done. Then, this query MUST NOT depend on computed stored fields (e.g. balance). + # It happens as the ORM calls create() with the 'no_recompute' statement. + self.env['account.move.line'].flush_model(['debit', 'credit', 'balance', 'currency_id', 'move_id']) self.env['account.move'].flush_model(['journal_id']) self._cr.execute(''' WITH error_moves AS ( SELECT line.move_id, - ROUND(SUM(line.balance), currency.decimal_places) balance + ROUND(SUM(line.balance), currency.decimal_places) balance, + ROUND(SUM(line.debit), currency.decimal_places) debit, + ROUND(SUM(line.credit), currency.decimal_places) credit FROM account_move_line line JOIN account_move move ON move.id = line.move_id JOIN account_journal journal ON journal.id = move.journal_id @@ -1694,16 +1708,7 @@ class AccountMove(models.Model): WHERE balance !=0 ''', [tuple(moves.ids)]) - query_res = self._cr.fetchall() - if query_res: - error_msg = _("There was a problem with the following move(s):\n") - for move in query_res: - id_, balance = move - error_msg += _("- Move with id %i\n", id_) - if balance != 0.0: - error_msg += _("\tCannot create unbalanced journal entry. The balance is equal to %s\n", - format_amount(self.env, balance, self.env['account.move'].browse(id_).currency_id)) - raise UserError(error_msg) + return self._cr.fetchall() def _check_fiscalyear_lock_date(self): for move in self: @@ -1903,6 +1908,34 @@ class AccountMove(models.Model): _apply_cash_rounding(self, diff_balance, diff_amount_currency, existing_cash_rounding_line) + @contextmanager + def _sync_unbalanced_lines(self, container): + yield + + # Unlink tax lines if all tax tags have been removed. + if not container['records'].line_ids.tax_ids: + container['records'].line_ids.filtered('tax_line_id').unlink() + + # Unlink the automatic balancing line, if any, to prevent having multiple ones. + balance_name = _('Automatic Balancing Line') + container['records'].line_ids.filtered(lambda line: line.name == balance_name).unlink() + + # Create an automatic balancing line to make sure the entry can be saved. + vals_list = [] + unbalanced_moves = self._get_unbalanced_moves(container) + if unbalanced_moves: + for move_id, _balance, debit, credit in unbalanced_moves: + move = self.browse(move_id) + balance = debit - credit + vals_list.append({ + 'name': balance_name, + 'move_id': move.id, + 'account_id': move.company_id.account_journal_suspense_account_id.id, + 'debit': -balance if balance < 0.0 else 0.0, + 'credit': balance if balance > 0.0 else 0.0, + }) + container['records'].env['account.move.line'].create(vals_list) + @contextmanager def _sync_rounding_lines(self, container): yield @@ -2050,8 +2083,10 @@ class AccountMove(models.Model): # Only invoice-like and journal entries in "auto tax mode" are synced tax_filter = lambda m: (m.is_invoice(True) or m.line_ids.tax_ids and not m.tax_cash_basis_origin_move_id) invoice_filter = lambda m: (m.is_invoice(True)) + misc_filter = lambda m: (m.move_type == 'entry' and not m.tax_cash_basis_origin_move_id) tax_container = {'records': container['records'].filtered(tax_filter)} invoice_container = {'records': container['records'].filtered(invoice_filter)} + misc_container = {'records': container['records'].filtered(misc_filter)} with ExitStack() as stack: stack.enter_context(self._sync_dynamic_line( @@ -2061,6 +2096,7 @@ class AccountMove(models.Model): line_type='payment_term', container=invoice_container, )) + stack.enter_context(self._sync_unbalanced_lines(misc_container)) stack.enter_context(self._sync_rounding_lines(invoice_container)) stack.enter_context(self._sync_dynamic_line( existing_key_fname='tax_key', @@ -2083,6 +2119,7 @@ class AccountMove(models.Model): line_container['records'] = self.line_ids tax_container['records'] = container['records'].filtered(tax_filter) invoice_container['records'] = container['records'].filtered(invoice_filter) + misc_container['records'] = container['records'].filtered(misc_filter) # Delete the tax lines if the journal entry is not in "auto tax mode" anymore for move in container['records']: diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index 4510ca95cc8..36b4e632418 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -1492,7 +1492,7 @@ class AccountMoveLine(models.Model): def _prevent_automatic_line_deletion(self): if not self.env.context.get('dynamic_unlink'): for line in self: - if line.display_type == 'tax': + if line.display_type == 'tax' and line.move_id.line_ids.tax_ids: raise ValidationError(_( "You cannot delete a tax line as it would impact the tax report" )) diff --git a/addons/account/static/src/components/autosave_many2many_tags/autosave_many2many_tags.js b/addons/account/static/src/components/autosave_many2many_tags/autosave_many2many_tags.js new file mode 100644 index 00000000000..dd4bf0aba14 --- /dev/null +++ b/addons/account/static/src/components/autosave_many2many_tags/autosave_many2many_tags.js @@ -0,0 +1,42 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field"; + +const { onWillUpdateProps } = owl; + +export class AutosaveMany2ManyTagsField extends Many2ManyTagsField { + setup() { + super.setup(); + + onWillUpdateProps((nextProps) => this.willUpdateProps(nextProps)); + + this.lastBalance = this.props.record.data.balance; + + const super_update = this.update; + this.update = (recordlist) => { + super_update(recordlist); + this._saveOnUpdate(); + }; + } + + deleteTag(id) { + super.deleteTag(id); + this._saveOnUpdate(); + } + + willUpdateProps(nextProps) { + const currentBalance = this.props.record.data.balance; + const hasTax = this.props.record.data.tax_ids.records.length > 0; + if (hasTax && currentBalance !== this.lastBalance) { + this.lastBalance = currentBalance; + this._saveOnUpdate(); + } + } + + async _saveOnUpdate() { + await this.props.record.model.root.save({ stayInEdition: true }); + } +} + +registry.category("fields").add("autosave_many2many_tags", AutosaveMany2ManyTagsField); diff --git a/addons/account/tests/test_account_move_entry.py b/addons/account/tests/test_account_move_entry.py index f0ece9ccfa4..2ffacf12fb6 100644 --- a/addons/account/tests/test_account_move_entry.py +++ b/addons/account/tests/test_account_move_entry.py @@ -393,24 +393,6 @@ class TestAccountMove(AccountTestInvoicingCommon): with self.assertRaises(UserError), self.cr.savepoint(): draft_moves.unlink() - def test_misc_always_balanced_move(self): - ''' Ensure there is no way to make ''' - # You can't remove a journal item making the journal entry unbalanced. - with self.assertRaises(UserError), self.cr.savepoint(): - self.test_move.line_ids[0].unlink() - - # Same check using write instead of unlink. - with self.assertRaises(UserError), self.cr.savepoint(): - balance = self.test_move.line_ids[0].balance + 5 - self.test_move.line_ids[0].write({ - 'debit': balance if balance > 0.0 else 0.0, - 'credit': -balance if balance < 0.0 else 0.0, - }) - - # You can remove journal items if the related journal entry is still balanced. - self.test_move.line_ids.filtered(lambda l: not l.tax_repartition_line_id).balance = 0 - self.test_move.line_ids[0].unlink() - def test_add_followers_on_post(self): # Add some existing partners, some from another company company = self.env['res.company'].create({'name': 'Oopo'}) diff --git a/addons/account/views/account_journal_views.xml b/addons/account/views/account_journal_views.xml index 1d7aa9cda86..d9c668b1409 100644 --- a/addons/account/views/account_journal_views.xml +++ b/addons/account/views/account_journal_views.xml @@ -83,6 +83,11 @@ attrs="{'required': [('type', '=', 'purchase')], 'invisible': [('type', '!=', 'purchase')]}" options="{'no_quick_create': True}" groups="account.group_account_readonly"/> + + diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index e29830ded27..922452c0e62 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -1071,7 +1071,7 @@ - - +