[FIX] account: Fix opening balance account
Allow the user to set back the debit or credit to 0. It's a bug introduced by https://github.com/odoo/odoo/commit/c3793357d823273cb4297b0eb31308ad73a9dff8 Allow to create an opening balance for an account having a foreign currency. closes odoo/odoo#157307 Opw: 3665557 X-original-commit: e30bbdda5cd5dd8f0f5ff2cf8a1f564b6174a0d5 Related: odoo/enterprise#58462 Signed-off-by: William André (wan) <wan@odoo.com>
This commit is contained in:
committed by
william-andre
parent
f8be70f670
commit
65ce66d4ee
@@ -4,7 +4,6 @@ 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
|
||||
from odoo.tools.sql import SQL
|
||||
from bisect import bisect_left
|
||||
from collections import defaultdict
|
||||
@@ -41,6 +40,7 @@ class AccountAccount(models.Model):
|
||||
name = fields.Char(string="Account Name", required=True, index='trigram', tracking=True, translate=True)
|
||||
currency_id = fields.Many2one('res.currency', string='Account Currency', tracking=True,
|
||||
help="Forces all journal items in this account to have a specific currency (i.e. bank journals). If no currency is set, entries can use any currency.")
|
||||
company_currency_id = fields.Many2one(related='company_id.currency_id')
|
||||
code = fields.Char(size=64, required=True, tracking=True, index=True, unaccent=False)
|
||||
deprecated = fields.Boolean(default=False, tracking=True)
|
||||
used = fields.Boolean(compute='_compute_used', search='_search_used')
|
||||
@@ -108,9 +108,9 @@ class AccountAccount(models.Model):
|
||||
help="Define in which journals this account can be used. If empty, can be used in all journals.",
|
||||
check_company=True,
|
||||
)
|
||||
opening_debit = fields.Monetary(string="Opening Debit", compute='_compute_opening_debit_credit', inverse='_set_opening_debit')
|
||||
opening_credit = fields.Monetary(string="Opening Credit", compute='_compute_opening_debit_credit', inverse='_set_opening_credit')
|
||||
opening_balance = fields.Monetary(string="Opening Balance", compute='_compute_opening_debit_credit', inverse='_set_opening_balance')
|
||||
opening_debit = fields.Monetary(string="Opening Debit", compute='_compute_opening_debit_credit', inverse='_set_opening_debit', currency_field='company_currency_id')
|
||||
opening_credit = fields.Monetary(string="Opening Credit", compute='_compute_opening_debit_credit', inverse='_set_opening_credit', currency_field='company_currency_id')
|
||||
opening_balance = fields.Monetary(string="Opening Balance", compute='_compute_opening_debit_credit', inverse='_set_opening_balance', currency_field='company_currency_id')
|
||||
|
||||
current_balance = fields.Float(compute='_compute_current_balance')
|
||||
related_taxes_amount = fields.Integer(compute='_compute_related_taxes_amount')
|
||||
@@ -460,10 +460,11 @@ class AccountAccount(models.Model):
|
||||
record._set_opening_debit_credit(record.opening_credit, 'credit')
|
||||
|
||||
def _set_opening_balance(self):
|
||||
# Tracking of the balances to be used after the import to populate the opening move in batch.
|
||||
for account in self:
|
||||
if account.opening_balance:
|
||||
side = 'debit' if account.opening_balance > 0 else 'credit'
|
||||
account._set_opening_debit_credit(abs(account.opening_balance), side)
|
||||
balance = account.opening_balance
|
||||
account._set_opening_debit_credit(abs(balance) if balance > 0.0 else 0.0, 'debit')
|
||||
account._set_opening_debit_credit(abs(balance) if balance < 0.0 else 0.0, 'credit')
|
||||
|
||||
def _set_opening_debit_credit(self, amount, field):
|
||||
""" Generic function called by both opening_debit and opening_credit's
|
||||
@@ -471,61 +472,15 @@ class AccountAccount(models.Model):
|
||||
either 'debit' or 'credit', depending on which one of these two fields
|
||||
got assigned.
|
||||
"""
|
||||
# only set the opening debit/credit if the amount is not zero,
|
||||
# otherwise return early
|
||||
if float_is_zero(amount, precision_digits=2):
|
||||
return
|
||||
|
||||
self.company_id.create_op_move_if_non_existant()
|
||||
opening_move = self.company_id.account_opening_move_id
|
||||
|
||||
if opening_move.state == 'draft':
|
||||
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'))
|
||||
|
||||
# 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[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'].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()
|
||||
self.ensure_one()
|
||||
if 'import_account_opening_balance' not in self._cr.precommit.data:
|
||||
data = self._cr.precommit.data['import_account_opening_balance'] = {}
|
||||
self._cr.precommit.add(self._load_precommit_update_opening_move)
|
||||
else:
|
||||
data = self._cr.precommit.data['import_account_opening_balance']
|
||||
data.setdefault(self.id, [None, None])
|
||||
index = 0 if field == 'debit' else 1
|
||||
data[self.id][index] = amount
|
||||
|
||||
@api.model
|
||||
def default_get(self, default_fields):
|
||||
@@ -661,34 +616,23 @@ class AccountAccount(models.Model):
|
||||
}], dirty=True)
|
||||
|
||||
@api.model
|
||||
def load(self, fields, data):
|
||||
""" Overridden for better performances when importing a list of account
|
||||
with opening debit/credit. In that case, the auto-balance is postpone
|
||||
until the whole file has been imported.
|
||||
def _load_precommit_update_opening_move(self):
|
||||
""" precommit callback to recompute the opening move according the opening balances that changed.
|
||||
This is particularly useful when importing a csv containing the 'opening_balance' column.
|
||||
In that case, we don't want to use the inverse method set on field since it will be
|
||||
called for each account separately. That would be quite costly in terms of performances.
|
||||
Instead, the opening balances are collected and this method is called once at the end
|
||||
to update the opening move accordingly.
|
||||
"""
|
||||
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
|
||||
data = self._cr.precommit.data.pop('import_account_opening_balance', {})
|
||||
accounts = self.browse(data.keys())
|
||||
|
||||
accounts_per_company = defaultdict(lambda: self.env['account.account'])
|
||||
for account in accounts:
|
||||
accounts_per_company[account.company_id] |= account
|
||||
|
||||
for company, company_accounts in accounts_per_company.items():
|
||||
company._update_opening_move({account: data[account.id] for account in company_accounts})
|
||||
|
||||
def _toggle_reconcile_to_true(self):
|
||||
'''Toggle the `reconcile´ boolean from False -> True
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta, datetime, date
|
||||
import calendar
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo import fields, models, api, _, Command
|
||||
from odoo.exceptions import ValidationError, UserError, RedirectWarning
|
||||
from odoo.tools.mail import is_html_empty
|
||||
from odoo.tools.misc import format_date
|
||||
@@ -412,29 +413,39 @@ class ResCompany(models.Model):
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_default_opening_move_values(self):
|
||||
""" Get the default values to create the opening move.
|
||||
|
||||
:return: A dictionary to be passed to account.move.create.
|
||||
"""
|
||||
self.ensure_one()
|
||||
default_journal = self.env['account.journal'].search(
|
||||
domain=[
|
||||
*self.env['account.journal']._check_company_domain(self),
|
||||
('type', '=', 'general'),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if not default_journal:
|
||||
raise UserError(_("Please install a chart of accounts or create a miscellaneous journal before proceeding."))
|
||||
|
||||
return {
|
||||
'ref': _('Opening Journal Entry'),
|
||||
'company_id': self.id,
|
||||
'journal_id': default_journal.id,
|
||||
'date': self.account_opening_date - timedelta(days=1),
|
||||
}
|
||||
|
||||
def create_op_move_if_non_existant(self):
|
||||
""" Creates an empty opening move in 'draft' state for the current company
|
||||
if there wasn't already one defined. For this, the function needs at least
|
||||
one journal of type 'general' to exist (required by account.move).
|
||||
"""
|
||||
# TO BE REMOVED IN MASTER
|
||||
self.ensure_one()
|
||||
if not self.account_opening_move_id:
|
||||
default_journal = self.env['account.journal'].search([
|
||||
*self.env['account.journal']._check_company_domain(self),
|
||||
('type', '=', 'general')
|
||||
], limit=1)
|
||||
|
||||
if not default_journal:
|
||||
raise UserError(_("Please install a chart of accounts or create a miscellaneous journal before proceeding."))
|
||||
|
||||
opening_date = self.account_opening_date - timedelta(days=1)
|
||||
|
||||
self.account_opening_move_id = self.env['account.move'].create({
|
||||
'ref': _('Opening Journal Entry'),
|
||||
'company_id': self.id,
|
||||
'journal_id': default_journal.id,
|
||||
'date': opening_date,
|
||||
})
|
||||
self.account_opening_move_id = self.env['account.move'].create(self._get_default_opening_move_values())
|
||||
|
||||
def opening_move_posted(self):
|
||||
""" Returns true if this company has an opening account move and this move is posted."""
|
||||
@@ -467,6 +478,7 @@ class ResCompany(models.Model):
|
||||
})
|
||||
|
||||
def get_opening_move_differences(self, opening_move_lines):
|
||||
# TO BE REMOVED IN MASTER
|
||||
currency = self.currency_id
|
||||
balancing_move_line = opening_move_lines.filtered(lambda x: x.account_id == self.get_unaffected_earnings_account())
|
||||
|
||||
@@ -487,6 +499,7 @@ class ResCompany(models.Model):
|
||||
and is unbalanced, balances it with a automatic account.move.line in the
|
||||
current year earnings account.
|
||||
"""
|
||||
# TO BE REMOVED IN MASTER
|
||||
if self.account_opening_move_id and self.account_opening_move_id.state == 'draft':
|
||||
balancing_account = self.get_unaffected_earnings_account()
|
||||
currency = self.currency_id
|
||||
@@ -517,6 +530,93 @@ class ResCompany(models.Model):
|
||||
'credit': debit_diff,
|
||||
})
|
||||
|
||||
def _update_opening_move(self, to_update):
|
||||
""" Create or update the opening move for the accounts passed as parameter.
|
||||
|
||||
:param to_update: A dictionary mapping each account with a tuple (debit, credit).
|
||||
A separated opening line is created for both fields. A None value on debit/credit means the corresponding
|
||||
line will not be updated.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Don't allow to modify the opening move if not in draft.
|
||||
opening_move = self.account_opening_move_id
|
||||
if opening_move and opening_move.state != 'draft':
|
||||
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.',
|
||||
self.account_opening_move_id.name,
|
||||
))
|
||||
|
||||
def del_lines(lines):
|
||||
nonlocal open_balance
|
||||
for line in lines:
|
||||
open_balance -= line.balance
|
||||
yield Command.delete(line.id)
|
||||
|
||||
def update_vals(account, side, balance, balancing=False):
|
||||
nonlocal open_balance
|
||||
corresponding_lines = corresponding_lines_per_account[(account, side)]
|
||||
currency = account.currency_id or self.currency_id
|
||||
amount_currency = balance if balancing else self.currency_id._convert(balance, currency, date=conversion_date)
|
||||
open_balance += balance
|
||||
if self.currency_id.is_zero(balance):
|
||||
yield from del_lines(corresponding_lines)
|
||||
elif corresponding_lines:
|
||||
line_to_update = corresponding_lines[0]
|
||||
open_balance -= line_to_update.balance
|
||||
yield Command.update(line_to_update.id, {
|
||||
'balance': balance,
|
||||
'amount_currency': amount_currency,
|
||||
})
|
||||
yield from del_lines(corresponding_lines[1:])
|
||||
else:
|
||||
yield Command.create({
|
||||
'name':_("Automatic Balancing Line") if balancing else _("Opening balance"),
|
||||
'account_id': account.id,
|
||||
'balance': balance,
|
||||
'amount_currency': amount_currency,
|
||||
'currency_id': currency.id,
|
||||
})
|
||||
|
||||
# Decode the existing opening move.
|
||||
corresponding_lines_per_account = defaultdict(lambda: self.env['account.move.line'])
|
||||
corresponding_lines_per_account.update(opening_move.line_ids.grouped(lambda line: (
|
||||
line.account_id,
|
||||
'debit' if line.balance > 0.0 or line.amount_currency > 0.0 else 'credit',
|
||||
)))
|
||||
|
||||
# Update the opening move's lines.
|
||||
balancing_account = self.get_unaffected_earnings_account()
|
||||
open_balance = (
|
||||
sum(corresponding_lines_per_account[(balancing_account, 'credit')].mapped('credit'))
|
||||
-sum(corresponding_lines_per_account[(balancing_account, 'debit')].mapped('debit'))
|
||||
)
|
||||
commands = []
|
||||
move_values = {'line_ids': commands}
|
||||
if opening_move:
|
||||
conversion_date = opening_move.date
|
||||
else:
|
||||
move_values.update(self._get_default_opening_move_values())
|
||||
conversion_date = move_values['date']
|
||||
for account, (debit, credit) in to_update.items():
|
||||
if debit is not None:
|
||||
commands.extend(update_vals(account, 'debit', debit))
|
||||
if credit is not None:
|
||||
commands.extend(update_vals(account, 'credit', -credit))
|
||||
|
||||
commands.extend(update_vals(balancing_account, 'debit', max(-open_balance, 0), balancing=True))
|
||||
commands.extend(update_vals(balancing_account, 'credit', -max(open_balance, 0), balancing=True))
|
||||
|
||||
# Nothing to do.
|
||||
if not commands:
|
||||
return
|
||||
|
||||
if opening_move:
|
||||
opening_move.write(move_values)
|
||||
else:
|
||||
self.account_opening_move_id = self.env['account.move'].create(move_values)
|
||||
|
||||
def action_save_onboarding_sale_tax(self):
|
||||
""" Set the onboarding step as done """
|
||||
self.env['onboarding.onboarding.step'].action_validate_step('account.onboarding_onboarding_step_sales_tax')
|
||||
|
||||
@@ -380,3 +380,79 @@ class TestAccountAccount(AccountTestInvoicingCommon):
|
||||
move_type="out_invoice"
|
||||
)
|
||||
self.assertFalse(account.id in results_2, "Deprecated account should NOT appear in account suggestions")
|
||||
|
||||
@freeze_time('2017-01-01')
|
||||
def test_account_opening_balance(self):
|
||||
company = self.env.company
|
||||
account = self.company_data['default_account_revenue']
|
||||
balancing_account = company.get_unaffected_earnings_account()
|
||||
|
||||
self.assertFalse(company.account_opening_move_id)
|
||||
|
||||
account.opening_debit = 300
|
||||
self.cr.precommit.run()
|
||||
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'account_id': account.id, 'balance': 300.0},
|
||||
{'account_id': balancing_account.id, 'balance': -300.0},
|
||||
])
|
||||
|
||||
account.opening_credit = 500
|
||||
self.cr.precommit.run()
|
||||
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'account_id': account.id, 'balance': 300.0},
|
||||
{'account_id': account.id, 'balance': -500.0},
|
||||
{'account_id': balancing_account.id, 'balance': 200.0},
|
||||
])
|
||||
|
||||
account.opening_balance = 0
|
||||
self.cr.precommit.run()
|
||||
self.assertFalse(company.account_opening_move_id.line_ids)
|
||||
|
||||
account.currency_id = self.currency_data['currency']
|
||||
account.opening_debit = 100
|
||||
self.cr.precommit.run()
|
||||
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
|
||||
{'account_id': balancing_account.id, 'balance': -100.0, 'amount_currency': -100.0},
|
||||
])
|
||||
|
||||
company.account_opening_move_id.write({'line_ids': [
|
||||
Command.create({
|
||||
'account_id': account.id,
|
||||
'balance': 100.0,
|
||||
'amount_currency': 200.0,
|
||||
'currency_id': account.currency_id.id,
|
||||
}),
|
||||
Command.create({
|
||||
'account_id': balancing_account.id,
|
||||
'balance': -100.0,
|
||||
}),
|
||||
]})
|
||||
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
|
||||
{'account_id': balancing_account.id, 'balance': -100.0, 'amount_currency': -100.0},
|
||||
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
|
||||
{'account_id': balancing_account.id, 'balance': -100.0, 'amount_currency': -100.0},
|
||||
])
|
||||
|
||||
account.opening_credit = 1000
|
||||
self.cr.precommit.run()
|
||||
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
|
||||
{'account_id': account.id, 'balance': 100.0, 'amount_currency': 200.0},
|
||||
{'account_id': account.id, 'balance': -1000.0, 'amount_currency': -2000.0},
|
||||
{'account_id': balancing_account.id, 'balance': 800.0, 'amount_currency': 800.0},
|
||||
])
|
||||
|
||||
account.opening_debit = 1000
|
||||
self.cr.precommit.run()
|
||||
self.assertRecordValues(company.account_opening_move_id.line_ids.sorted(), [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'account_id': account.id, 'balance': 1000.0, 'amount_currency': 2000.0},
|
||||
{'account_id': account.id, 'balance': -1000.0, 'amount_currency': -2000.0},
|
||||
])
|
||||
|
||||
@@ -76,9 +76,9 @@
|
||||
<field name="company_id" column_invisible="True"/>
|
||||
<field name="account_type" widget="account_type_selection"/>
|
||||
<field name="reconcile" widget="boolean_toggle"/>
|
||||
<field name="opening_debit"/>
|
||||
<field name="opening_credit"/>
|
||||
<field name="opening_balance" optional="hide"/>
|
||||
<field name="opening_debit" options="{'no_symbol': True}"/>
|
||||
<field name="opening_credit" options="{'no_symbol': True}"/>
|
||||
<field name="opening_balance" optional="hide" options="{'no_symbol': True}"/>
|
||||
<field name="tax_ids" optional="hide" widget="many2many_tags"/>
|
||||
<field name="tag_ids" optional="hide" widget="many2many_tags"/>
|
||||
<field name="allowed_journal_ids" optional="hide" widget="many2many_tags"/>
|
||||
|
||||
Reference in New Issue
Block a user