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 @@
-
-
+