From 152d1f4fc7e0c19410704e1f1dac0a65872f4802 Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Thu, 29 Aug 2019 08:53:28 +0000 Subject: [PATCH] [FIX] account: Fix some issues since account-pocalypse 1) Improve readability / performance of moves checks The queries checking the move name & the duplicate vendor reference was complex and unefficient for no reason. 2) Recompute amount_currency when reversing invoice When reversing an invoice, the amount_currency wasn't recomputed even when dealing with a refund date having another currency rate. 3) The refund must not keep the payment ref of the reverted invoice. The payment ref is stored inside the receivable/payable line's name. However, when duplicating the invoice to create a refund, this must be emptied until a new payment ref will be set. 4) Reintroduce user_id field on account.move The field user_id has been renamed to invoice_user_id. However, some mail templates need to access the generic user_id field for multiple models. 5) Fix 'misc' Journal Entry totals Misc journal entry must have an amount total always positive and not signed. Furthermore, we don't know how to compute the amount_untaxed/amount_tax so don't compute that with such entries. 6) Fix cancelling an exchange difference entry / tax cash basis entry. When cancelling an exchange difference entry, the reconciliation is undo and then, the exchange difference entry is reversed. Same for tax cash basis entries. issue: https://github.com/odoo/odoo/issues/35841 7) Fix creation of cash basis entries for reversed journal entries. When reconciling entries with one being the reverse of the another one, cash basis entries for taxes must not be created. However, it must be the case when reconciling the reverse entry (like refunds) with something. issue: https://github.com/odoo/odoo/issues/35276 8) Fix matching rules ignoring the structured communication. Since account-pocalypse, the structured communication is landing on the name of the journal items. Then, the matching rules must consider it. closes odoo/odoo#36062 Signed-off-by: Laurent Smet --- addons/account/models/account_move.py | 112 ++++++++------ .../account/models/account_reconcile_model.py | 32 +++- .../account/tests/test_account_move_entry.py | 13 ++ .../tests/test_account_move_in_invoice.py | 139 +++++++++++++++++- .../tests/test_account_move_out_invoice.py | 131 ++++++++++++++++- 5 files changed, 373 insertions(+), 54 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 3b973b3f01c..2f5d405b224 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -170,6 +170,8 @@ class AccountMove(models.Model): states={'draft': [('readonly', False)]}, string='Salesperson', default=lambda self: self.env.user) + user_id = fields.Many2one(string='User', related='invoice_user_id', + help='Technical field used to fit the generic behavior in mail templates.') invoice_payment_state = fields.Selection(selection=[ ('not_paid', 'Not Paid'), ('in_payment', 'In Payment'), @@ -933,35 +935,43 @@ class AccountMove(models.Model): total_tax_currency = 0.0 total_residual = 0.0 total_residual_currency = 0.0 + total = 0.0 + total_currency = 0.0 currencies = set() for line in move.line_ids: if line.currency_id: currencies.add(line.currency_id) - # Untaxed amount. - if (move.is_invoice(include_receipts=True) and not line.exclude_from_invoice_tab)\ - or (move.type == 'entry' and line.debit and not line.tax_line_id): - total_untaxed += line.balance - total_untaxed_currency += line.amount_currency + if move.is_invoice(include_receipts=True): + # === Invoices === - # Tax amount. - if line.tax_line_id: - total_tax += line.balance - total_tax_currency += line.amount_currency + if not line.exclude_from_invoice_tab: + # Untaxed amount. + total_untaxed += line.balance + total_untaxed_currency += line.amount_currency + total += line.balance + total_currency += line.amount_currency + elif line.tax_line_id: + # Tax amount. + total_tax += line.balance + total_tax_currency += line.amount_currency + total += line.balance + total_currency += line.amount_currency + elif line.account_id.user_type_id.type in ('receivable', 'payable'): + # Residual amount. + total_residual += line.amount_residual + total_residual_currency += line.amount_residual_currency + else: + # === Miscellaneous journal entry === + if line.debit: + total += line.balance + total_currency += line.amount_currency - # Residual amount. - if move.type == 'entry' or line.account_id.user_type_id.type in ('receivable', 'payable'): - total_residual += line.amount_residual - total_residual_currency += line.amount_residual_currency - - total = total_untaxed + total_tax - total_currency = total_untaxed_currency + total_tax_currency - - if (move.type == 'entry' and total < 0.0) or move.is_inbound(): - sign = -1 - else: + if move.type == 'entry' or move.is_outbound(): sign = 1 + else: + sign = -1 move.amount_untaxed = sign * (total_untaxed_currency if len(currencies) == 1 else total_untaxed) move.amount_tax = sign * (total_tax_currency if len(currencies) == 1 else total_tax) @@ -1236,27 +1246,23 @@ class AccountMove(models.Model): if 'posted' in self.mapped('line_ids.payment_id.state'): raise ValidationError(_("You cannot modify a journal entry linked to a posted payment.")) - @api.constrains('name', 'journal_id') + @api.constrains('name', 'journal_id', 'state') def _check_unique_sequence_number(self): - if not self: + moves = self.filtered(lambda move: move.state == 'posted') + if not moves: return + # /!\ Computed stored fields are not yet inside the database. self._cr.execute(''' - WITH current_moves (id, name, state, company_id, journal_id, type) as ( values {} ) SELECT move2.id - FROM ( SELECT id, name, state, company_id, journal_id, type from account_move - UNION ALL SELECT id, name, state, company_id, journal_id, type from current_moves - ) move + FROM account_move move INNER JOIN account_move move2 ON move2.name = move.name - AND move2.company_id = move.company_id AND move2.journal_id = move.journal_id AND move2.type = move.type AND move2.id != move.id - WHERE move.id IN %s - AND move.state = 'posted' - AND move2.state = 'posted' - '''.format(", ".join(["(%s, %s, %s, %s, %s, %s)"] * len(self))), list(chain(*self.mapped(lambda r: [r.id, r.name, r.state, r.company_id.id, r.journal_id.id, r.type]))) + [tuple(self.ids)]) + WHERE move.id IN %s AND move2.state = 'posted' + ''', [tuple(moves.ids)]) res = self._cr.fetchone() if res: raise ValidationError(_('Posted journal entry must have an unique sequence number per company.')) @@ -1267,23 +1273,21 @@ class AccountMove(models.Model): if not moves: return + # /!\ Computed stored fields are not yet inside the database. self._cr.execute(''' - WITH current_moves (id, ref, company_id, commercial_partner_id, type, invoice_date) as ( values {} ) SELECT move2.id - FROM ( SELECT id, ref, company_id, commercial_partner_id, type, invoice_date from account_move - UNION ALL SELECT id, ref, company_id, commercial_partner_id, type, invoice_date from current_moves - ) move + FROM account_move move + JOIN account_journal journal ON journal.id = move.journal_id + JOIN res_partner partner ON partner.id = move.partner_id INNER JOIN account_move move2 ON move2.ref = move.ref - AND move2.company_id = move.company_id - AND move2.commercial_partner_id = move.commercial_partner_id + AND move2.company_id = journal.company_id + AND move2.commercial_partner_id = partner.commercial_partner_id AND move2.type = move.type - AND (move2.invoice_date = move.invoice_date OR move.invoice_date is NULL) + AND (move.invoice_date is NULL OR move2.invoice_date = move.invoice_date) AND move2.id != move.id WHERE move.id IN %s - AND move.type in ('in_invoice', 'in_refund') - AND move.ref IS NOT NULL - '''.format(", ".join(["(%s, %s, %s, %s, %s, CAST(%s as DATE))"] * len(self))), list(chain(*self.mapped(lambda r: [r.id, r.ref, r.company_id.id, r.commercial_partner_id.id, r.type, r.invoice_date or None]))) + [tuple(moves.ids)]) + ''', [tuple(moves.ids)]) if self._cr.fetchone(): raise ValidationError(_('Duplicated vendor reference detected. You probably encoded twice the same vendor bill/credit note.')) @@ -1860,7 +1864,16 @@ class AccountMove(models.Model): 'reversed_entry_id': move.id, }) move_vals_list.append(move._reverse_move_vals(default_values, cancel=cancel)) + reverse_moves = self.env['account.move'].create(move_vals_list) + for move, reverse_move in zip(self, reverse_moves.with_context(check_move_validity=False)): + # Update amount_currency if the date has changed. + if move.date != reverse_move.date: + for line in reverse_move.line_ids: + if line.currency_id: + line._onchange_currency() + reverse_move._recompute_dynamic_lines(recompute_all_taxes=False) + reverse_moves._check_balanced() # Reconcile moves together to cancel the previous one. if cancel: @@ -1980,6 +1993,10 @@ class AccountMove(models.Model): excluded_move_ids = AccountMoveLine.search(AccountMoveLine._get_suspense_moves_domain() + [('move_id', 'in', self.ids)]).mapped('move_id').ids for move in self: + if move in move.line_ids.mapped('full_reconcile_id.exchange_move_id'): + raise UserError(_('You cannot cancel an exchange difference journal entry.')) + if move.tax_cash_basis_rec_id: + raise UserError(_('You cannot cancel a tax cash basis journal entry.')) if not move.journal_id.update_posted and move.id not in excluded_move_ids: raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.')) # We remove all the analytics entries for this journal @@ -3211,7 +3228,11 @@ class AccountMoveLine(models.Model): for after_rec_dict in cash_basis_subjected: new_rec = part_rec.create(after_rec_dict) # if the pair belongs to move being reverted, do not create CABA entry - if cash_basis and not (new_rec.debit_move_id + new_rec.credit_move_id).mapped('move_id.reversed_entry_id'): + if cash_basis and not ( + new_rec.debit_move_id.move_id == new_rec.credit_move_id.move_id.reversed_entry_id + or + new_rec.credit_move_id.move_id == new_rec.debit_move_id.move_id.reversed_entry_id + ): new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec) self.recompute() @@ -3386,8 +3407,11 @@ class AccountMoveLine(models.Model): @api.multi def copy_data(self, default=None): res = super(AccountMoveLine, self).copy_data(default=default) - if self._context.get('include_business_fields'): - for line, values in zip(self, res): + for line, values in zip(self, res): + # Don't copy the name of a payment term line. + if line.move_id.is_invoice() and line.account_id.user_type_id.type in ('receivable', 'payable'): + values['name'] = '' + if self._context.get('include_business_fields'): line._copy_data_extend_business_fields(values) return res diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 108d7b75550..94e7407521c 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -433,12 +433,23 @@ class AccountReconcileModel(models.Model): aml.amount_currency AS aml_amount_currency, account.internal_type AS account_internal_type, - -- Determine a matching or not with the statement line communication using the move.name or move.ref. - regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+') - && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') + -- Determine a matching or not with the statement line communication using the aml.name, move.name or move.ref. + ( + aml.name IS NOT NULL + AND + TRIM(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g')) != '' + AND + regexp_split_to_array(TRIM(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g')),'\s+') + && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') + ) + OR + regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+') + && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') OR ( move.ref IS NOT NULL + AND + TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')) != '' AND regexp_split_to_array(TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')),'\s+') && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') @@ -482,11 +493,22 @@ class AccountReconcileModel(models.Model): TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')) != '' AND ( - regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+') - && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') + ( + aml.name IS NOT NULL + AND + TRIM(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g')) != '' + AND + regexp_split_to_array(TRIM(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g')),'\s+') + && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') + ) + OR + regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+') + && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') OR ( move.ref IS NOT NULL + AND + TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')) != '' AND regexp_split_to_array(TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')),'\s+') && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') diff --git a/addons/account/tests/test_account_move_entry.py b/addons/account/tests/test_account_move_entry.py index e831ab54878..82df20e3e0b 100644 --- a/addons/account/tests/test_account_move_entry.py +++ b/addons/account/tests/test_account_move_entry.py @@ -110,3 +110,16 @@ class TestAccountMove(InvoiceTestCommon): with self.assertRaises(UserError): self.test_move.post() + + def test_misc_unique_sequence_number(self): + ''' Ensure two journal entries can't share the same name when using the same sequence. ''' + self.test_move.post() + + # Edit the sequence to force the next move to get the same name. + self.test_move.journal_id\ + .sequence_id.date_range_ids\ + .filtered(lambda seq: seq.date_from == fields.Date.from_string('2016-01-01')).number_next -= 1 + + test_move2 = self.test_move.copy() + with self.assertRaises(ValidationError): + test_move2.post() diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py index 79f2353c9e2..18d4fedce46 100644 --- a/addons/account/tests/test_account_move_in_invoice.py +++ b/addons/account/tests/test_account_move_in_invoice.py @@ -3,6 +3,7 @@ from odoo.addons.account.tests.invoice_test_common import InvoiceTestCommon from odoo.tests.common import Form from odoo.tests import tagged from odoo import fields +from odoo.exceptions import ValidationError @tagged('post_install', '-at_install') @@ -851,7 +852,7 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon): self.term_line_vals_1, ], self.move_vals) - def test_in_invoice_create_draft_refund(self): + def test_in_invoice_create_refund(self): self.invoice.post() move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({ @@ -885,8 +886,10 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon): }, { **self.term_line_vals_1, + 'name': '', 'debit': 1128.0, 'credit': 0.0, + 'date_maturity': move_reversal.date, }, ], { **self.move_vals, @@ -898,7 +901,7 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon): move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({ 'date': fields.Date.from_string('2019-02-01'), - 'reason': 'because', + 'reason': 'no reason again', 'refund_method': 'cancel', }) reversal = move_reversal.reverse_moves() @@ -927,8 +930,10 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon): }, { **self.term_line_vals_1, + 'name': '', 'debit': 1128.0, 'credit': 0.0, + 'date_maturity': move_reversal.date, }, ], { **self.move_vals, @@ -938,6 +943,128 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon): 'invoice_payment_state': 'paid', }) + def test_in_invoice_create_refund_multi_currency(self): + ''' Test the account.move.reversal takes care about the currency rates when setting + a custom reversal date. + ''' + move_form = Form(self.invoice) + move_form.date = '2016-01-01' + move_form.currency_id = self.currency_data['currency'] + move_form.save() + + self.invoice.post() + + # The currency rate changed from 1/3 to 1/2. + move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({ + 'date': fields.Date.from_string('2017-01-01'), + 'reason': 'no reason', + 'refund_method': 'refund', + }) + reversal = move_reversal.reverse_moves() + reverse_move = self.env['account.move'].browse(reversal['res_id']) + + self.assertInvoiceValues(reverse_move, [ + { + **self.product_line_vals_1, + 'amount_currency': -800.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 400.0, + }, + { + **self.product_line_vals_2, + 'amount_currency': -160.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 80.0, + }, + { + **self.tax_line_vals_1, + 'amount_currency': -144.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 72.0, + }, + { + **self.tax_line_vals_2, + 'amount_currency': -24.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 12.0, + }, + { + **self.term_line_vals_1, + 'name': '', + 'amount_currency': 1128.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 564.0, + 'credit': 0.0, + 'date_maturity': move_reversal.date, + }, + ], { + **self.move_vals, + 'currency_id': self.currency_data['currency'].id, + 'date': move_reversal.date, + 'state': 'draft', + 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason), + 'invoice_payment_state': 'not_paid', + }) + + move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({ + 'date': fields.Date.from_string('2017-01-01'), + 'reason': 'no reason again', + 'refund_method': 'cancel', + }) + reversal = move_reversal.reverse_moves() + reverse_move = self.env['account.move'].browse(reversal['res_id']) + + self.assertInvoiceValues(reverse_move, [ + { + **self.product_line_vals_1, + 'amount_currency': -800.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 400.0, + }, + { + **self.product_line_vals_2, + 'amount_currency': -160.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 80.0, + }, + { + **self.tax_line_vals_1, + 'amount_currency': -144.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 72.0, + }, + { + **self.tax_line_vals_2, + 'amount_currency': -24.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 12.0, + }, + { + **self.term_line_vals_1, + 'name': '', + 'amount_currency': 1128.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 564.0, + 'credit': 0.0, + 'date_maturity': move_reversal.date, + }, + ], { + **self.move_vals, + 'currency_id': self.currency_data['currency'].id, + 'date': move_reversal.date, + 'state': 'posted', + 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason), + 'invoice_payment_state': 'paid', + }) + def test_in_invoice_create_1(self): # Test creating an account_move with the least information. move = self.env['account.move'].create({ @@ -1041,3 +1168,11 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon): **self.move_vals, 'currency_id': self.currency_data['currency'].id, }) + + def test_in_invoice_duplicate_supplier_reference(self): + ''' Ensure two vendor bills can't share the same vendor reference. ''' + self.invoice.ref = 'a supplier reference' + invoice2 = self.invoice.copy() + + with self.assertRaises(ValidationError): + invoice2.ref = 'a supplier reference' diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index 3c097924b31..5a5b87853ab 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -836,7 +836,7 @@ class TestAccountMoveOutInvoiceOnchanges(InvoiceTestCommon): self.assertRecordValues(invoice_copy, [{'name': 'INV/2019/0043'}]) - def test_out_invoice_create_draft_refund(self): + def test_out_invoice_create_refund(self): self.invoice.post() move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({ @@ -870,12 +870,14 @@ class TestAccountMoveOutInvoiceOnchanges(InvoiceTestCommon): }, { **self.term_line_vals_1, - 'name': self.invoice.name, + 'name': '', 'debit': 0.0, 'credit': 1410.0, + 'date_maturity': move_reversal.date, }, ], { **self.move_vals, + 'name': '/', 'date': move_reversal.date, 'state': 'draft', 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason), @@ -913,9 +915,10 @@ class TestAccountMoveOutInvoiceOnchanges(InvoiceTestCommon): }, { **self.term_line_vals_1, - 'name': self.invoice.name, + 'name': '', 'debit': 0.0, 'credit': 1410.0, + 'date_maturity': move_reversal.date, }, ], { **self.move_vals, @@ -925,6 +928,128 @@ class TestAccountMoveOutInvoiceOnchanges(InvoiceTestCommon): 'invoice_payment_state': 'paid', }) + def test_out_invoice_create_refund_multi_currency(self): + ''' Test the account.move.reversal takes care about the currency rates when setting + a custom reversal date. + ''' + move_form = Form(self.invoice) + move_form.date = '2016-01-01' + move_form.currency_id = self.currency_data['currency'] + move_form.save() + + self.invoice.post() + + # The currency rate changed from 1/3 to 1/2. + move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({ + 'date': fields.Date.from_string('2017-01-01'), + 'reason': 'no reason', + 'refund_method': 'refund', + }) + reversal = move_reversal.reverse_moves() + reverse_move = self.env['account.move'].browse(reversal['res_id']) + + self.assertInvoiceValues(reverse_move, [ + { + **self.product_line_vals_1, + 'amount_currency': 1000.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 500.0, + 'credit': 0.0, + }, + { + **self.product_line_vals_2, + 'amount_currency': 200.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 100.0, + 'credit': 0.0, + }, + { + **self.tax_line_vals_1, + 'amount_currency': 180.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 90.0, + 'credit': 0.0, + }, + { + **self.tax_line_vals_2, + 'amount_currency': 30.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 15.0, + 'credit': 0.0, + }, + { + **self.term_line_vals_1, + 'name': '', + 'amount_currency': -1410.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 705.0, + 'date_maturity': move_reversal.date, + }, + ], { + **self.move_vals, + 'currency_id': self.currency_data['currency'].id, + 'date': move_reversal.date, + 'state': 'draft', + 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason), + 'invoice_payment_state': 'not_paid', + }) + + move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({ + 'date': fields.Date.from_string('2017-01-01'), + 'reason': 'no reason', + 'refund_method': 'cancel', + }) + reversal = move_reversal.reverse_moves() + reverse_move = self.env['account.move'].browse(reversal['res_id']) + + self.assertInvoiceValues(reverse_move, [ + { + **self.product_line_vals_1, + 'amount_currency': 1000.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 500.0, + 'credit': 0.0, + }, + { + **self.product_line_vals_2, + 'amount_currency': 200.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 100.0, + 'credit': 0.0, + }, + { + **self.tax_line_vals_1, + 'amount_currency': 180.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 90.0, + 'credit': 0.0, + }, + { + **self.tax_line_vals_2, + 'amount_currency': 30.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 15.0, + 'credit': 0.0, + }, + { + **self.term_line_vals_1, + 'name': '', + 'amount_currency': -1410.0, + 'currency_id': self.currency_data['currency'].id, + 'debit': 0.0, + 'credit': 705.0, + 'date_maturity': move_reversal.date, + }, + ], { + **self.move_vals, + 'currency_id': self.currency_data['currency'].id, + 'date': move_reversal.date, + 'state': 'posted', + 'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason), + 'invoice_payment_state': 'paid', + }) + def test_out_invoice_create_1(self): # Test creating an account_move with the least information. move = self.env['account.move'].create({