diff --git a/account_invoice_section_sale_order/models/sale_order.py b/account_invoice_section_sale_order/models/sale_order.py index b6013f0771d..ea215455284 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: - if ( + invoices = super()._create_invoices(grouped=grouped, final=final, date=date) + for invoice in invoices.sudo(): + if invoice.line_ids and ( 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() @@ -55,19 +57,17 @@ 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}" + 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 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/__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 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..a8d3a372275 --- /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 TransactionCase + + +@tagged("-at_install", "post_install") +class Common(TransactionCase): + @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..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 @@ -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 = { @@ -118,10 +23,8 @@ def test_create_invoice(self): 20: ("order 1 line 1", "product"), 30: ("order 1 line 2", "product"), 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", "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") @@ -135,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""" @@ -197,13 +100,11 @@ def test_custom_grouping_by_sale_order_user(self): 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"), + 40: ("order 2 line 1", "product"), + 50: ("order 2 line 2", "product"), + 60: ("Mocked value from ResUsers", "line_section"), + 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: