[IMP] account: implementation of storno accounting

Storno accounting is the term used to described the process of recording
a reverse action as a negative amount on the same site instead of a
positive amount on the opposite site of the credt/debit.
For instance, when one creates a credit note for an invoice, the values
of debit and credit will be kept in the same column with negative signs
instead of swapping columns like in non-Storno accounting.

It is a business practice commonly used in Eastern European countries.

Countries where Storno accounting is mandatory or considered as best
practice would be :
Czech Republic, Poland, Romania, Russia, Slovakia, Ukraine, Croatia,
Bosnia and Herzegovina, Serbia, Romania, China, Russia, Slovenia

It is available as an option in the settings and is automatically
activated for some countries which use this type of accounting.

otask-2064899

closes odoo/odoo#79581

Related: odoo/upgrade#3212
Signed-off-by: William André (wan) <wan@odoo.com>
Co-authored-by: william-andre <wan@odoo.com>
Co-authored-by: qdp-odoo <qdp@odoo.com>
This commit is contained in:
Ricardo Gomes Rodrigues (rigr)
2022-02-03 15:21:28 +00:00
co-authored by william-andre qdp-odoo
parent 676c5b5f69
commit 0b38cc527b
16 changed files with 293 additions and 34 deletions
+86 -34
View File
@@ -175,6 +175,8 @@ class AccountMove(models.Model):
], string='Type', required=True, store=True, index=True, readonly=True, tracking=True,
default="entry", change_default=True)
type_name = fields.Char('Type Name', compute='_compute_type_name')
is_storno = fields.Boolean(compute='_compute_is_storno', store=True, copy=False, readonly=False,
help='Utility field to express whether the journal entry is subject to storno accounting. That is when the company uses storno and the journal entry is a refund or a reversal.')
to_check = fields.Boolean(string='To Check', default=False, tracking=True,
help='If this checkbox is ticked, it means that the user was not sure of all the related information at the time of the creation of the move and that the move needs to be checked again.')
journal_id = fields.Many2one('account.journal', string='Journal', required=True, readonly=True,
@@ -809,11 +811,12 @@ class AccountMove(models.Model):
self.company_id,
self.date or fields.Date.context_today(self),
)
currency_id = taxes_map_entry['grouping_dict']['currency_id']
to_write_on_line = {
'amount_currency': taxes_map_entry['amount'],
'currency_id': taxes_map_entry['grouping_dict']['currency_id'],
'debit': balance > 0.0 and balance or 0.0,
'credit': balance < 0.0 and -balance or 0.0,
'currency_id': currency_id,
'debit': self._get_debit_from_balance(balance, currency_id),
'credit': self._get_credit_from_balance(balance, currency_id),
'tax_base_amount': tax_base_amount,
}
@@ -892,8 +895,8 @@ class AccountMove(models.Model):
:return: The newly created rounding line.
'''
rounding_line_vals = {
'debit': diff_balance > 0.0 and diff_balance or 0.0,
'credit': diff_balance < 0.0 and -diff_balance or 0.0,
'debit': self._get_debit_from_balance(diff_balance, self.currency_id.id),
'credit': self._get_credit_from_balance(diff_balance, self.currency_id.id),
'quantity': 1.0,
'amount_currency': diff_amount_currency,
'partner_id': self.partner_id.id,
@@ -1062,16 +1065,16 @@ class AccountMove(models.Model):
candidate.update({
'date_maturity': date_maturity,
'amount_currency': -amount_currency,
'debit': balance < 0.0 and -balance or 0.0,
'credit': balance > 0.0 and balance or 0.0,
'debit': self._get_debit_from_balance(-balance, currency.id),
'credit': self._get_credit_from_balance(-balance, currency.id),
})
else:
# Create new line.
create_method = in_draft_mode and self.env['account.move.line'].new or self.env['account.move.line'].create
candidate = create_method({
'name': self.payment_reference or '',
'debit': balance < 0.0 and -balance or 0.0,
'credit': balance > 0.0 and balance or 0.0,
'debit': self._get_debit_from_balance(-balance, currency.id),
'credit': self._get_credit_from_balance(-balance, currency.id),
'quantity': 1.0,
'amount_currency': -amount_currency,
'date_maturity': date_maturity,
@@ -1162,6 +1165,11 @@ class AccountMove(models.Model):
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('move_type')
def _compute_is_storno(self):
for move in self:
move.is_storno = move.is_storno or (move.move_type in ('out_refund', 'in_refund') and move.company_id.account_storno)
@api.depends('partner_id')
def _compute_partner_shipping_id(self):
for move in self:
@@ -2054,22 +2062,40 @@ class AccountMove(models.Model):
self.env['account.move.line'].flush(self.env['account.move.line']._fields)
self.env['account.move'].flush(['journal_id'])
self._cr.execute('''
SELECT line.move_id, ROUND(SUM(line.debit - line.credit), currency.decimal_places)
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
JOIN res_company company ON company.id = journal.company_id
JOIN res_currency currency ON currency.id = company.currency_id
WHERE line.move_id IN %s
GROUP BY line.move_id, currency.decimal_places
HAVING ROUND(SUM(line.debit - line.credit), currency.decimal_places) != 0.0;
WITH error_moves AS (
SELECT line.move_id,
ROUND(SUM(line.debit - line.credit), currency.decimal_places) balance,
bool_or((not is_storno) AND (credit + debit < 0)) negative_normal,
bool_or((is_storno) AND (credit + debit > 0)) positive_storno
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
JOIN res_company company ON company.id = journal.company_id
JOIN res_currency currency ON currency.id = company.currency_id
WHERE line.move_id IN %s
GROUP BY line.move_id, currency.decimal_places
)
SELECT *
FROM error_moves
WHERE balance !=0
OR negative_normal
OR positive_storno
''', [tuple(self.ids)])
query_res = self._cr.fetchall()
if query_res:
ids = [res[0] for res in query_res]
sums = [res[1] for res in query_res]
raise UserError(_("Cannot create unbalanced journal entry. Ids: %s\nDifferences debit - credit: %s") % (ids, sums))
error_msg = _("There was a problem with the following move(s):\n")
for move in query_res:
id_, balance, negative_normal, positive_storno = 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.currency_id))
if negative_normal:
error_msg += _("\tThe debit and credit should be positive and only one can be set\n")
if positive_storno:
error_msg += _("\tThe debit and credit should be negative and only one can be set\n")
raise UserError(error_msg)
def _check_fiscalyear_lock_date(self):
for move in self:
@@ -2106,6 +2132,19 @@ class AccountMove(models.Model):
# -------------------------------------------------------------------------
# LOW-LEVEL METHODS
# -------------------------------------------------------------------------
def _get_debit_from_balance(self, balance, currency_id):
currency = self.env['res.currency'].browse(currency_id)
compare_res = currency.compare_amounts(balance, 0.0)
if self.is_storno:
return balance if compare_res < 0.0 else 0.0
return balance if compare_res > 0.0 else 0.0
def _get_credit_from_balance(self, balance, currency_id):
currency = self.env['res.currency'].browse(currency_id)
compare_res = currency.compare_amounts(balance, 0.0)
if self.is_storno:
return -balance if compare_res > 0.0 else 0.0
return -balance if compare_res < 0.0 else 0.0
def _move_autocomplete_invoice_lines_values(self):
''' This method recomputes dynamic lines on the current journal entry that include taxes, cash rounding
@@ -2626,6 +2665,11 @@ class AccountMove(models.Model):
move_vals = self.with_context(include_business_fields=True).copy_data(default=default_values)[0]
is_storno_move = move_vals['move_type'] in ('entry', 'out_refund', 'in_refund') and self.company_id.account_storno
if is_storno_move:
move_vals['is_storno'] = True
tax_repartition_lines_mapping = compute_tax_repartition_lines_mapping(move_vals)
is_refund = False
if move_vals['move_type'] in ('out_refund', 'in_refund'):
is_refund = True
@@ -2642,9 +2686,15 @@ class AccountMove(models.Model):
for line_command in move_vals.get('line_ids', []):
line_vals = line_command[2] # (0, 0, {...})
# ==== Inverse debit / credit / amount_currency ====
# ==== Non-Storno: Inverse debit <-> credit and negate amount_currency ====
amount_currency = -line_vals.get('amount_currency', 0.0)
balance = line_vals['credit'] - line_vals['debit']
debit = line_vals['credit']
credit = line_vals['debit']
# ==== Storno: Negate debit / credit / amount_currency in same column====
if is_storno_move:
debit = -line_vals['debit']
credit = -line_vals['credit']
if 'tax_tag_invert' in line_vals:
# This is an editable computed field; we want to it recompute itself
@@ -2652,8 +2702,8 @@ class AccountMove(models.Model):
line_vals.update({
'amount_currency': amount_currency,
'debit': balance > 0.0 and balance or 0.0,
'credit': balance < 0.0 and -balance or 0.0,
'debit': debit,
'credit': credit,
})
if not is_refund:
@@ -3192,10 +3242,8 @@ class AccountMove(models.Model):
# Inverse all invoice_line_ids
for cmd, virtualid, line_vals in new_invoice_line_ids:
line_vals.update({
'quantity' : -line_vals['quantity'],
'amount_currency' : -line_vals['amount_currency'],
'debit' : line_vals['credit'],
'credit' : line_vals['debit']
'quantity': -line_vals['quantity'],
'amount_currency': -line_vals['amount_currency'],
})
move.write({
'move_type': move.move_type.replace('invoice', 'refund'),
@@ -3508,6 +3556,10 @@ class AccountMoveLine(models.Model):
company_currency_id = fields.Many2one(related='company_id.currency_id', string='Company Currency',
readonly=True, store=True,
help='Utility field to express amount currency')
is_storno = fields.Boolean(
related='move_id.is_storno',
string='Company Storno Accounting',
help='Utility field to express whether the journal item is subject to storno accounting')
account_id = fields.Many2one('account.account', string='Account',
index=True, ondelete="cascade",
domain="[('deprecated', '=', False), ('company_id', '=', 'company_id'),('is_off_balance', '=', False)]",
@@ -3631,7 +3683,7 @@ class AccountMoveLine(models.Model):
_sql_constraints = [
(
'check_credit_debit',
'CHECK(credit + debit>=0 AND credit * debit=0)',
'CHECK(credit * debit=0)',
'Wrong credit or debit value in accounting entry !'
),
(
@@ -3979,8 +4031,8 @@ class AccountMoveLine(models.Model):
return {
'amount_currency': amount_currency,
'currency_id': currency.id,
'debit': balance > 0.0 and balance or 0.0,
'credit': balance < 0.0 and -balance or 0.0,
'debit': self.move_id._get_debit_from_balance(balance, company.currency_id.id),
'credit': self.move_id._get_credit_from_balance(balance, company.currency_id.id),
}
def _get_fields_onchange_balance(self, quantity=None, discount=None, amount_currency=None, move_type=None, currency=None, taxes=None, price_subtotal=None, force_computation=False):
@@ -4198,8 +4250,8 @@ class AccountMoveLine(models.Model):
for line in self:
company = line.move_id.company_id
balance = line.currency_id._convert(line.amount_currency, company.currency_id, company, line.move_id.date or fields.Date.context_today(line))
line.debit = balance if balance > 0.0 else 0.0
line.credit = -balance if balance < 0.0 else 0.0
line.debit = line.move_id._get_debit_from_balance(balance, company.currency_id.id)
line.credit = line.move_id._get_credit_from_balance(balance, company.currency_id.id)
if not line.move_id.is_invoice(include_receipts=True):
continue
+2
View File
@@ -93,6 +93,7 @@ class AccountChartTemplate(models.Model):
"templates, this is useful when you want to generate accounts of this template only when loading its child template.")
currency_id = fields.Many2one('res.currency', string='Currency', required=True)
use_anglo_saxon = fields.Boolean(string="Use Anglo-Saxon accounting", default=False)
use_storno_accounting = fields.Boolean(string="Use Storno accounting", default=False)
complete_tax_set = fields.Boolean(string='Complete Set of Taxes', default=True,
help="This boolean helps you to choose if you want to propose to the user to encode the sale and purchase rates or choose from list "
"of taxes. This last choice assumes that the set of tax defined on this template is complete")
@@ -259,6 +260,7 @@ class AccountChartTemplate(models.Model):
company.write({'currency_id': self.currency_id.id,
'anglo_saxon_accounting': self.use_anglo_saxon,
'account_storno': self.use_storno_accounting,
'bank_account_code_prefix': self.bank_account_code_prefix,
'cash_account_code_prefix': self.cash_account_code_prefix,
'transfer_account_code_prefix': self.transfer_account_code_prefix,
+3
View File
@@ -167,6 +167,9 @@ class ResCompany(models.Model):
help="Account that will be set on lines created in cash basis journal entry and used to keep track of the "
"tax base amount.")
# Storno Accounting
account_storno = fields.Boolean(string="Storno accounting", readonly=False)
@api.constrains('account_opening_move_id', 'fiscalyear_last_day', 'fiscalyear_last_month')
def _check_fiscalyear_last_day(self):
# if the user explicitly chooses the 29th of February we allow it:
@@ -145,6 +145,9 @@ class ResConfigSettings(models.TransientModel):
# Technical field to hide country specific fields from accounting configuration
country_code = fields.Char(related='company_id.account_fiscal_country_id.code', readonly=True)
# Storno Accounting
account_storno = fields.Boolean(string="Storno accounting", readonly=False, related='company_id.account_storno')
# Allows for the use of a different delivery address
group_sale_delivery_address = fields.Boolean("Customer Addresses", implied_group='account.group_delivery_invoice_address')
@@ -53,6 +53,18 @@ class TestAccountMove(AccountTestInvoicingCommon):
}),
]
})
cls.entry_line_vals_1 = {
'name': 'Line 1',
'account_id': cls.company_data['default_account_revenue'].id,
'debit': 500.0,
'credit': 0.0,
}
cls.entry_line_vals_2 = {
'name': 'Line 2',
'account_id': cls.company_data['default_account_expense'].id,
'debit': 0.0,
'credit': 500.0,
}
def test_custom_currency_on_account_1(self):
custom_account = self.company_data['default_account_revenue'].copy()
@@ -510,3 +522,38 @@ class TestAccountMove(AccountTestInvoicingCommon):
move.action_post()
self.assertEqual(move.state, 'posted')
def test_entry_reverse_storno(self):
# Test creating journal entries and reverting them
# while in Storno accounting
self.env.company.account_storno = True
move = self.env['account.move'].create({
'move_type': 'entry',
'date': fields.Date.from_string('2021-01-01'),
'line_ids': [
(0, None, self.entry_line_vals_1),
(0, None, self.entry_line_vals_2),
]
})
move.action_post()
move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=move.ids).create({
'date': fields.Date.from_string('2021-02-01'),
'refund_method': 'refund',
'journal_id': move.journal_id.id,
})
reversal = move_reversal.reverse_moves()
reversed_move = self.env['account.move'].browse(reversal['res_id'])
self.assertRecordValues(reversed_move.line_ids, [
{
**self.entry_line_vals_1,
'debit': -500.0,
'credit': 0.0,
}, {
**self.entry_line_vals_2,
'debit': 0.0,
'credit': -500.0,
}
])
@@ -1010,3 +1010,66 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
**self.move_vals,
'currency_id': self.currency_data['currency'].id,
})
def test_in_refund_create_storno(self):
# Test creating an account_move refund (credit note)
# with multiple lines while in Storno accounting
self.env.company.account_storno = True
move = self.env['account.move'].create({
'move_type': 'in_refund',
'partner_id': self.partner_a.id,
'invoice_date': fields.Date.from_string('2019-01-01'),
'currency_id': self.currency_data['currency'].id,
'invoice_payment_term_id': self.pay_terms_a.id,
'invoice_line_ids': [
(0, None, self.product_line_vals_1),
(0, None, self.product_line_vals_2),
]
})
self.assertInvoiceValues(move, [
{
**self.product_line_vals_1,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -800.0,
'balance': -400.0,
'debit': -400.0,
'credit': 0.0,
},
{
**self.product_line_vals_2,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -160.0,
'balance': -80.0,
'debit': -80.0,
'credit': 0.0,
},
{
**self.tax_line_vals_1,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -144.0,
'balance': -72.0,
'debit': -72.0,
'credit': 0.0,
},
{
**self.tax_line_vals_2,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -24.0,
'balance': -12.0,
'debit': -12.0,
'credit': 0.0,
},
{
**self.term_line_vals_1,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 1128.0,
'balance': 564.0,
'debit': 0.0,
'credit': -564.0,
},
], {
**self.move_vals,
'currency_id': self.currency_data['currency'].id,
})
@@ -993,3 +993,66 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
**self.move_vals,
'currency_id': self.currency_data['currency'].id,
})
def test_out_refund_create_storno(self):
# Test creating an account_move refund (credit note)
# with multiple lines while in Storno accounting
self.env.company.account_storno = True
move = self.env['account.move'].create({
'move_type': 'out_refund',
'partner_id': self.partner_a.id,
'invoice_date': fields.Date.from_string('2019-01-01'),
'currency_id': self.currency_data['currency'].id,
'invoice_payment_term_id': self.pay_terms_a.id,
'invoice_line_ids': [
(0, None, self.product_line_vals_1),
(0, None, self.product_line_vals_2),
]
})
self.assertInvoiceValues(move, [
{
**self.product_line_vals_1,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 1000.0,
'balance': 500.0,
'debit': 0.0,
'credit': -500.0,
},
{
**self.product_line_vals_2,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 200.0,
'balance': 100.0,
'debit': 0.0,
'credit': -100.0,
},
{
**self.tax_line_vals_1,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 180.0,
'balance': 90.0,
'debit': 0.0,
'credit': -90.0,
},
{
**self.tax_line_vals_2,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 30.0,
'balance': 15.0,
'debit': 0.0,
'credit': -15.0,
},
{
**self.term_line_vals_1,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -1410.0,
'balance': -705.0,
'debit': -705.0,
'credit': 0.0,
},
], {
**self.move_vals,
'currency_id': self.currency_data['currency'].id,
})
@@ -639,6 +639,23 @@
</div>
</div>
</div>
<h2>Storno Accounting</h2>
<div class="row mt16 o_settings_container" id="storno">
<div class="col-12 col-lg-6 o_setting_box"
id="enable_storno_accounting"
title="Allows you to use Storno accounting.">
<div class="o_setting_left_pane">
<field name="account_storno"/>
</div>
<div class="o_setting_right_pane">
<label for="account_storno"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
Use Storno accounting
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
@@ -22,6 +22,7 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html
<field name="transfer_account_code_prefix">1012</field>
<field name="spoken_languages" eval="'en_US'"/>
<field name="country_id" ref="base.cn"/>
<field name="use_storno_accounting" eval="True"/>
</record>
</data>
</odoo>
+1
View File
@@ -10,6 +10,7 @@
<field name="transfer_account_code_prefix">261</field>
<field name="currency_id" ref="base.CZK"/>
<field name="country_id" ref="base.cz"/>
<field name="use_storno_accounting" eval="True"/>
</record>
</data>
</odoo>
@@ -13,6 +13,7 @@
<field name="currency_id" ref="base.HRK"/>
<field name="code_digits">0</field>
<field name="country_id" ref="base.hr"/>
<field name="use_storno_accounting" eval="True"/>
</record>
</data>
@@ -21,6 +21,7 @@
<field name="cash_account_code_prefix">12-000-000</field>
<field name="transfer_account_code_prefix">11-090-000</field>
<field name="country_id" ref="base.pl"/>
<field name="use_storno_accounting" eval="True"/>
</record>
</data>
@@ -11,5 +11,6 @@
<field name="code_digits">6</field>
<field name="currency_id" ref="base.RON"/>
<field name="country_id" ref="base.ro"/>
<field name="use_storno_accounting" eval="True"/>
</record>
</odoo>
@@ -10,5 +10,6 @@
<field name="code_digits">6</field>
<field name="currency_id" ref="base.EUR"/>
<field name="country_id" ref="base.si"/>
<field name="use_storno_accounting" eval="True"/>
</record>
</odoo>
+1
View File
@@ -10,6 +10,7 @@
<field name="transfer_account_code_prefix">261</field>
<field name="currency_id" ref="base.EUR"/>
<field name="country_id" ref="base.sk"/>
<field name="use_storno_accounting" eval="True"/>
</record>
</data>
</odoo>
@@ -9,6 +9,7 @@
<field name="code_digits">6</field>
<field name="currency_id" ref="base.UAH"/>
<field name="country_id" ref="base.ua"/>
<field name="use_storno_accounting" eval="True"/>
</record>
<record id="l10n_ua_ias_chart_template" model="account.chart.template">
@@ -19,5 +20,6 @@
<field name="code_digits">6</field>
<field name="currency_id" ref="base.UAH"/>
<field name="country_id" ref="base.ua"/>
<field name="use_storno_accounting" eval="True"/>
</record>
</odoo>