From 5c09a8d9f7df6d9790054a2be66ce8e9d3697ff9 Mon Sep 17 00:00:00 2001 From: "Audric Onockx (auon)" Date: Fri, 17 Sep 2021 14:01:24 +0000 Subject: [PATCH] [FIX] purchase : Account analytic default changed by user is reset in invoice Reproduce : 1. Add a default analytical rule: if Coin Gourmand partner then Administrative analytical account 2. Create a PO at the Coin Gourmand supplier, add a product, and select the Internal analytical account. 3. Bill for this product. Result : Internal analytical account has disappeared from the invoice line. It has been replaced by Administrative. Issue : AccountMove.create() computes an account in the move and this causes the analytic account to be reset at its default value from the rule. Fix : Since the account pocalypse in 13.0, account.move is making magic things during the creation of the invoice to create dynamically the invoice lines like account.invoice did before this huge refactoring. The create is making a 'New' record to simulate the onchange but this code is a hack triggering unexpected recomputation like the analytic account. To avoid that, we ensure to assign the minimum number of fields to preserve fields like the analytic account. closes odoo/odoo#79321 X-original-commit: 876e2d1073312fe65cb6b2e3038235aeaa082c8a Related: odoo/enterprise#22074 Signed-off-by: Laurent Smet Signed-off-by: Josse Colpaert --- addons/account/models/account_move.py | 20 +++-- .../tests/test_account_move_in_invoice.py | 74 +++++++++++---- .../tests/test_account_move_in_refund.py | 36 ++++++-- .../tests/test_account_move_out_invoice.py | 90 ++++++++++++++----- .../tests/test_account_move_out_refund.py | 36 ++++++-- .../purchase/tests/test_purchase_invoice.py | 28 ++++++ 6 files changed, 224 insertions(+), 60 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 190deeedc53..a9f35b6a604 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -2074,10 +2074,10 @@ class AccountMove(models.Model): ''' self.ensure_one() - for line in self.line_ids: - # Do something only on invoice lines. - if line.exclude_from_invoice_tab: - continue + invoice_lines = self.line_ids.filtered(lambda x: not x.exclude_from_invoice_tab and not x.display_type and not x._origin) + cached_vals_list = [dict(x._cache) for x in invoice_lines] + + for cached_vals, line in zip(cached_vals_list, invoice_lines): # Shortcut to load the demo data. # Doing line.account_id triggers a default_get(['account_id']) that could returns a result. @@ -2096,10 +2096,15 @@ class AccountMove(models.Model): # Ensure related fields are well copied. if line.partner_id != self.partner_id.commercial_partner_id: line.partner_id = self.partner_id.commercial_partner_id + line.date = self.date line.recompute_tax_line = True line.currency_id = self.currency_id + for k, v in cached_vals.items(): + field = line._fields[k] + if field.compute and not field.readonly: + line[k] = v self.line_ids._onchange_price_subtotal() self._recompute_dynamic_lines(recompute_all_taxes=True) @@ -2174,7 +2179,12 @@ class AccountMove(models.Model): vals['line_ids'] = vals.pop('invoice_line_ids') for invoice in self: - invoice_new = invoice.with_context(default_move_type=invoice.move_type, default_journal_id=invoice.journal_id.id).new(origin=invoice) + invoice_new = invoice.with_context( + default_move_type=invoice.move_type, + default_journal_id=invoice.journal_id.id, + default_partner_id=invoice.partner_id.id, + default_currency_id=invoice.currency_id.id, + ).new(origin=invoice) invoice_new.update(vals) values = invoice_new._move_autocomplete_invoice_lines_values() values.pop('invoice_line_ids', None) diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py index a7d1da55164..a624c44c7d3 100644 --- a/addons/account/tests/test_account_move_in_invoice.py +++ b/addons/account/tests/test_account_move_in_invoice.py @@ -4,7 +4,7 @@ from freezegun import freeze_time from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests.common import Form from odoo.tests import tagged -from odoo import fields +from odoo import fields, Command from odoo.exceptions import UserError, ValidationError @@ -1396,9 +1396,19 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertInvoiceValues(move, [ @@ -1446,13 +1456,23 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + ], }) move.write({ 'invoice_line_ids': [ - (0, None, self.product_line_vals_2), - ] + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertInvoiceValues(move, [ @@ -1508,9 +1528,19 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) move.action_switch_invoice_into_refund_credit_note() @@ -1558,10 +1588,6 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): def test_in_invoice_switch_in_refund_2(self): # Test creating an account_move with an in_invoice_type and switch it in an in_refund and a negative quantity. - modified_product_line_vals_1 = self.product_line_vals_1.copy() - modified_product_line_vals_1.update({'quantity': -modified_product_line_vals_1['quantity']}) - modified_product_line_vals_2 = self.product_line_vals_2.copy() - modified_product_line_vals_2.update({'quantity': -modified_product_line_vals_2['quantity']}) move = self.env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': self.partner_a.id, @@ -1569,9 +1595,21 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): 'currency_id': self.currency_data['currency'].id, 'invoice_payment_term_id': self.pay_terms_a.id, 'invoice_line_ids': [ - (0, None, modified_product_line_vals_1), - (0, None, modified_product_line_vals_2), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'quantity': -self.product_line_vals_1['quantity'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'quantity': -self.product_line_vals_2['quantity'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertInvoiceValues(move, [ diff --git a/addons/account/tests/test_account_move_in_refund.py b/addons/account/tests/test_account_move_in_refund.py index 5897c5e32b1..54a498bc412 100644 --- a/addons/account/tests/test_account_move_in_refund.py +++ b/addons/account/tests/test_account_move_in_refund.py @@ -3,7 +3,7 @@ from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests.common import Form from odoo.tests import tagged -from odoo import fields +from odoo import fields, Command @tagged('post_install', '-at_install') @@ -844,9 +844,19 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertInvoiceValues(move, [ @@ -894,13 +904,23 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + ], }) move.write({ 'invoice_line_ids': [ - (0, None, self.product_line_vals_2), - ] + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertInvoiceValues(move, [ diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index 6eddf8055ef..7af8ef5af2e 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -3,7 +3,7 @@ from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests.common import Form from odoo.tests import tagged -from odoo import fields +from odoo import fields, Command from odoo.exceptions import UserError from unittest.mock import patch @@ -2066,9 +2066,19 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertEqual(partner_a_child.id, move.partner_id.id, 'Keep child partner on the account move record') @@ -2083,13 +2093,23 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + ], }) move.write({ 'invoice_line_ids': [ - (0, None, self.product_line_vals_2), - ] + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertInvoiceValues(move, [ @@ -2168,9 +2188,19 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) # Remove the invoice_date to check: @@ -2359,9 +2389,19 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) move.action_switch_invoice_into_refund_credit_note() @@ -2409,10 +2449,6 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): def test_out_invoice_switch_out_refund_2(self): # Test creating an account_move with an out_invoice_type and switch it in an out_refund and a negative quantity. - modified_product_line_vals_1 = self.product_line_vals_1.copy() - modified_product_line_vals_1.update({'quantity': -modified_product_line_vals_1['quantity']}) - modified_product_line_vals_2 = self.product_line_vals_2.copy() - modified_product_line_vals_2.update({'quantity': -modified_product_line_vals_2['quantity']}) move = self.env['account.move'].create({ 'move_type': 'out_invoice', 'partner_id': self.partner_a.id, @@ -2420,9 +2456,21 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): 'currency_id': self.currency_data['currency'].id, 'invoice_payment_term_id': self.pay_terms_a.id, 'invoice_line_ids': [ - (0, None, modified_product_line_vals_1), - (0, None, modified_product_line_vals_2), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'quantity': -self.product_line_vals_1['quantity'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'quantity': -self.product_line_vals_2['quantity'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertInvoiceValues(move, [ diff --git a/addons/account/tests/test_account_move_out_refund.py b/addons/account/tests/test_account_move_out_refund.py index 7c9aaa3eba5..29a22700428 100644 --- a/addons/account/tests/test_account_move_out_refund.py +++ b/addons/account/tests/test_account_move_out_refund.py @@ -3,7 +3,7 @@ from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests.common import Form from odoo.tests import tagged -from odoo import fields +from odoo import fields, Command @tagged('post_install', '-at_install') @@ -827,9 +827,19 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertInvoiceValues(move, [ @@ -877,13 +887,23 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon): '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), - ] + Command.create({ + 'product_id': self.product_line_vals_1['product_id'], + 'product_uom_id': self.product_line_vals_1['product_uom_id'], + 'price_unit': self.product_line_vals_1['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_1['tax_ids'])], + }), + ], }) move.write({ 'invoice_line_ids': [ - (0, None, self.product_line_vals_2), - ] + Command.create({ + 'product_id': self.product_line_vals_2['product_id'], + 'product_uom_id': self.product_line_vals_2['product_uom_id'], + 'price_unit': self.product_line_vals_2['price_unit'], + 'tax_ids': [Command.set(self.product_line_vals_2['tax_ids'])], + }), + ], }) self.assertInvoiceValues(move, [ diff --git a/addons/purchase/tests/test_purchase_invoice.py b/addons/purchase/tests/test_purchase_invoice.py index 49fbf70fbb1..e3fbb8ccd39 100644 --- a/addons/purchase/tests/test_purchase_invoice.py +++ b/addons/purchase/tests/test_purchase_invoice.py @@ -299,3 +299,31 @@ class TestPurchaseToInvoice(AccountTestInvoicingCommon): move = move_form.save() self.assertEqual(move.amount_total, 0.01) + + def test_vendor_bill_analytic_account_default_change(self): + """ Tests whether, when an analytic account rule is set, and user changes manually the analytic account on + the po, it is the same that is mentioned in the bill. + """ + analytic_account_default = self.env['account.analytic.account'].create({'name': 'default'}) + analytic_account_manual = self.env['account.analytic.account'].create({'name': 'manual'}) + + self.env['account.analytic.default'].create({ + 'analytic_id': analytic_account_default.id, + 'product_id': self.product_order.id, + }) + + po_form = Form(self.env['purchase.order'].with_context(tracking_disable=True)) + po_form.partner_id = self.partner_a + with po_form.order_line.new() as po_line_form: + po_line_form.name = self.product_order.name + po_line_form.product_id = self.product_order + po_line_form.product_qty = 1.0 + po_line_form.price_unit = 10 + po_line_form.account_analytic_id = analytic_account_manual + + purchase_order = po_form.save() + purchase_order.button_confirm() + purchase_order.action_create_invoice() + + aml = self.env['account.move.line'].search([('purchase_line_id', '=', purchase_order.order_line.id)]) + self.assertRecordValues(aml, [{'analytic_account_id': analytic_account_manual.id}])