From 66f44ba6db723968fb096ff98de4ed84ae5d5060 Mon Sep 17 00:00:00 2001 From: Mmequignon Date: Fri, 9 Aug 2024 12:38:50 +0200 Subject: [PATCH 1/5] account_invoice_section_sale_order: Fix read/write access the `sale.order._create_invoices() method does not only requires `create` access to the `account.move` and `account.move.line` models. Because we're actually sorting lines after the creating, it requires `read` access to sort, and `write` access to modify their sequence. This is problematic when interracting with other modules like `invoice_mode_at_shipping` where stock users are the ones to create invoices. This commit adds a few `sudo()` in the code, in order to avoid granting `read` and `write` access to users that shouldn't be allowed to read and write invoices. --- .../models/sale_order.py | 20 ++- .../tests/common.py | 84 ++++++++++++ .../tests/test_access_rights.py | 60 +++++++++ .../tests/test_invoice_group_by_sale_order.py | 127 ++---------------- 4 files changed, 167 insertions(+), 124 deletions(-) create mode 100644 account_invoice_section_sale_order/tests/common.py create mode 100644 account_invoice_section_sale_order/tests/test_access_rights.py diff --git a/account_invoice_section_sale_order/models/sale_order.py b/account_invoice_section_sale_order/models/sale_order.py index b6013f0771d..4e0bffdbe79 100644 --- a/account_invoice_section_sale_order/models/sale_order.py +++ b/account_invoice_section_sale_order/models/sale_order.py @@ -16,14 +16,16 @@ def _create_invoices(self, grouped=False, final=False, date=None): the group name. Only do this for invoices targetting multiple groups """ - invoice_ids = super()._create_invoices(grouped=grouped, final=final, date=date) - for invoice in invoice_ids: + invoices = super()._create_invoices(grouped=grouped, final=final, date=date) + for invoice in invoices.sudo(): if ( len(invoice.line_ids.mapped(invoice.line_ids._get_section_grouping())) == 1 ): continue sequence = 10 + # Because invoices are already created, this would require + # an extra read access in order to read order fields. move_lines = invoice._get_ordered_invoice_lines() # Group move lines according to their sale order section_grouping_matrix = OrderedDict() @@ -56,18 +58,14 @@ def _create_invoices(self, grouped=False, final=False, date=None): ) sequence += 10 for move_line in self.env["account.move.line"].browse(move_line_ids): - if move_line.display_type == "line_section": - # add extra indent for existing SO Sections - move_line.name = f"- {move_line.name}" + # Because invoices are already created, this would require + # an extra write access in order to read order fields. move_line.sequence = sequence sequence += 10 + # Because invoices are already created, this would require + # an extra write access in order to read order fields. invoice.line_ids = section_lines - return invoice_ids - - def _get_ordered_invoice_lines(self, invoice): - return invoice.invoice_line_ids.sorted( - key=lambda r: r.sale_line_ids.order_id.id - ) + return invoices def _get_invoice_section_name(self): """Returns the text for the section name.""" diff --git a/account_invoice_section_sale_order/tests/common.py b/account_invoice_section_sale_order/tests/common.py new file mode 100644 index 00000000000..24991186231 --- /dev/null +++ b/account_invoice_section_sale_order/tests/common.py @@ -0,0 +1,84 @@ +from odoo.tests import tagged +from odoo.tests.common import SavepointCase + + +@tagged("-at_install", "post_install") +class Common(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.setUpClassOrder() + + @classmethod + def setUpClassOrder(cls): + cls.partner_1 = cls.env.ref("base.res_partner_1") + cls.product_1 = cls.env.ref("product.product_product_1") + cls.product_2 = cls.env.ref("product.product_product_2") + cls.product_1.invoice_policy = "order" + cls.product_2.invoice_policy = "order" + cls.order1_p1 = cls.env["sale.order"].create( + { + "partner_id": cls.partner_1.id, + "partner_shipping_id": cls.partner_1.id, + "partner_invoice_id": cls.partner_1.id, + "client_order_ref": "ref123", + "order_line": [ + ( + 0, + 0, + { + "name": "order 1 line 1", + "product_id": cls.product_1.id, + "price_unit": 20, + "product_uom_qty": 1, + "product_uom": cls.product_1.uom_id.id, + }, + ), + ( + 0, + 0, + { + "name": "order 1 line 2", + "product_id": cls.product_2.id, + "price_unit": 20, + "product_uom_qty": 1, + "product_uom": cls.product_1.uom_id.id, + }, + ), + ], + } + ) + cls.order1_p1.action_confirm() + cls.order2_p1 = cls.env["sale.order"].create( + { + "partner_id": cls.partner_1.id, + "partner_shipping_id": cls.partner_1.id, + "partner_invoice_id": cls.partner_1.id, + "order_line": [ + ( + 0, + 0, + { + "name": "order 2 line 1", + "product_id": cls.product_1.id, + "price_unit": 20, + "product_uom_qty": 1, + "product_uom": cls.product_1.uom_id.id, + }, + ), + ( + 0, + 0, + { + "name": "order 2 line 2", + "product_id": cls.product_2.id, + "price_unit": 20, + "product_uom_qty": 1, + "product_uom": cls.product_1.uom_id.id, + }, + ), + ], + } + ) + cls.order2_p1.action_confirm() diff --git a/account_invoice_section_sale_order/tests/test_access_rights.py b/account_invoice_section_sale_order/tests/test_access_rights.py new file mode 100644 index 00000000000..f1afd39d1f1 --- /dev/null +++ b/account_invoice_section_sale_order/tests/test_access_rights.py @@ -0,0 +1,60 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.tests import tagged + +from .common import Common + + +@tagged("-at_install", "post_install") +class TestAccessRights(Common): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.setUpClassUser() + + @classmethod + def setUpClassUser(cls): + cls.create_only_group = cls.env["res.groups"].create( + {"name": "Create Only Group"} + ) + cls.sale_manager_group = cls.env.ref("sales_team.group_sale_manager") + cls.env["ir.model.access"].create( + [ + { + "name": "invoice_create_only", + "model_id": cls.env.ref("account.model_account_move").id, + "group_id": cls.create_only_group.id, + "perm_read": 0, + "perm_write": 0, + "perm_create": 1, + "perm_unlink": 0, + }, + { + "name": "invoice_line_create_only", + "model_id": cls.env.ref("account.model_account_move_line").id, + "group_id": cls.create_only_group.id, + "perm_read": 0, + "perm_write": 0, + "perm_create": 1, + "perm_unlink": 0, + }, + ] + ) + cls.create_only_user = cls.env["res.users"].create( + { + "name": "Create Only User", + "login": "createonlyuser@example.com", + "groups_id": [ + (6, 0, (cls.create_only_group | cls.sale_manager_group).ids), + ], + } + ) + + def test_access_rights(self): + orders = self.order1_p1 + self.order2_p1 + # We're testing that no exception is raised while creating invoices + # with a user having only create access on the invoices models + invoice_ids = orders.with_user(self.create_only_user)._create_invoices() + self.assertTrue(bool(invoice_ids)) diff --git a/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py b/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py index 872d5a22337..aab1b6212a1 100644 --- a/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py +++ b/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py @@ -3,7 +3,8 @@ from unittest import mock from odoo.exceptions import UserError -from odoo.tests.common import TransactionCase + +from .common import Common SECTION_GROUPING_FUNCTION = "odoo.addons.account_invoice_section_sale_order.models.account_move.AccountMoveLine._get_section_grouping" # noqa SECTION_NAME_FUNCTION = ( @@ -11,103 +12,7 @@ ) -class TestInvoiceGroupBySaleOrder(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.partner_1 = cls.env.ref("base.res_partner_1") - cls.product_1 = cls.env.ref("product.product_product_1") - cls.product_2 = cls.env.ref("product.product_product_2") - cls.product_1.invoice_policy = "order" - cls.product_2.invoice_policy = "order" - eur = cls.env.ref("base.EUR") - cls.pricelist = cls.env["product.pricelist"].create( - {"name": "Europe pricelist", "currency_id": eur.id} - ) - cls.order1_p1 = cls.env["sale.order"].create( - { - "partner_id": cls.partner_1.id, - "partner_shipping_id": cls.partner_1.id, - "partner_invoice_id": cls.partner_1.id, - "pricelist_id": cls.pricelist.id, - "client_order_ref": "ref123", - "order_line": [ - ( - 0, - 0, - { - "name": "order 1 line 1", - "product_id": cls.product_1.id, - "price_unit": 20, - "product_uom_qty": 1, - "product_uom": cls.product_1.uom_id.id, - }, - ), - ( - 0, - 0, - { - "name": "order 1 line 2", - "product_id": cls.product_2.id, - "price_unit": 20, - "product_uom_qty": 1, - "product_uom": cls.product_1.uom_id.id, - }, - ), - ], - } - ) - cls.order1_p1.action_confirm() - cls.order2_p1 = cls.env["sale.order"].create( - { - "partner_id": cls.partner_1.id, - "partner_shipping_id": cls.partner_1.id, - "partner_invoice_id": cls.partner_1.id, - "pricelist_id": cls.pricelist.id, - "order_line": [ - ( - 0, - 0, - { - "name": "order 2 section 1", - "display_type": "line_section", - }, - ), - ( - 0, - 0, - { - "name": "order 2 line 1", - "product_id": cls.product_1.id, - "price_unit": 20, - "product_uom_qty": 1, - "product_uom": cls.product_1.uom_id.id, - }, - ), - ( - 0, - 0, - { - "name": "order 2 section 2", - "display_type": "line_section", - }, - ), - ( - 0, - 0, - { - "name": "order 2 line 2", - "product_id": cls.product_2.id, - "price_unit": 20, - "product_uom_qty": 1, - "product_uom": cls.product_1.uom_id.id, - }, - ), - ], - } - ) - cls.order2_p1.action_confirm() - +class TestInvoiceGroupBySaleOrder(Common): def test_create_invoice(self): """Check invoice is generated with sale order sections.""" result = { @@ -115,13 +20,11 @@ def test_create_invoice(self): "".join([self.order1_p1.name, " - ", self.order1_p1.client_order_ref]), "line_section", ), - 20: ("order 1 line 1", "product"), - 30: ("order 1 line 2", "product"), + 20: ("order 1 line 1", False), + 30: ("order 1 line 2", False), 40: (self.order2_p1.name, "line_section"), - 50: ("- order 2 section 1", "line_section"), - 60: ("order 2 line 1", "product"), - 70: ("- order 2 section 2", "line_section"), - 80: ("order 2 line 2", "product"), + 50: ("order 2 line 1", False), + 60: ("order 2 line 2", False), } invoice_ids = (self.order1_p1 + self.order2_p1)._create_invoices() lines = invoice_ids[0].invoice_line_ids.sorted("sequence") @@ -195,15 +98,13 @@ def test_custom_grouping_by_sale_order_user(self): invoice = (orders + sale_order_3)._create_invoices() result = { 10: ("Mocked value from ResUsers", "line_section"), - 20: ("order 1 line 1", "product"), - 30: ("order 1 line 2", "product"), - 40: ("- order 2 section 1", "line_section"), - 50: ("order 2 line 1", "product"), - 60: ("- order 2 section 2", "line_section"), - 70: ("order 2 line 2", "product"), - 80: ("Mocked value from ResUsers", "line_section"), - 90: ("order 3 line 1", "product"), - 100: ("order 3 line 2", "product"), + 20: ("order 1 line 1", False), + 30: ("order 1 line 2", False), + 40: ("order 2 line 1", False), + 50: ("order 2 line 2", False), + 60: ("Mocked value from ResUsers", "line_section"), + 70: ("order 3 line 1", False), + 80: ("order 3 line 2", False), } for line in invoice.invoice_line_ids.sorted("sequence"): if line.sequence not in result: From 0b27584e2395e37115982f7ee7d9e6ff317ff209 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 9 Dec 2024 11:48:25 +0100 Subject: [PATCH 2/5] account_invoice_section_sale_order: fix access right tests not running --- account_invoice_section_sale_order/tests/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/account_invoice_section_sale_order/tests/__init__.py b/account_invoice_section_sale_order/tests/__init__.py index 20dcf91002a..ca02ec06eca 100644 --- a/account_invoice_section_sale_order/tests/__init__.py +++ b/account_invoice_section_sale_order/tests/__init__.py @@ -1 +1,2 @@ from . import test_invoice_group_by_sale_order +from . import test_access_rights From 2f890a31cf21442c2b62bfdee5eb6791cd02af01 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 9 Dec 2024 11:48:54 +0100 Subject: [PATCH 3/5] account_invoice_section_sale_order: fix access right on move line write --- account_invoice_section_sale_order/models/sale_order.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/account_invoice_section_sale_order/models/sale_order.py b/account_invoice_section_sale_order/models/sale_order.py index 4e0bffdbe79..f8cc9f9b596 100644 --- a/account_invoice_section_sale_order/models/sale_order.py +++ b/account_invoice_section_sale_order/models/sale_order.py @@ -57,7 +57,9 @@ def _create_invoices(self, grouped=False, final=False, date=None): ) ) sequence += 10 - for move_line in self.env["account.move.line"].browse(move_line_ids): + for move_line in ( + self.env["account.move.line"].sudo().browse(move_line_ids) + ): # Because invoices are already created, this would require # an extra write access in order to read order fields. move_line.sequence = sequence From 678ad5499d22b75731fe9d1b3510ee0390c813ad Mon Sep 17 00:00:00 2001 From: sergiocorato Date: Thu, 1 Aug 2024 10:06:42 +0200 Subject: [PATCH 4/5] [FIX] account_invoice_section_sale_order check line_ids exists --- account_invoice_section_sale_order/models/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_invoice_section_sale_order/models/sale_order.py b/account_invoice_section_sale_order/models/sale_order.py index f8cc9f9b596..ea215455284 100644 --- a/account_invoice_section_sale_order/models/sale_order.py +++ b/account_invoice_section_sale_order/models/sale_order.py @@ -18,7 +18,7 @@ def _create_invoices(self, grouped=False, final=False, date=None): """ invoices = super()._create_invoices(grouped=grouped, final=final, date=date) for invoice in invoices.sudo(): - if ( + if invoice.line_ids and ( len(invoice.line_ids.mapped(invoice.line_ids._get_section_grouping())) == 1 ): From 17292ca69b8e87e9c66e6af62a346b645b793e0f Mon Sep 17 00:00:00 2001 From: Lukas Tran Date: Tue, 31 Dec 2024 16:50:54 +0700 Subject: [PATCH 5/5] [FIX] account_invoice_section_sale_order: fix test-case test_create_invoice_with_currency --- .../tests/common.py | 4 ++-- .../tests/test_invoice_group_by_sale_order.py | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/account_invoice_section_sale_order/tests/common.py b/account_invoice_section_sale_order/tests/common.py index 24991186231..a8d3a372275 100644 --- a/account_invoice_section_sale_order/tests/common.py +++ b/account_invoice_section_sale_order/tests/common.py @@ -1,9 +1,9 @@ from odoo.tests import tagged -from odoo.tests.common import SavepointCase +from odoo.tests.common import TransactionCase @tagged("-at_install", "post_install") -class Common(SavepointCase): +class Common(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() diff --git a/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py b/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py index aab1b6212a1..2a3369cdb60 100644 --- a/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py +++ b/account_invoice_section_sale_order/tests/test_invoice_group_by_sale_order.py @@ -20,11 +20,11 @@ def test_create_invoice(self): "".join([self.order1_p1.name, " - ", self.order1_p1.client_order_ref]), "line_section", ), - 20: ("order 1 line 1", False), - 30: ("order 1 line 2", False), + 20: ("order 1 line 1", "product"), + 30: ("order 1 line 2", "product"), 40: (self.order2_p1.name, "line_section"), - 50: ("order 2 line 1", False), - 60: ("order 2 line 2", False), + 50: ("order 2 line 1", "product"), + 60: ("order 2 line 2", "product"), } invoice_ids = (self.order1_p1 + self.order2_p1)._create_invoices() lines = invoice_ids[0].invoice_line_ids.sorted("sequence") @@ -38,7 +38,7 @@ def test_create_invoice_with_currency(self): """Check invoice is generated with a correct total amount""" orders = self.order1_p1 | self.order2_p1 invoices = orders._create_invoices() - self.assertEqual(invoices.amount_total, 80) + self.assertEqual(invoices.amount_untaxed, 80) def test_create_invoice_with_default_journal(self): """Using a specific journal for the invoice should not be broken""" @@ -98,13 +98,13 @@ def test_custom_grouping_by_sale_order_user(self): invoice = (orders + sale_order_3)._create_invoices() result = { 10: ("Mocked value from ResUsers", "line_section"), - 20: ("order 1 line 1", False), - 30: ("order 1 line 2", False), - 40: ("order 2 line 1", False), - 50: ("order 2 line 2", False), + 20: ("order 1 line 1", "product"), + 30: ("order 1 line 2", "product"), + 40: ("order 2 line 1", "product"), + 50: ("order 2 line 2", "product"), 60: ("Mocked value from ResUsers", "line_section"), - 70: ("order 3 line 1", False), - 80: ("order 3 line 2", False), + 70: ("order 3 line 1", "product"), + 80: ("order 3 line 2", "product"), } for line in invoice.invoice_line_ids.sorted("sequence"): if line.sequence not in result: