diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 9330d3c3e17..dedd265cf97 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -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 diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 37bb600b1b2..583eb3ce97f 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -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, diff --git a/addons/account/models/company.py b/addons/account/models/company.py index a1fc0900883..4100f2ba29c 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -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: diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 2bd78b9e15c..f60834dd7e3 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -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') diff --git a/addons/account/tests/test_account_move_entry.py b/addons/account/tests/test_account_move_entry.py index 1f3fb61d844..7a7dffa6733 100644 --- a/addons/account/tests/test_account_move_entry.py +++ b/addons/account/tests/test_account_move_entry.py @@ -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, + } + ]) diff --git a/addons/account/tests/test_account_move_in_refund.py b/addons/account/tests/test_account_move_in_refund.py index 454ee1d6052..4e8be6f8033 100644 --- a/addons/account/tests/test_account_move_in_refund.py +++ b/addons/account/tests/test_account_move_in_refund.py @@ -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, + }) diff --git a/addons/account/tests/test_account_move_out_refund.py b/addons/account/tests/test_account_move_out_refund.py index fc5321a63ee..7651bb1517d 100644 --- a/addons/account/tests/test_account_move_out_refund.py +++ b/addons/account/tests/test_account_move_out_refund.py @@ -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, + }) diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index db9beae3eee..4c5e30fa273 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -639,6 +639,23 @@ +

Storno Accounting

+
+
+
+ +
+
+
+
+
diff --git a/addons/l10n_cn/data/l10n_cn_chart_data.xml b/addons/l10n_cn/data/l10n_cn_chart_data.xml index 80f804255bd..166ba2a5676 100644 --- a/addons/l10n_cn/data/l10n_cn_chart_data.xml +++ b/addons/l10n_cn/data/l10n_cn_chart_data.xml @@ -22,6 +22,7 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html 1012 + diff --git a/addons/l10n_cz/data/l10n_cz_coa_data.xml b/addons/l10n_cz/data/l10n_cz_coa_data.xml index 620fb50f490..2023bef17ff 100644 --- a/addons/l10n_cz/data/l10n_cz_coa_data.xml +++ b/addons/l10n_cz/data/l10n_cz_coa_data.xml @@ -10,6 +10,7 @@ 261 + diff --git a/addons/l10n_hr/data/l10n_hr_chart_data.xml b/addons/l10n_hr/data/l10n_hr_chart_data.xml index fe497fabd41..aa5f163b19c 100644 --- a/addons/l10n_hr/data/l10n_hr_chart_data.xml +++ b/addons/l10n_hr/data/l10n_hr_chart_data.xml @@ -13,6 +13,7 @@ 0 + diff --git a/addons/l10n_pl/data/l10n_pl_chart_data.xml b/addons/l10n_pl/data/l10n_pl_chart_data.xml index 3cb19e41036..c539c7d4ad3 100644 --- a/addons/l10n_pl/data/l10n_pl_chart_data.xml +++ b/addons/l10n_pl/data/l10n_pl_chart_data.xml @@ -21,6 +21,7 @@ 12-000-000 11-090-000 + diff --git a/addons/l10n_ro/data/l10n_ro_chart_data.xml b/addons/l10n_ro/data/l10n_ro_chart_data.xml index 21c3aa4e397..829180e8619 100644 --- a/addons/l10n_ro/data/l10n_ro_chart_data.xml +++ b/addons/l10n_ro/data/l10n_ro_chart_data.xml @@ -11,5 +11,6 @@ 6 + diff --git a/addons/l10n_si/data/l10n_si_chart_data.xml b/addons/l10n_si/data/l10n_si_chart_data.xml index 351e5386e96..702c0d2d4ff 100644 --- a/addons/l10n_si/data/l10n_si_chart_data.xml +++ b/addons/l10n_si/data/l10n_si_chart_data.xml @@ -10,5 +10,6 @@ 6 + diff --git a/addons/l10n_sk/data/l10n_sk_coa_data.xml b/addons/l10n_sk/data/l10n_sk_coa_data.xml index 8c745e57782..2e5ae1eab2e 100644 --- a/addons/l10n_sk/data/l10n_sk_coa_data.xml +++ b/addons/l10n_sk/data/l10n_sk_coa_data.xml @@ -10,6 +10,7 @@ 261 + diff --git a/addons/l10n_ua/data/account_chart_template.xml b/addons/l10n_ua/data/account_chart_template.xml index e09895dbd9e..5d9d3dc03aa 100644 --- a/addons/l10n_ua/data/account_chart_template.xml +++ b/addons/l10n_ua/data/account_chart_template.xml @@ -9,6 +9,7 @@ 6 + @@ -19,5 +20,6 @@ 6 +