From 508ca5fc754a8862ed8f318ab4c253510179399a Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Thu, 2 Dec 2021 18:54:01 +0100 Subject: [PATCH 01/12] Create module account_invoice_section_picking --- account_invoice_section_picking/__init__.py | 1 + .../__manifest__.py | 13 +++++ .../models/__init__.py | 3 ++ .../models/account_move_line.py | 15 ++++++ .../models/res_company.py | 11 ++++ .../models/stock_picking.py | 18 +++++++ .../tests/__init__.py | 1 + .../test_account_invoice_section_picking.py | 53 +++++++++++++++++++ 8 files changed, 115 insertions(+) create mode 100644 account_invoice_section_picking/__init__.py create mode 100644 account_invoice_section_picking/__manifest__.py create mode 100644 account_invoice_section_picking/models/__init__.py create mode 100644 account_invoice_section_picking/models/account_move_line.py create mode 100644 account_invoice_section_picking/models/res_company.py create mode 100644 account_invoice_section_picking/models/stock_picking.py create mode 100644 account_invoice_section_picking/tests/__init__.py create mode 100644 account_invoice_section_picking/tests/test_account_invoice_section_picking.py diff --git a/account_invoice_section_picking/__init__.py b/account_invoice_section_picking/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/account_invoice_section_picking/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_invoice_section_picking/__manifest__.py b/account_invoice_section_picking/__manifest__.py new file mode 100644 index 00000000000..d30aedc406d --- /dev/null +++ b/account_invoice_section_picking/__manifest__.py @@ -0,0 +1,13 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Acccount Invoice Section Picking", + "version": "14.0.1.0.1", + "summary": "Extension of Acccount Invoice Section Sale Order to allow " + "grouping of invoice lines according to delivery picking.", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-invoicing", + "license": "AGPL-3", + "category": "Accounting & Finance", + "depends": ["account_invoice_section_sale_order", "stock"], +} diff --git a/account_invoice_section_picking/models/__init__.py b/account_invoice_section_picking/models/__init__.py new file mode 100644 index 00000000000..5eff3afaeb5 --- /dev/null +++ b/account_invoice_section_picking/models/__init__.py @@ -0,0 +1,3 @@ +from . import account_move_line +from . import res_company +from . import stock_picking diff --git a/account_invoice_section_picking/models/account_move_line.py b/account_invoice_section_picking/models/account_move_line.py new file mode 100644 index 00000000000..62a45947f61 --- /dev/null +++ b/account_invoice_section_picking/models/account_move_line.py @@ -0,0 +1,15 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import api, fields, models + + +class AccountMoveLine(models.Model): + + _inherit = "account.move.line" + + @api.model + def _get_section_grouping(self): + invoice_section_grouping = self.env.company.invoice_section_grouping + if invoice_section_grouping == "delivery_picking": + return "sale_line_ids.move_ids.picking_id" + return super()._get_section_grouping() diff --git a/account_invoice_section_picking/models/res_company.py b/account_invoice_section_picking/models/res_company.py new file mode 100644 index 00000000000..74f1bcb8e67 --- /dev/null +++ b/account_invoice_section_picking/models/res_company.py @@ -0,0 +1,11 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + invoice_section_grouping = fields.Selection( + selection_add=[("delivery_picking", "Group by delivery picking")], + ) diff --git a/account_invoice_section_picking/models/stock_picking.py b/account_invoice_section_picking/models/stock_picking.py new file mode 100644 index 00000000000..e54ee0ca04b --- /dev/null +++ b/account_invoice_section_picking/models/stock_picking.py @@ -0,0 +1,18 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import models +from odoo.tools.safe_eval import safe_eval, time + + +class StockPicking(models.Model): + + _inherit = "stock.picking" + + def _get_invoice_section_name(self): + """Returns the text for the section name.""" + self.ensure_one() + naming_scheme = self.partner_id.invoice_section_name_scheme or self.company_id.invoice_section_name_scheme + if naming_scheme: + return safe_eval(naming_scheme, {'object': self, 'time': time}) + else: + return self.name diff --git a/account_invoice_section_picking/tests/__init__.py b/account_invoice_section_picking/tests/__init__.py new file mode 100644 index 00000000000..c09ba92697e --- /dev/null +++ b/account_invoice_section_picking/tests/__init__.py @@ -0,0 +1 @@ +from . import test_account_invoice_section_picking diff --git a/account_invoice_section_picking/tests/test_account_invoice_section_picking.py b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py new file mode 100644 index 00000000000..4d8fea1e6bc --- /dev/null +++ b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py @@ -0,0 +1,53 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.tests.common import Form, SavepointCase + + +class TestAccountInvoiceSectionPicking(SavepointCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env.company.invoice_section_grouping = "delivery_picking" + cls.partner_1 = cls.env.ref("base.res_partner_1") + cls.product_1 = cls.env.ref("product.product_delivery_01") + cls.product_1.invoice_policy = "order" + cls.order_1 = cls._create_order() + cls.order_2 = cls._create_order(order_line_name=cls.product_1.name) + cls.order_1.action_confirm() + cls.order_2.action_confirm() + + @classmethod + def _create_order(cls, order_line_name=None): + order_form = Form(cls.env["sale.order"]) + order_form.partner_id = cls.partner_1 + with order_form: + with order_form.order_line.new() as line_form: + line_form.product_id = cls.product_1 + if order_line_name is not None: + line_form.name = order_line_name + return order_form.save() + + def test_group_by_delivery_picking(self): + invoice = (self.order_1 + self.order_2)._create_invoices() + self.assertEqual(len(invoice), 1) + result = [ + self.order_1.picking_ids.name, + self.order_1.order_line.name, + self.order_2.picking_ids.name, + self.order_2.order_line.name, + ] + for cnt, invoice_line in enumerate( + invoice.line_ids.filtered( + lambda l: not l.exclude_from_invoice_tab + ).sorted("sequence") + ): + self.assertEqual(invoice_line.name, result[cnt]) + + # TODO: add test with warehouse using multiple delivery steps to ensure + # it's the delivery picking name that is printed + + # TODO: add test with product using delivered quantities after creation of + # a backorder + # - Handle possible issue with sale order line having part of its qty on + # first delivery and other part in backorder(s) From 870c17151f861b81c5172721235d98ebeb2e43a0 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Fri, 3 Dec 2021 13:21:11 +0100 Subject: [PATCH 02/12] Add readme parts --- account_invoice_section_picking/readme/CONTRIBUTORS.rst | 1 + account_invoice_section_picking/readme/DESCRIPTION.rst | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 account_invoice_section_picking/readme/CONTRIBUTORS.rst create mode 100644 account_invoice_section_picking/readme/DESCRIPTION.rst diff --git a/account_invoice_section_picking/readme/CONTRIBUTORS.rst b/account_invoice_section_picking/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..e31e2f0c4fc --- /dev/null +++ b/account_invoice_section_picking/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Akim Juillerat diff --git a/account_invoice_section_picking/readme/DESCRIPTION.rst b/account_invoice_section_picking/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..b01c3ae2173 --- /dev/null +++ b/account_invoice_section_picking/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module extends `account_invoice_section_sale_order` to allow using pickings +linked to the sale order line for the grouping invoice lines when invoicing sale +orders. From 9301f4881ed8e9c429a1171cae540929b0c43d38 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Fri, 3 Dec 2021 13:21:55 +0100 Subject: [PATCH 03/12] [IMP] pre-commit --- account_invoice_section_picking/__manifest__.py | 2 +- .../models/account_move_line.py | 2 +- account_invoice_section_picking/models/stock_picking.py | 7 +++++-- .../tests/test_account_invoice_section_picking.py | 7 +++---- .../odoo/addons/account_invoice_section_picking | 1 + setup/account_invoice_section_picking/setup.py | 6 ++++++ 6 files changed, 17 insertions(+), 8 deletions(-) create mode 120000 setup/account_invoice_section_picking/odoo/addons/account_invoice_section_picking create mode 100644 setup/account_invoice_section_picking/setup.py diff --git a/account_invoice_section_picking/__manifest__.py b/account_invoice_section_picking/__manifest__.py index d30aedc406d..14820730061 100644 --- a/account_invoice_section_picking/__manifest__.py +++ b/account_invoice_section_picking/__manifest__.py @@ -4,7 +4,7 @@ "name": "Acccount Invoice Section Picking", "version": "14.0.1.0.1", "summary": "Extension of Acccount Invoice Section Sale Order to allow " - "grouping of invoice lines according to delivery picking.", + "grouping of invoice lines according to delivery picking.", "author": "Camptocamp, Odoo Community Association (OCA)", "website": "https://github.com/OCA/account-invoicing", "license": "AGPL-3", diff --git a/account_invoice_section_picking/models/account_move_line.py b/account_invoice_section_picking/models/account_move_line.py index 62a45947f61..6f12862a47f 100644 --- a/account_invoice_section_picking/models/account_move_line.py +++ b/account_invoice_section_picking/models/account_move_line.py @@ -1,6 +1,6 @@ # Copyright 2021 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import api, fields, models +from odoo import api, models class AccountMoveLine(models.Model): diff --git a/account_invoice_section_picking/models/stock_picking.py b/account_invoice_section_picking/models/stock_picking.py index e54ee0ca04b..b28ac15c80c 100644 --- a/account_invoice_section_picking/models/stock_picking.py +++ b/account_invoice_section_picking/models/stock_picking.py @@ -11,8 +11,11 @@ class StockPicking(models.Model): def _get_invoice_section_name(self): """Returns the text for the section name.""" self.ensure_one() - naming_scheme = self.partner_id.invoice_section_name_scheme or self.company_id.invoice_section_name_scheme + naming_scheme = ( + self.partner_id.invoice_section_name_scheme + or self.company_id.invoice_section_name_scheme + ) if naming_scheme: - return safe_eval(naming_scheme, {'object': self, 'time': time}) + return safe_eval(naming_scheme, {"object": self, "time": time}) else: return self.name diff --git a/account_invoice_section_picking/tests/test_account_invoice_section_picking.py b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py index 4d8fea1e6bc..68aae76b23c 100644 --- a/account_invoice_section_picking/tests/test_account_invoice_section_picking.py +++ b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py @@ -4,7 +4,6 @@ class TestAccountInvoiceSectionPicking(SavepointCase): - @classmethod def setUpClass(cls): super().setUpClass() @@ -38,9 +37,9 @@ def test_group_by_delivery_picking(self): self.order_2.order_line.name, ] for cnt, invoice_line in enumerate( - invoice.line_ids.filtered( - lambda l: not l.exclude_from_invoice_tab - ).sorted("sequence") + invoice.line_ids.filtered(lambda l: not l.exclude_from_invoice_tab).sorted( + "sequence" + ) ): self.assertEqual(invoice_line.name, result[cnt]) diff --git a/setup/account_invoice_section_picking/odoo/addons/account_invoice_section_picking b/setup/account_invoice_section_picking/odoo/addons/account_invoice_section_picking new file mode 120000 index 00000000000..6a203601614 --- /dev/null +++ b/setup/account_invoice_section_picking/odoo/addons/account_invoice_section_picking @@ -0,0 +1 @@ +../../../../account_invoice_section_picking \ No newline at end of file diff --git a/setup/account_invoice_section_picking/setup.py b/setup/account_invoice_section_picking/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_invoice_section_picking/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 3fd40e1a374d32385ddbb8e51e5af6004e225b54 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Fri, 3 Dec 2021 13:24:36 +0100 Subject: [PATCH 04/12] Improve test --- .../test_account_invoice_section_picking.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/account_invoice_section_picking/tests/test_account_invoice_section_picking.py b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py index 68aae76b23c..4582965976e 100644 --- a/account_invoice_section_picking/tests/test_account_invoice_section_picking.py +++ b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py @@ -30,18 +30,17 @@ def _create_order(cls, order_line_name=None): def test_group_by_delivery_picking(self): invoice = (self.order_1 + self.order_2)._create_invoices() self.assertEqual(len(invoice), 1) - result = [ - self.order_1.picking_ids.name, - self.order_1.order_line.name, - self.order_2.picking_ids.name, - self.order_2.order_line.name, - ] - for cnt, invoice_line in enumerate( - invoice.line_ids.filtered(lambda l: not l.exclude_from_invoice_tab).sorted( - "sequence" - ) - ): - self.assertEqual(invoice_line.name, result[cnt]) + result = { + 10: (self.order_1.picking_ids.name, "line_section"), + 20: (self.order_1.order_line.name, False), + 30: (self.order_2.picking_ids.name, "line_section"), + 40: (self.order_2.order_line.name, False), + } + for line in invoice.line_ids.filtered( + lambda l: not l.exclude_from_invoice_tab + ).sorted("sequence"): + self.assertEqual(line.name, result[line.sequence][0]) + self.assertEqual(line.display_type, result[line.sequence][1]) # TODO: add test with warehouse using multiple delivery steps to ensure # it's the delivery picking name that is printed From e92d0850c1409edf2b5c9032104cc192e31b76dc Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Fri, 10 Dec 2021 16:43:01 +0100 Subject: [PATCH 05/12] Handle delivered quantities with backorders --- .../models/account_move_line.py | 31 ++++ .../models/res_company.py | 1 + .../models/stock_picking.py | 22 ++- .../tests/common.py | 29 +++ .../test_account_invoice_section_picking.py | 72 ++++++-- ...count_invoice_section_picking_backorder.py | 168 ++++++++++++++++++ 6 files changed, 301 insertions(+), 22 deletions(-) create mode 100644 account_invoice_section_picking/tests/common.py create mode 100644 account_invoice_section_picking/tests/test_account_invoice_section_picking_backorder.py diff --git a/account_invoice_section_picking/models/account_move_line.py b/account_invoice_section_picking/models/account_move_line.py index 6f12862a47f..6171cd145d6 100644 --- a/account_invoice_section_picking/models/account_move_line.py +++ b/account_invoice_section_picking/models/account_move_line.py @@ -7,6 +7,37 @@ class AccountMoveLine(models.Model): _inherit = "account.move.line" + def _get_section_group(self): + group = super()._get_section_group() + # If product is invoiced by delivered quantities: + # - filter on done pickings to avoid displaying backorders + # - Remove pickings linked with same sale order lines if an invoice + # was created after its date_done + invoice_section_grouping = self.env.company.invoice_section_grouping + if ( + invoice_section_grouping == "delivery_picking" + and self.product_id.invoice_policy == "delivery" + ): + last_invoice_line = self.search( + [ + ("sale_line_ids", "in", self.sale_line_ids.ids), + ("id", "not in", self.ids), + ], + order="create_date", + limit=1, + ) + pickings_already_invoiced = self.env["stock.picking"].search( + [ + ("date_done", "<=", last_invoice_line.create_date), + ("id", "in", self.sale_line_ids.mapped("order_id.picking_ids").ids), + ] + ) + group = group.filtered( + lambda p: p.state == "done" + and p.id not in pickings_already_invoiced.ids + ) + return group + @api.model def _get_section_grouping(self): invoice_section_grouping = self.env.company.invoice_section_grouping diff --git a/account_invoice_section_picking/models/res_company.py b/account_invoice_section_picking/models/res_company.py index 74f1bcb8e67..5e053971029 100644 --- a/account_invoice_section_picking/models/res_company.py +++ b/account_invoice_section_picking/models/res_company.py @@ -8,4 +8,5 @@ class ResCompany(models.Model): invoice_section_grouping = fields.Selection( selection_add=[("delivery_picking", "Group by delivery picking")], + ondelete={"delivery_picking": "set default"}, ) diff --git a/account_invoice_section_picking/models/stock_picking.py b/account_invoice_section_picking/models/stock_picking.py index b28ac15c80c..7dde2a9b623 100644 --- a/account_invoice_section_picking/models/stock_picking.py +++ b/account_invoice_section_picking/models/stock_picking.py @@ -10,12 +10,16 @@ class StockPicking(models.Model): def _get_invoice_section_name(self): """Returns the text for the section name.""" - self.ensure_one() - naming_scheme = ( - self.partner_id.invoice_section_name_scheme - or self.company_id.invoice_section_name_scheme - ) - if naming_scheme: - return safe_eval(naming_scheme, {"object": self, "time": time}) - else: - return self.name + section_names = [] + for pick in self: + naming_scheme = ( + self.partner_id.invoice_section_name_scheme + or self.company_id.invoice_section_name_scheme + ) + if naming_scheme: + section_names.append( + safe_eval(naming_scheme, {"object": pick, "time": time}) + ) + else: + section_names.append(pick.name) + return ", ".join(section_names) diff --git a/account_invoice_section_picking/tests/common.py b/account_invoice_section_picking/tests/common.py new file mode 100644 index 00000000000..df22662c6d1 --- /dev/null +++ b/account_invoice_section_picking/tests/common.py @@ -0,0 +1,29 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.tests.common import Form, SavepointCase + + +class TestAccountInvoiceSectionPickingCommon(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env.company.invoice_section_grouping = "delivery_picking" + cls.partner_1 = cls.env.ref("base.res_partner_1") + cls.product_1 = cls.env.ref("product.product_delivery_01") + stock = cls.env.ref("stock.stock_location_stock") + cls.env["stock.quant"]._update_available_quantity(cls.product_1, stock, 1000) + cls.product_1.invoice_policy = "order" + cls.order_1 = cls._create_order() + cls.order_2 = cls._create_order(order_line_name=cls.product_1.name) + + @classmethod + def _create_order(cls, order_line_name=None): + order_form = Form(cls.env["sale.order"]) + order_form.partner_id = cls.partner_1 + with order_form: + with order_form.order_line.new() as line_form: + line_form.product_id = cls.product_1 + line_form.product_uom_qty = 5 + if order_line_name is not None: + line_form.name = order_line_name + return order_form.save() diff --git a/account_invoice_section_picking/tests/test_account_invoice_section_picking.py b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py index 4582965976e..79d337e0754 100644 --- a/account_invoice_section_picking/tests/test_account_invoice_section_picking.py +++ b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py @@ -1,20 +1,22 @@ # Copyright 2021 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo.tests.common import Form, SavepointCase +from odoo.tests.common import Form +from .common import TestAccountInvoiceSectionPickingCommon -class TestAccountInvoiceSectionPicking(SavepointCase): + +class TestAccountInvoiceSectionPicking(TestAccountInvoiceSectionPickingCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.env.company.invoice_section_grouping = "delivery_picking" cls.partner_1 = cls.env.ref("base.res_partner_1") cls.product_1 = cls.env.ref("product.product_delivery_01") + stock = cls.env.ref("stock.stock_location_stock") + cls.env["stock.quant"]._update_available_quantity(cls.product_1, stock, 1000) cls.product_1.invoice_policy = "order" cls.order_1 = cls._create_order() cls.order_2 = cls._create_order(order_line_name=cls.product_1.name) - cls.order_1.action_confirm() - cls.order_2.action_confirm() @classmethod def _create_order(cls, order_line_name=None): @@ -23,11 +25,15 @@ def _create_order(cls, order_line_name=None): with order_form: with order_form.order_line.new() as line_form: line_form.product_id = cls.product_1 + line_form.product_uom_qty = 5 if order_line_name is not None: line_form.name = order_line_name return order_form.save() def test_group_by_delivery_picking(self): + self.order_1.action_confirm() + self.order_2.action_confirm() + invoice = (self.order_1 + self.order_2)._create_invoices() self.assertEqual(len(invoice), 1) result = { @@ -36,16 +42,56 @@ def test_group_by_delivery_picking(self): 30: (self.order_2.picking_ids.name, "line_section"), 40: (self.order_2.order_line.name, False), } - for line in invoice.line_ids.filtered( - lambda l: not l.exclude_from_invoice_tab - ).sorted("sequence"): + for line in invoice.invoice_line_ids.sorted("sequence"): + self.assertEqual(line.name, result[line.sequence][0]) + self.assertEqual(line.display_type, result[line.sequence][1]) + + def test_group_by_delivery_picking_multi_steps(self): + warehouse = self.env.ref("stock.warehouse0") + warehouse.write({"delivery_steps": "pick_pack_ship"}) + + self.order_1.action_confirm() + self.order_2.action_confirm() + + invoice = (self.order_1 + self.order_2)._create_invoices() + self.assertEqual(len(invoice), 1) + result = { + 10: (self.order_1.order_line.move_ids.picking_id.name, "line_section"), + 20: (self.order_1.order_line.name, False), + 30: (self.order_2.order_line.move_ids.picking_id.name, "line_section"), + 40: (self.order_2.order_line.name, False), + } + for line in invoice.invoice_line_ids.sorted("sequence"): self.assertEqual(line.name, result[line.sequence][0]) self.assertEqual(line.display_type, result[line.sequence][1]) - # TODO: add test with warehouse using multiple delivery steps to ensure - # it's the delivery picking name that is printed + def test_group_by_delivery_picking_backorder(self): + self.order_1.action_confirm() + self.order_2.action_confirm() - # TODO: add test with product using delivered quantities after creation of - # a backorder - # - Handle possible issue with sale order line having part of its qty on - # first delivery and other part in backorder(s) + delivery_1_move = self.order_1.order_line.move_ids + delivery_1_move.move_line_ids.qty_done = 2.0 + delivery_1 = delivery_1_move.picking_id + backorder_wiz_action = delivery_1.button_validate() + backorder_wiz = ( + self.env["stock.backorder.confirmation"] + .with_context(**backorder_wiz_action.get("context")) + .create({}) + ) + backorder_wiz.process() + delivery_1_backorder_move = self.order_1.order_line.move_ids - delivery_1_move + delivery_1_backorder = delivery_1_backorder_move.picking_id + delivery_2 = self.order_2.order_line.move_ids.picking_id + invoice = (self.order_1 + self.order_2)._create_invoices() + result = { + 10: ( + ", ".join([delivery_1.name, delivery_1_backorder.name]), + "line_section", + ), + 20: (self.order_1.order_line.name, False), + 30: (delivery_2.name, "line_section"), + 40: (self.order_2.order_line.name, False), + } + for line in invoice.invoice_line_ids.sorted("sequence"): + self.assertEqual(line.name, result[line.sequence][0]) + self.assertEqual(line.display_type, result[line.sequence][1]) diff --git a/account_invoice_section_picking/tests/test_account_invoice_section_picking_backorder.py b/account_invoice_section_picking/tests/test_account_invoice_section_picking_backorder.py new file mode 100644 index 00000000000..ea8e7df0970 --- /dev/null +++ b/account_invoice_section_picking/tests/test_account_invoice_section_picking_backorder.py @@ -0,0 +1,168 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import datetime +import logging +import re +from types import MethodType + +from psycopg2 import sql +from psycopg2.extensions import AsIs + +from .common import TestAccountInvoiceSectionPickingCommon + +_logger = logging.getLogger(__name__) + +try: + from freezegun import freeze_time +except (ImportError, IOError) as err: + _logger.debug(err) + + +class TestAccountInvoiceSectionPickingBackorder(TestAccountInvoiceSectionPickingCommon): + def setUp(self): + super().setUp() + self._monkey_patch_cursor_freeze_time() + self.addCleanup(self._reset_monkey_patch_cursor_freezetime) + + def _reset_monkey_patch_cursor_freezetime(self): + self.env.cr.execute = self.env.cr.standard_exec + + def _monkey_patch_cursor_freeze_time(self): + """Monkey patch cursor to replace SQL TIME/DATE keywords""" + # TODO: Extract this part into a fork of freezegun for odoo? + # Copied from https://github.com/CloverHealth/pytest-pgsql/blob/1.1.2/pytest_pgsql/time.py#L12-L24 # noqa + _TIMESTAMP_REPLACEMENT_FORMATS = ( + # Functions + ( + r"\b((NOW|CLOCK_TIMESTAMP|STATEMENT_TIMESTAMP|TRANSACTION_TIMESTAMP)\s*\(\s*\))", # noqa + r"'{:%Y-%m-%d %H:%M:%S.%f %z}'::TIMESTAMPTZ", + ), + ( + r"\b(TIMEOFDAY\s*\(\s*\))", + r"'{:%Y-%m-%d %H:%M:%S.%f %z}'::TEXT", + ), + # Keywords + (r"\b(CURRENT_DATE)\b", r"'{:%Y-%m-%d}'::DATE"), + (r"\b(CURRENT_TIME)\b", r"'{:%H:%M:%S.%f %z}'::TIMETZ"), + ( + r"\b(CURRENT_TIMESTAMP)\b", + r"'{:%Y-%m-%d %H:%M:%S.%f %z}'::TIMESTAMPTZ", + ), + (r"\b(LOCALTIME)\b", r"'{:%H:%M:%S.%f}'::TIME"), + (r"\b(LOCALTIMESTAMP)\b", r"'{:%Y-%m-%d %H:%M:%S.%f}'::TIMESTAMP"), + ) + + self.env.cr.standard_exec = self.env.cr.execute + + def frozen_time_execute(self, query, params=None, log_exceptions=None): + def replace_keywords(regex, replacement, timestamp, query): + return re.sub( + regex, + replacement.format(timestamp), + query, + flags=re.IGNORECASE, + ) + + timestamp = datetime.datetime.now() + for regex, replacement in _TIMESTAMP_REPLACEMENT_FORMATS: + if isinstance(query, sql.Composed): + new_composed = [] + for comp in query: + if isinstance(comp, sql.SQL): + comp = replace_keywords( + regex, replacement, timestamp, comp.string + ) + new_composed.append(sql.SQL(comp)) + else: + new_composed.append(comp) + query = sql.Composed(new_composed) + else: + query = replace_keywords(regex, replacement, timestamp, query) + if params is not None: + for i, param in enumerate(params): + if isinstance(param, AsIs): + new_param = AsIs( + replace_keywords( + regex, + replacement, + timestamp, + param.getquoted().decode(), + ) + ) + params[i] = new_param + return self.standard_exec( + query, params=params, log_exceptions=log_exceptions + ) + + self.env.cr.execute = MethodType(frozen_time_execute, self.env.cr) + + def test_group_by_delivery_picking_backorder_delivered_qties(self): + self.product_1.invoice_policy = "delivery" + with freeze_time("2021-12-10 10:00:00"): + self.order_1.action_confirm() + with freeze_time("2021-12-10 10:00:01"): + self.order_2.action_confirm() + with freeze_time("2021-12-10 10:00:02"): + delivery_1_move = self.order_1.order_line.move_ids + delivery_1_move.move_line_ids.qty_done = 2.0 + delivery_1 = delivery_1_move.picking_id + + backorder_wiz_action = delivery_1.button_validate() + backorder_wiz = ( + self.env["stock.backorder.confirmation"] + .with_context(**backorder_wiz_action.get("context")) + .create({}) + ) + backorder_wiz.process() + with freeze_time("2021-12-10 10:00:03"): + delivery_1_backorder_move = ( + self.order_1.order_line.move_ids - delivery_1_move + ) + delivery_1_backorder = delivery_1_backorder_move.picking_id + delivery_2_move = self.order_2.order_line.move_ids + delivery_2_move.move_line_ids.qty_done = 3.0 + delivery_2 = delivery_2_move.picking_id + backorder_wiz_action = delivery_2.button_validate() + backorder_wiz = ( + self.env["stock.backorder.confirmation"] + .with_context(**backorder_wiz_action.get("context")) + .create({}) + ) + backorder_wiz.process() + with freeze_time("2021-12-10 10:00:04"): + delivery_2_backorder_move = ( + self.order_2.order_line.move_ids - delivery_2_move + ) + delivery_2_backorder = delivery_2_backorder_move.picking_id + self.env["account.move"].flush([]) + invoice = (self.order_1 + self.order_2)._create_invoices() + result = { + 10: (delivery_1.name, "line_section"), + 20: (self.order_1.order_line.name, False), + 30: (delivery_2.name, "line_section"), + 40: (self.order_2.order_line.name, False), + } + for line in invoice.invoice_line_ids.sorted("sequence"): + self.assertEqual(line.name, result[line.sequence][0]) + self.assertEqual(line.display_type, result[line.sequence][1]) + with freeze_time("2021-12-10 10:00:05"): + delivery_1_backorder.action_assign() + with freeze_time("2021-12-10 10:00:06"): + delivery_2_backorder.action_assign() + with freeze_time("2021-12-10 10:00:07"): + delivery_1_backorder_move.move_line_ids.qty_done = 3.0 + delivery_1_backorder.button_validate() + with freeze_time("2021-12-10 10:00:08"): + delivery_2_backorder_move.move_line_ids.qty_done = 2.0 + delivery_2_backorder.button_validate() + with freeze_time("2021-12-10 10:00:09"): + invoice_backorder = (self.order_1 + self.order_2)._create_invoices() + result_backorder = { + 10: (delivery_1_backorder.name, "line_section"), + 20: (self.order_1.order_line.name, False), + 30: (delivery_2_backorder.name, "line_section"), + 40: (self.order_2.order_line.name, False), + } + for line in invoice_backorder.invoice_line_ids.sorted("sequence"): + self.assertEqual(line.name, result_backorder[line.sequence][0]) + self.assertEqual(line.display_type, result_backorder[line.sequence][1]) From caa367c2edc22812c95f233189e8570139dad53e Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Fri, 10 Dec 2021 16:52:19 +0100 Subject: [PATCH 06/12] [DROPME] Add test_requirements.txt --- test_requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 test_requirements.txt diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 00000000000..09e6cb1152a --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1 @@ +git+https://github.com/OCA/account-invoicing@refs/pull/1050/head#subdirectory=setup/account_invoice_section_sale_order From b89758dc84e2de16afce0e345e4b2e3f1db0a044 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Tue, 14 Dec 2021 16:49:11 +0100 Subject: [PATCH 07/12] Apply suggestions from code review Co-authored-by: Simone Orsi --- .../models/stock_picking.py | 4 +-- .../tests/common.py | 1 + .../test_account_invoice_section_picking.py | 25 ------------------- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/account_invoice_section_picking/models/stock_picking.py b/account_invoice_section_picking/models/stock_picking.py index 7dde2a9b623..e0d8d5e5ecc 100644 --- a/account_invoice_section_picking/models/stock_picking.py +++ b/account_invoice_section_picking/models/stock_picking.py @@ -13,8 +13,8 @@ def _get_invoice_section_name(self): section_names = [] for pick in self: naming_scheme = ( - self.partner_id.invoice_section_name_scheme - or self.company_id.invoice_section_name_scheme + pick.partner_id.invoice_section_name_scheme + or pick.company_id.invoice_section_name_scheme ) if naming_scheme: section_names.append( diff --git a/account_invoice_section_picking/tests/common.py b/account_invoice_section_picking/tests/common.py index df22662c6d1..f136c674166 100644 --- a/account_invoice_section_picking/tests/common.py +++ b/account_invoice_section_picking/tests/common.py @@ -7,6 +7,7 @@ class TestAccountInvoiceSectionPickingCommon(SavepointCase): @classmethod def setUpClass(cls): super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls.env.company.invoice_section_grouping = "delivery_picking" cls.partner_1 = cls.env.ref("base.res_partner_1") cls.product_1 = cls.env.ref("product.product_delivery_01") diff --git a/account_invoice_section_picking/tests/test_account_invoice_section_picking.py b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py index 79d337e0754..922aba29307 100644 --- a/account_invoice_section_picking/tests/test_account_invoice_section_picking.py +++ b/account_invoice_section_picking/tests/test_account_invoice_section_picking.py @@ -1,35 +1,10 @@ # Copyright 2021 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo.tests.common import Form from .common import TestAccountInvoiceSectionPickingCommon class TestAccountInvoiceSectionPicking(TestAccountInvoiceSectionPickingCommon): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.env.company.invoice_section_grouping = "delivery_picking" - cls.partner_1 = cls.env.ref("base.res_partner_1") - cls.product_1 = cls.env.ref("product.product_delivery_01") - stock = cls.env.ref("stock.stock_location_stock") - cls.env["stock.quant"]._update_available_quantity(cls.product_1, stock, 1000) - cls.product_1.invoice_policy = "order" - cls.order_1 = cls._create_order() - cls.order_2 = cls._create_order(order_line_name=cls.product_1.name) - - @classmethod - def _create_order(cls, order_line_name=None): - order_form = Form(cls.env["sale.order"]) - order_form.partner_id = cls.partner_1 - with order_form: - with order_form.order_line.new() as line_form: - line_form.product_id = cls.product_1 - line_form.product_uom_qty = 5 - if order_line_name is not None: - line_form.name = order_line_name - return order_form.save() - def test_group_by_delivery_picking(self): self.order_1.action_confirm() self.order_2.action_confirm() From f8187b8bdbd7f062a08ffd1dc74d097dce21655f Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Tue, 21 Dec 2021 16:01:23 +0100 Subject: [PATCH 08/12] Do not rely on company from environment --- .../models/account_move_line.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/account_invoice_section_picking/models/account_move_line.py b/account_invoice_section_picking/models/account_move_line.py index 6171cd145d6..75131c7eea3 100644 --- a/account_invoice_section_picking/models/account_move_line.py +++ b/account_invoice_section_picking/models/account_move_line.py @@ -1,6 +1,6 @@ # Copyright 2021 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import api, models +from odoo import models class AccountMoveLine(models.Model): @@ -13,7 +13,7 @@ def _get_section_group(self): # - filter on done pickings to avoid displaying backorders # - Remove pickings linked with same sale order lines if an invoice # was created after its date_done - invoice_section_grouping = self.env.company.invoice_section_grouping + invoice_section_grouping = self.company_id.invoice_section_grouping if ( invoice_section_grouping == "delivery_picking" and self.product_id.invoice_policy == "delivery" @@ -38,9 +38,8 @@ def _get_section_group(self): ) return group - @api.model def _get_section_grouping(self): - invoice_section_grouping = self.env.company.invoice_section_grouping + invoice_section_grouping = self.company_id.invoice_section_grouping if invoice_section_grouping == "delivery_picking": return "sale_line_ids.move_ids.picking_id" return super()._get_section_grouping() From 457f0e03500a503b7199a826b4e0e23cd8626606 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Tue, 21 Dec 2021 16:01:43 +0100 Subject: [PATCH 09/12] Add docstrings, load test --- account_invoice_section_picking/models/account_move_line.py | 4 +++- account_invoice_section_picking/tests/__init__.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/account_invoice_section_picking/models/account_move_line.py b/account_invoice_section_picking/models/account_move_line.py index 75131c7eea3..93da7cf287a 100644 --- a/account_invoice_section_picking/models/account_move_line.py +++ b/account_invoice_section_picking/models/account_move_line.py @@ -8,9 +8,11 @@ class AccountMoveLine(models.Model): _inherit = "account.move.line" def _get_section_group(self): + """Group invoice lines according to uninvoiced delivery pickings""" group = super()._get_section_group() # If product is invoiced by delivered quantities: - # - filter on done pickings to avoid displaying backorders + # - filter on done pickings to avoid displaying backorders not yet + # processed # - Remove pickings linked with same sale order lines if an invoice # was created after its date_done invoice_section_grouping = self.company_id.invoice_section_grouping diff --git a/account_invoice_section_picking/tests/__init__.py b/account_invoice_section_picking/tests/__init__.py index c09ba92697e..955e4115470 100644 --- a/account_invoice_section_picking/tests/__init__.py +++ b/account_invoice_section_picking/tests/__init__.py @@ -1 +1,2 @@ from . import test_account_invoice_section_picking +from . import test_account_invoice_section_picking_backorder From 25c7be28e1944686c8a62e672a079d6eae599b9e Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Wed, 22 Dec 2021 12:03:09 +0100 Subject: [PATCH 10/12] Apply suggestions from code review Co-authored-by: Iryna Vyshnevska --- account_invoice_section_picking/__manifest__.py | 2 +- account_invoice_section_picking/models/account_move_line.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/account_invoice_section_picking/__manifest__.py b/account_invoice_section_picking/__manifest__.py index 14820730061..3fdfd30115b 100644 --- a/account_invoice_section_picking/__manifest__.py +++ b/account_invoice_section_picking/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) { "name": "Acccount Invoice Section Picking", - "version": "14.0.1.0.1", + "version": "14.0.1.0.0", "summary": "Extension of Acccount Invoice Section Sale Order to allow " "grouping of invoice lines according to delivery picking.", "author": "Camptocamp, Odoo Community Association (OCA)", diff --git a/account_invoice_section_picking/models/account_move_line.py b/account_invoice_section_picking/models/account_move_line.py index 93da7cf287a..1ab53cba4fe 100644 --- a/account_invoice_section_picking/models/account_move_line.py +++ b/account_invoice_section_picking/models/account_move_line.py @@ -23,7 +23,7 @@ def _get_section_group(self): last_invoice_line = self.search( [ ("sale_line_ids", "in", self.sale_line_ids.ids), - ("id", "not in", self.ids), + ("id", "!=", self.id), ], order="create_date", limit=1, From 07fb5e5faca9620ea2a64f475f058be0ef45f695 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Wed, 22 Dec 2021 16:56:48 +0100 Subject: [PATCH 11/12] Do not select cancel invoices, add known issues --- .../models/account_move_line.py | 1 + account_invoice_section_picking/readme/ROADMAP.rst | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 account_invoice_section_picking/readme/ROADMAP.rst diff --git a/account_invoice_section_picking/models/account_move_line.py b/account_invoice_section_picking/models/account_move_line.py index 1ab53cba4fe..c65b081610a 100644 --- a/account_invoice_section_picking/models/account_move_line.py +++ b/account_invoice_section_picking/models/account_move_line.py @@ -23,6 +23,7 @@ def _get_section_group(self): last_invoice_line = self.search( [ ("sale_line_ids", "in", self.sale_line_ids.ids), + ("state", "!=", "cancel"), ("id", "!=", self.id), ], order="create_date", diff --git a/account_invoice_section_picking/readme/ROADMAP.rst b/account_invoice_section_picking/readme/ROADMAP.rst new file mode 100644 index 00000000000..0151c42fdd6 --- /dev/null +++ b/account_invoice_section_picking/readme/ROADMAP.rst @@ -0,0 +1,10 @@ +* The selection of pickings for the section name relies on the last invoice + that was created and is linked to the sale order line. In such case, there's + no guarantee the selection is correct if the quantity is reduced on prior + invoice lines. +* Moreover, as Odoo considers the draft invoices for the computation of + `qty_invoiced` on sales order line, we couldn't base the selection of the last + invoice on another field than the `create_date` although it would have been + cleaner to rely on a `date` field, but this one is only set on the posting. + Finally, defining another field for the generation of invoices wouldn't have + helped solve these issues. From 6913009015a3fbaa85ce1ad8b625b17028660eb7 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Wed, 19 Jan 2022 18:35:54 +0100 Subject: [PATCH 12/12] Update account_invoice_section_picking/models/account_move_line.py Co-authored-by: ajaniszewska-dev <59824990+ajaniszewska-dev@users.noreply.github.com> --- account_invoice_section_picking/models/account_move_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_invoice_section_picking/models/account_move_line.py b/account_invoice_section_picking/models/account_move_line.py index c65b081610a..80f174b2fff 100644 --- a/account_invoice_section_picking/models/account_move_line.py +++ b/account_invoice_section_picking/models/account_move_line.py @@ -23,7 +23,7 @@ def _get_section_group(self): last_invoice_line = self.search( [ ("sale_line_ids", "in", self.sale_line_ids.ids), - ("state", "!=", "cancel"), + ("parent_state", "!=", "cancel"), ("id", "!=", self.id), ], order="create_date",