From 727fd4f2088f39d4a7e3deb3e5df34a3d6e1bf4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nshimiyimana=20S=C3=A9na?= Date: Mon, 1 Aug 2022 09:47:19 +0000 Subject: [PATCH] =?UTF-8?q?[FIX]=20sale,=20purchase:=20fix=20invoicing=20i?= =?UTF-8?q?nteraction=20with=20=E2=80=9CInvoicing=20Switch=20Threshold?= =?UTF-8?q?=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps to reproduce: - Create a sale order with a product that's invoiced on delivered quantities. - Set delivered quantity (partial delivery) and create an invoice based on the delivered quantity. Set the "Invoice Date" to sometime in the past. - Go to the settings and set "Invoicing Switch Threshold" to any date in the future (so that the invoice you created has a date BEFORE the new threshold) - The invoice will get the label "invoicing app legacy". - Go back to the sales order and change the delivered quantity. You should see that the invoiced quantity is automatically set to 0. A similar behavior can be observed with purchase orders. Why this is happening: When a new “Invoicing Switch Threshold” is set, all posted invoices before the threshold are marked as `canceled`. In v15, changing the delivered quantities triggers the invoiced quantities to be recalculated as well. However, computing invoiced quantities doesn't take into account invoices that are marked as `canceled`. This mean that the newly computed invoiced quantities won't include invoices posted before threshold. opw-2896797 closes odoo/odoo#98909 X-original-commit: cc979734bd7ef3f2a94b411cb7b4f3e782b7ddab Signed-off-by: William André (wan) Signed-off-by: Nshimiyimana Serge Séna (sesn) --- addons/purchase/models/purchase.py | 2 +- .../purchase/tests/test_purchase_invoice.py | 44 +++++++++++++++++++ addons/sale/models/sale_order_line.py | 2 +- addons/sale/tests/test_sale_to_invoice.py | 39 ++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 7071d9f1b00..21238bfe978 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -1018,7 +1018,7 @@ class PurchaseOrderLine(models.Model): # compute qty_invoiced qty = 0.0 for inv_line in line._get_invoice_lines(): - if inv_line.move_id.state not in ['cancel']: + if inv_line.move_id.state not in ['cancel'] or inv_line.move_id.payment_state == 'invoicing_legacy': if inv_line.move_id.move_type == 'in_invoice': qty += inv_line.product_uom_id._compute_quantity(inv_line.quantity, line.product_uom) elif inv_line.move_id.move_type == 'in_refund': diff --git a/addons/purchase/tests/test_purchase_invoice.py b/addons/purchase/tests/test_purchase_invoice.py index f9ddac9b0b5..c28901d111a 100644 --- a/addons/purchase/tests/test_purchase_invoice.py +++ b/addons/purchase/tests/test_purchase_invoice.py @@ -3,6 +3,7 @@ from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests import tagged from odoo.tests.common import Form +from odoo import Command, fields @tagged('post_install', '-at_install') @@ -413,3 +414,46 @@ class TestPurchaseToInvoice(AccountTestInvoicingCommon): ] for line in invoice.invoice_line_ids.sorted('sequence'): self.assertEqual(line.purchase_order_id, expected_purchase.pop(0)) + + def test_partial_billing_interaction_with_invoicing_switch_threshold(self): + """ Let's say you create a partial bill 'B' for a given PO. Now if you change the + 'Invoicing Switch Threshold' such that the bill date of 'B' is before the new threshold, + the PO should still take bill 'B' into account. + """ + if not self.env['ir.module.module'].search([('name', '=', 'account_accountant'), ('state', '=', 'installed')]): + self.skipTest("This test requires the installation of the account_account module") + + purchase_order = self.env['purchase.order'].with_context(tracking_disable=True).create({ + 'partner_id': self.partner_a.id, + 'order_line': [ + Command.create({ + 'name': self.product_deliver.name, + 'product_id': self.product_deliver.id, + 'product_qty': 20.0, + 'product_uom': self.product_deliver.uom_id.id, + 'price_unit': self.product_deliver.list_price, + 'taxes_id': False, + }), + ], + }) + line = purchase_order.order_line[0] + + purchase_order.button_confirm() + line.qty_received = 10 + purchase_order.action_create_invoice() + + invoice = purchase_order.invoice_ids + invoice.invoice_date = invoice.date + invoice.action_post() + + self.assertEqual(line.qty_invoiced, 10) + + self.env['res.config.settings'].create({ + 'invoicing_switch_threshold': fields.Date.add(invoice.invoice_date, days=30), + }).execute() + + invoice.invalidate_model(fnames=['payment_state']) + + self.assertEqual(line.qty_invoiced, 10) + line.qty_received = 15 + self.assertEqual(line.qty_invoiced, 10) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 7098a532b00..48f7c46640e 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -704,7 +704,7 @@ class SaleOrderLine(models.Model): for line in self: qty_invoiced = 0.0 for invoice_line in line._get_invoice_lines(): - if invoice_line.move_id.state != 'cancel': + if invoice_line.move_id.state != 'cancel' or invoice_line.move_id.payment_state == 'invoicing_legacy': if invoice_line.move_id.move_type == 'out_invoice': qty_invoiced += invoice_line.product_uom_id._compute_quantity(invoice_line.quantity, line.product_uom) elif invoice_line.move_id.move_type == 'out_refund': diff --git a/addons/sale/tests/test_sale_to_invoice.py b/addons/sale/tests/test_sale_to_invoice.py index 0c3caaa4085..7072ef85587 100644 --- a/addons/sale/tests/test_sale_to_invoice.py +++ b/addons/sale/tests/test_sale_to_invoice.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import fields from odoo.fields import Command from odoo.tests import Form, tagged from odoo.tools import float_is_zero @@ -706,3 +707,41 @@ class TestSaleToInvoice(TestSaleCommon): inv.company_id, self.company_data['company'], 'invoices should be created in the company of the SO, not the main company of the context') + + def test_partial_invoicing_interaction_with_invoicing_switch_threshold(self): + """ Let's say you partially invoice a SO, let's call the resuling invoice 'A'. Now if you change the + 'Invoicing Switch Threshold' such that the invoice date of 'A' is before the new threshold, + the SO should still take invoice 'A' into account. + """ + if not self.env['ir.module.module'].search([('name', '=', 'account_accountant'), ('state', '=', 'installed')]): + self.skipTest("This test requires the installation of the account_account module") + + sale_order = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'order_line': [ + Command.create({ + 'product_id': self.company_data['product_delivery_no'].id, + 'product_uom_qty': 20, + }), + ], + }) + line = sale_order.order_line[0] + + sale_order.action_confirm() + + line.qty_delivered = 10 + + invoice = sale_order._create_invoices() + invoice.action_post() + + self.assertEqual(line.qty_invoiced, 10) + + self.env['res.config.settings'].create({ + 'invoicing_switch_threshold': fields.Date.add(invoice.invoice_date, days=30), + }).execute() + + invoice.invalidate_model(fnames=['payment_state']) + + self.assertEqual(line.qty_invoiced, 10) + line.qty_delivered = 15 + self.assertEqual(line.qty_invoiced, 10)