From 35e107bcbdfeca99f7394d3a6a8490bcb0dfae4f Mon Sep 17 00:00:00 2001 From: "Nicolas (vin)" Date: Thu, 28 Jul 2022 08:03:15 +0000 Subject: [PATCH] [FIX] account: fix accounting dates Fix the accounting date in a few scenarios, such as exchange difference or CABA moves reversal. Also avoid updating the currency rate when reversing exchange difference moves. closes odoo/odoo#98543 X-original-commit: 2b8df0c111a785a9e51dc2f528ac5b00cb0eadb9 Signed-off-by: Laurent Smet Signed-off-by: Nicolas Viseur --- .../account/models/account_full_reconcile.py | 12 +- addons/account/models/account_move.py | 25 +- addons/account/models/account_move_line.py | 4 +- .../models/account_partial_reconcile.py | 12 +- addons/account/tests/__init__.py | 1 + .../tests/test_account_move_date_algorithm.py | 231 ++++++++++++++++++ 6 files changed, 260 insertions(+), 25 deletions(-) create mode 100644 addons/account/tests/test_account_move_date_algorithm.py diff --git a/addons/account/models/account_full_reconcile.py b/addons/account/models/account_full_reconcile.py index f2ff0ea0895..9516045ffba 100644 --- a/addons/account/models/account_full_reconcile.py +++ b/addons/account/models/account_full_reconcile.py @@ -27,11 +27,11 @@ class AccountFullReconcile(models.Model): res = super().unlink() # Reverse all exchange moves at once. - today = fields.Date.context_today(self) - default_values_list = [{ - 'date': today, - 'ref': _('Reversal of: %s') % move.name, - } for move in moves_to_reverse] - moves_to_reverse._reverse_moves(default_values_list, cancel=True) + if moves_to_reverse: + default_values_list = [{ + 'date': move._get_accounting_date(move.date, move._affect_tax_report()), + 'ref': _('Reversal of: %s') % move.name, + } for move in moves_to_reverse] + moves_to_reverse._reverse_moves(default_values_list, cancel=True) return res diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 3cb6e528a57..0c5088a3b6a 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -2008,13 +2008,14 @@ class AccountMove(models.Model): def copy_data(self, default=None): data_list = super().copy_data(default) - for data in data_list: - data['line_ids'] = [ - (command, _id, line_vals) - for command, _id, line_vals in data['line_ids'] - if command == 0 - and line_vals.get('display_type') not in ('payment_term', 'tax', 'rounding') - ] + for move, data in zip(self, data_list): + if move.move_type in ('out_invoice', 'in_invoice'): + data['line_ids'] = [ + (command, _id, line_vals) + for command, _id, line_vals in data['line_ids'] + if command == Command.CREATE + and line_vals.get('display_type') not in ('payment_term', 'tax', 'rounding') + ] if not self.journal_id.active and 'journal_id' in data_list: del default['journal_id'] return data_list @@ -3112,10 +3113,12 @@ class AccountMove(models.Model): return action def action_post(self): - if self.payment_id: - self.payment_id.action_post() - else: - self._post(soft=False) + moves_with_payments = self.filtered('payment_id') + other_moves = self - moves_with_payments + if moves_with_payments: + moves_with_payments.payment_id.action_post() + if other_moves: + other_moves._post(soft=False) return False def js_assign_outstanding_line(self, line_id): diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index 889b4892301..447665dfaa2 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -1,7 +1,7 @@ import ast from collections import defaultdict from contextlib import contextmanager -from datetime import date +from datetime import date, timedelta from functools import lru_cache from odoo import api, fields, models, Command, _ @@ -1828,7 +1828,7 @@ class AccountMoveLine(models.Model): move_vals = { 'move_type': 'entry', - 'date': max(exchange_date or date.min, company._get_user_fiscal_lock_date()), + 'date': max(exchange_date or date.min, company._get_user_fiscal_lock_date() + timedelta(days=1)), 'journal_id': journal.id, 'line_ids': [], } diff --git a/addons/account/models/account_partial_reconcile.py b/addons/account/models/account_partial_reconcile.py index b449c745401..6a7ec11c25b 100644 --- a/addons/account/models/account_partial_reconcile.py +++ b/addons/account/models/account_partial_reconcile.py @@ -110,12 +110,12 @@ class AccountPartialReconcile(models.Model): full_to_unlink.unlink() # Reverse CABA entries. - today = fields.Date.context_today(self) - default_values_list = [{ - 'date': move.date if move.date > (move.company_id.period_lock_date or date.min) else today, - 'ref': _('Reversal of: %s') % move.name, - } for move in moves_to_reverse] - moves_to_reverse._reverse_moves(default_values_list, cancel=True) + if moves_to_reverse: + default_values_list = [{ + 'date': move._get_accounting_date(move.date, move._affect_tax_report()), + 'ref': _('Reversal of: %s') % move.name, + } for move in moves_to_reverse] + moves_to_reverse._reverse_moves(default_values_list, cancel=True) return res diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index 65a5440755a..57420cd1bd1 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -7,6 +7,7 @@ from . import test_account_move_out_refund from . import test_account_move_in_invoice from . import test_account_move_in_refund from . import test_account_move_entry +from . import test_account_move_date_algorithm from . import test_invoice_tax_totals from . import test_account_journal from . import test_account_account diff --git a/addons/account/tests/test_account_move_date_algorithm.py b/addons/account/tests/test_account_move_date_algorithm.py new file mode 100644 index 00000000000..7d674b7a24e --- /dev/null +++ b/addons/account/tests/test_account_move_date_algorithm.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +from odoo import fields, Command +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.tests import tagged + +import freezegun + + +@tagged('post_install', '-at_install') +class TestAccountMoveDateAlgorithm(AccountTestInvoicingCommon): + + # ------------------------------------------------------------------------- + # HELPERS + # ------------------------------------------------------------------------- + + def _create_invoice(self, move_type, date, **kwargs): + return self.env['account.move'].create({ + 'invoice_date': date, + 'partner_id': self.partner_a.id, + **kwargs, + 'move_type': move_type, + 'date': date, + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 1200.0, + 'tax_ids': [], + **line_kwargs, + }) + for line_kwargs in kwargs.get('invoice_line_ids', [{}]) + ], + }) + + def _create_payment(self, date, **kwargs): + return self.env['account.payment'].create({ + 'partner_id': self.partner_a.id, + 'payment_type': 'inbound', + 'partner_type': 'customer', + **kwargs, + 'date': date, + }) + + def _set_lock_date(self, lock_date): + self.env.company.fiscalyear_lock_date = fields.Date.from_string(lock_date) + + def _reverse_invoice(self, invoice): + move_reversal = self.env['account.move.reversal']\ + .with_context(active_model="account.move", active_ids=invoice.ids)\ + .create({ + 'journal_id': invoice.journal_id.id, + 'reason': "no reason", + 'refund_method': 'cancel', + }) + reversal = move_reversal.reverse_moves() + return self.env['account.move'].browse(reversal['res_id']) + + # ------------------------------------------------------------------------- + # TESTS + # ------------------------------------------------------------------------- + + @freezegun.freeze_time('2017-01-12') + def test_out_invoice_date_with_lock_date(self): + self._set_lock_date('2016-12-31') + move = self._create_invoice('out_invoice', '2016-01-01') + move.action_post() + + self.assertRecordValues(move, [{ + 'invoice_date': fields.Date.from_string('2016-01-01'), + 'date': fields.Date.from_string('2017-01-12'), + }]) + + @freezegun.freeze_time('2017-01-12') + def test_out_invoice_reverse_date_with_lock_date(self): + move = self._create_invoice('out_invoice', '2016-01-01') + move.action_post() + self._set_lock_date('2016-12-31') + reverse_move = self._reverse_invoice(move) + + self.assertRecordValues(reverse_move, [{ + 'invoice_date': fields.Date.from_string('2017-01-12'), + 'date': fields.Date.from_string('2017-01-12'), + }]) + + @freezegun.freeze_time('2017-01-12') + def test_out_refund_date_with_lock_date(self): + self._set_lock_date('2016-12-31') + move = self._create_invoice('out_refund', '2016-01-01') + move.action_post() + + self.assertRecordValues(move, [{ + 'invoice_date': fields.Date.from_string('2016-01-01'), + 'date': fields.Date.from_string('2017-01-12'), + }]) + + @freezegun.freeze_time('2017-01-12') + def test_out_refund_reverse_date_with_lock_date(self): + move = self._create_invoice('out_refund', '2016-01-01') + move.action_post() + self._set_lock_date('2016-12-31') + reverse_move = self._reverse_invoice(move) + + self.assertRecordValues(reverse_move, [{'date': fields.Date.from_string('2017-01-12')}]) + + @freezegun.freeze_time('2017-01-12') + def test_in_invoice_date_with_lock_date(self): + self._set_lock_date('2016-12-31') + move = self._create_invoice('in_invoice', '2016-01-01') + move.action_post() + + self.assertRecordValues(move, [{ + 'invoice_date': fields.Date.from_string('2016-01-01'), + 'date': fields.Date.from_string('2017-01-12'), + }]) + + @freezegun.freeze_time('2017-01-12') + def test_in_invoice_reverse_date_with_lock_date(self): + move = self._create_invoice('in_invoice', '2016-01-01') + move.action_post() + self._set_lock_date('2016-12-31') + reverse_move = self._reverse_invoice(move) + + self.assertRecordValues(reverse_move, [{ + 'invoice_date': fields.Date.from_string('2017-01-12'), + 'date': fields.Date.from_string('2017-01-12'), + }]) + + @freezegun.freeze_time('2017-01-12') + def test_in_refund_date_with_lock_date(self): + self._set_lock_date('2016-12-31') + move = self._create_invoice('in_refund', '2016-01-01') + move.action_post() + + self.assertRecordValues(move, [{ + 'invoice_date': fields.Date.from_string('2016-01-01'), + 'date': fields.Date.from_string('2017-01-12'), + }]) + + @freezegun.freeze_time('2017-01-12') + def test_in_refund_reverse_date_with_lock_date(self): + move = self._create_invoice('in_refund', '2016-01-01') + move.action_post() + self._set_lock_date('2016-12-31') + reverse_move = self._reverse_invoice(move) + + self.assertRecordValues(reverse_move, [{'date': fields.Date.from_string('2017-01-12')}]) + + @freezegun.freeze_time('2017-02-12') + def test_reconcile_with_lock_date(self): + invoice = self._create_invoice('out_invoice', '2016-01-01', currency_id=self.currency_data['currency'].id) + refund = self._create_invoice('out_refund', '2017-01-01', currency_id=self.currency_data['currency'].id) + (invoice + refund).action_post() + self._set_lock_date('2017-01-31') + + res = (invoice + refund).line_ids\ + .filtered(lambda x: x.account_id.account_type == 'asset_receivable')\ + .reconcile() + exchange_move = res['partials'].exchange_move_id + + self.assertRecordValues(exchange_move, [{ + 'date': fields.Date.from_string('2017-02-01'), + 'amount_total_signed': 200.0, + }]) + + @freezegun.freeze_time('2017-02-12') + def test_unreconcile_with_lock_date(self): + invoice = self._create_invoice('out_invoice', '2016-01-01', currency_id=self.currency_data['currency'].id) + refund = self._create_invoice('out_refund', '2017-01-01', currency_id=self.currency_data['currency'].id) + (invoice + refund).action_post() + + res = (invoice + refund).line_ids\ + .filtered(lambda x: x.account_id.account_type == 'asset_receivable')\ + .reconcile() + exchange_move = res['partials'].exchange_move_id + + self._set_lock_date('2017-01-31') + (invoice + refund).line_ids.remove_move_reconcile() + + reverse_exchange_move = exchange_move.line_ids.matched_credit_ids.credit_move_id.move_id + self.assertRecordValues(reverse_exchange_move, [{ + 'date': fields.Date.from_string('2017-02-12'), + 'amount_total_signed': 200.0, + }]) + + def test_caba_with_lock_date(self): + tax_waiting_account = self.env['account.account'].create({ + 'name': 'TAX_WAIT', + 'code': 'TWAIT', + 'account_type': 'liability_current', + 'reconcile': True, + }) + tax = self.env['account.tax'].create({ + 'name': 'cash basis 10%', + 'type_tax_use': 'sale', + 'amount': 10, + 'tax_exigibility': 'on_payment', + 'cash_basis_transition_account_id': tax_waiting_account.id, + }) + + invoice = self._create_invoice( + 'out_invoice', '2016-01-01', + currency_id=self.currency_data['currency'].id, + invoice_line_ids=[{'tax_ids': [Command.set(tax.ids)]}], + ) + payment = self._create_payment('2016-02-01', amount=invoice.amount_total) + (invoice + payment.move_id).action_post() + + self._set_lock_date('2017-01-03') + + with freezegun.freeze_time('2017-01-12'): + (invoice + payment.move_id).line_ids\ + .filtered(lambda x: x.account_id.account_type == 'asset_receivable')\ + .reconcile() + + caba_move = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', invoice.id)]) + + self.assertRecordValues(caba_move, [{ + 'date': fields.Date.from_string('2017-01-12'), + 'amount_total_signed': 440.0, + }]) + + self._set_lock_date('2017-02-01') + + with freezegun.freeze_time('2017-03-12'): + (invoice + payment.move_id).line_ids.remove_move_reconcile() + + reverse_exchange_move = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', invoice.id)]) - caba_move + + self.assertRecordValues(reverse_exchange_move, [{ + 'date': fields.Date.from_string('2017-02-28'), + 'amount_total_signed': 440.0, + }])