[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
This commit is contained in:
@@ -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']:
|
||||
|
||||
@@ -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"
|
||||
))
|
||||
|
||||
+42
@@ -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);
|
||||
@@ -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'})
|
||||
|
||||
@@ -83,6 +83,11 @@
|
||||
attrs="{'required': [('type', '=', 'purchase')], 'invisible': [('type', '!=', 'purchase')]}"
|
||||
options="{'no_quick_create': True}"
|
||||
groups="account.group_account_readonly"/>
|
||||
<!-- Misc -->
|
||||
<field name="default_account_id" help="If set, this account is used to automatically balance entries."
|
||||
attrs="{'invisible': [('type', '!=', 'general')]}"
|
||||
options="{'no_quick_create': True}"
|
||||
groups="account.group_account_readonly"/>
|
||||
<field name="refund_sequence" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}"/>
|
||||
<field name="payment_sequence" attrs="{'invisible': [('type', 'not in', ('bank', 'cash'))]}"/>
|
||||
<field name="code" placeholder="e.g. INV"/>
|
||||
|
||||
@@ -1071,7 +1071,7 @@
|
||||
<field name="currency_id" options="{'no_create': True}"
|
||||
optional="hide" groups="base.group_multi_currency"
|
||||
attrs="{'column_invisible': [('parent.move_type', '!=', 'entry')]}"/>
|
||||
<field name="tax_ids" widget="many2many_tags"
|
||||
<field name="tax_ids" widget="autosave_many2many_tags"
|
||||
optional="hide"
|
||||
domain="[('type_tax_use', '=?', parent.invoice_filter_type_domain)]"
|
||||
context="{'append_type_to_tax_name': not parent.invoice_filter_type_domain}"
|
||||
@@ -1148,7 +1148,7 @@
|
||||
<field name="debit" sum="Total Debit"/>
|
||||
<field name="credit" sum="Total Credit"/>
|
||||
<field name="balance" invisible="1"/>
|
||||
<field name="tax_ids" string="Taxes Applied" widget="many2many_tags" options="{'no_create': True}"/>
|
||||
<field name="tax_ids" string="Taxes Applied" widget="autosave_many2many_tags" options="{'no_create': True}"/>
|
||||
<field name="date_maturity" required="0" invisible="context.get('view_no_maturity', False)"/>
|
||||
</group>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user