diff --git a/requirements.txt b/requirements.txt index 9cd1629223b..180fc49789b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ # generated from manifests external_dependencies +openupgradelib diff --git a/sale_invoice_policy/README.rst b/sale_invoice_policy/README.rst index 282b8ad535c..f78b7c5c05c 100644 --- a/sale_invoice_policy/README.rst +++ b/sale_invoice_policy/README.rst @@ -28,21 +28,58 @@ Sale invoice Policy |badge1| |badge2| |badge3| |badge4| |badge5| -This modules helps to get Invoicing Policy on Sale Order Level without -breaking behaviour (as it is defined from >= v10 on product level). +This module adds an invoicing policy on sale order level in order to +apply that invoicing policy on the whole sale order. + +That invoicing policy can take three values: + +- Products Invoicing Policy: The sale order will follow the standard + behavior and apply the policy depending on products configurations. +- Ordered Quantities: The sale order will invoice the ordered quantities. +- Delivered Quantities: The sale order will invoice the delivered quantities. + +Following the chosen policy, the quantity to invoice and the +amount to invoice on each line will be computed accordingly. +Note that the sale order policy will affect only storable products. + +You will be able also to define a default invoicing policy +(globally per company) +that can be different than the default invoicing policy for new products. **Table of contents** .. contents:: :local: +Use Cases / Context +=================== + +In Odoo, products have their own invoicing policy that can be: + +- Invoicing on ordered quantities +- Invoicing on ordered quantities + +Following that configuration, when trying to create invoices from +sale orders, each line of product will apply its invoicing policy. + +In some cases, user needs to apply an invoicing policy on a whole +sale order. + +The solution proposed here is to add an invoicing policy on +sale order level. + +Configuration +============= + +* Go to Sale > Configuration > Settings > Sale Invoice Policy +* Choose the one that fits your needs. + Usage ===== * Create Sale Order -* Select Invoicing Policy on Sale Order or let it void -* Either the policy selected on Sale Order would be used, either if not - filled in, the policy would be chosen from product configuration +* Select Invoicing Policy on Sale Order or let it on Products Invoicing Policy +* The created invoices will use the configuration on sale order. Bug Tracker =========== @@ -72,6 +109,7 @@ Contributors * Luis J. Salvatierra * Alejandro Ji Cheung * Ioan Galan +* Laurent Mignon Maintainers ~~~~~~~~~~~ diff --git a/sale_invoice_policy/__init__.py b/sale_invoice_policy/__init__.py index 0650744f6bc..548c73eb1a1 100644 --- a/sale_invoice_policy/__init__.py +++ b/sale_invoice_policy/__init__.py @@ -1 +1,2 @@ +from .hooks import pre_init_hook from . import models diff --git a/sale_invoice_policy/__manifest__.py b/sale_invoice_policy/__manifest__.py index 28247ed01c9..588078ccd7d 100644 --- a/sale_invoice_policy/__manifest__.py +++ b/sale_invoice_policy/__manifest__.py @@ -10,9 +10,11 @@ "category": "Sales Management", "version": "16.0.2.0.0", "license": "AGPL-3", - "depends": ["sale_stock"], + "depends": ["sale_stock", "base_partition"], + "external_dependencies": {"python": ["openupgradelib"]}, "data": [ "views/res_config_settings_view.xml", "views/sale_view.xml", ], + "pre_init_hook": "pre_init_hook", } diff --git a/sale_invoice_policy/hooks.py b/sale_invoice_policy/hooks.py new file mode 100644 index 00000000000..320fcc6d816 --- /dev/null +++ b/sale_invoice_policy/hooks.py @@ -0,0 +1,25 @@ +# Copyright 2024 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from openupgradelib import openupgrade + +from odoo.api import SUPERUSER_ID, Environment + + +def pre_init_hook(cr): + """ + Create the sale order invoice policy with the "product" policy (standard) + but with a postgres query to avoid an update on all sale order records + """ + env = Environment(cr, SUPERUSER_ID, {}) + field_spec = [ + ( + "invoice_policy", + "sale.order", + False, + "selection", + False, + "sale_invoice_policy", + "product", + ) + ] + openupgrade.add_fields(env, field_spec=field_spec) diff --git a/sale_invoice_policy/models/__init__.py b/sale_invoice_policy/models/__init__.py index db2da088832..c7862adb115 100644 --- a/sale_invoice_policy/models/__init__.py +++ b/sale_invoice_policy/models/__init__.py @@ -1,3 +1,4 @@ from . import res_config_settings from . import sale_order from . import sale_order_line +from . import res_company diff --git a/sale_invoice_policy/models/res_company.py b/sale_invoice_policy/models/res_company.py new file mode 100644 index 00000000000..b7b6227bb85 --- /dev/null +++ b/sale_invoice_policy/models/res_company.py @@ -0,0 +1,20 @@ +# Copyright 2024 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResCompany(models.Model): + + _inherit = "res.company" + + sale_default_invoice_policy = fields.Selection( + [ + ("product", "Products Invoice Policy"), + ("order", "Ordered quantities"), + ("delivery", "Delivered quantities"), + ], + default="product", + required=True, + help="This will be the default invoice policy for sale orders.", + ) diff --git a/sale_invoice_policy/models/res_config_settings.py b/sale_invoice_policy/models/res_config_settings.py index 7e5a6c9539a..7ba62af605c 100644 --- a/sale_invoice_policy/models/res_config_settings.py +++ b/sale_invoice_policy/models/res_config_settings.py @@ -1,34 +1,13 @@ # Copyright 2018 ACSONE SA/NV () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, fields, models +from odoo import fields, models class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" - sale_invoice_policy_required = fields.Boolean( - help="This makes Invoice Policy required on Sale Orders" + sale_default_invoice_policy = fields.Selection( + related="company_id.sale_default_invoice_policy", + readonly=False, ) - - @api.model - def get_values(self): - res = super().get_values() - res.update( - sale_invoice_policy_required=self.env["ir.default"].get( - "res.config.settings", "sale_invoice_policy_required" - ) - ) - return res - - def set_values(self): - super().set_values() - ir_default_obj = self.env["ir.default"] - if self.env["res.users"].has_group("base.group_erp_manager"): - ir_default_obj = ir_default_obj.sudo() - ir_default_obj.set( - "res.config.settings", - "sale_invoice_policy_required", - self.sale_invoice_policy_required, - ) - return True diff --git a/sale_invoice_policy/models/sale_order.py b/sale_invoice_policy/models/sale_order.py index b9c2a5b1296..47d7c87f470 100644 --- a/sale_invoice_policy/models/sale_order.py +++ b/sale_invoice_policy/models/sale_order.py @@ -9,41 +9,27 @@ class SaleOrder(models.Model): _inherit = "sale.order" invoice_policy = fields.Selection( - [("order", "Ordered quantities"), ("delivery", "Delivered quantities")], - readonly=True, + [ + ("product", "Products Invoice Policy"), + ("order", "Ordered quantities"), + ("delivery", "Delivered quantities"), + ], + compute="_compute_invoice_policy", + store=True, + readonly=False, + required=True, states={"draft": [("readonly", False)], "sent": [("readonly", False)]}, + precompute=True, help="Ordered Quantity: Invoice based on the quantity the customer " "ordered.\n" "Delivered Quantity: Invoiced based on the quantity the vendor " - "delivered (time or deliveries).", + "delivered (time or deliveries). This applies for storable products only.", ) - invoice_policy_required = fields.Boolean( - compute="_compute_invoice_policy_required", - default=lambda self: self.env["ir.default"].get( - "res.config.settings", "sale_invoice_policy_required" - ), - ) - - @api.model - def default_get(self, fields_list): - res = super().default_get(fields_list) - default_invoice_policy = ( - self.env["res.config.settings"] - .sudo() - .default_get(["default_invoice_policy"]) - .get("default_invoice_policy", False) - ) - if "invoice_policy" not in res: - res.update({"invoice_policy": default_invoice_policy}) - return res - @api.depends("partner_id") - def _compute_invoice_policy_required(self): - invoice_policy_required = ( - self.env["res.config.settings"] - .sudo() - .default_get(["sale_invoice_policy_required"]) - .get("sale_invoice_policy_required", False) - ) - for sale in self: - sale.invoice_policy_required = invoice_policy_required + @api.depends("company_id") + def _compute_invoice_policy(self) -> None: + """ + Get default sale order invoice policy + """ + for company, sale_orders in self.partition("company_id").items(): + sale_orders.invoice_policy = company.sale_default_invoice_policy diff --git a/sale_invoice_policy/models/sale_order_line.py b/sale_invoice_policy/models/sale_order_line.py index a00f8204e02..fea12193336 100644 --- a/sale_invoice_policy/models/sale_order_line.py +++ b/sale_invoice_policy/models/sale_order_line.py @@ -1,24 +1,45 @@ # Copyright 2017 ACSONE SA/NV () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from contextlib import contextmanager -from odoo import api, fields, models +from odoo import api, models class SaleOrderLine(models.Model): _inherit = "sale.order.line" - @api.depends( - "qty_invoiced", - "qty_delivered", - "product_uom_qty", - "state", - "order_id.invoice_policy", - ) + @contextmanager + def _sale_invoice_policy(self, lines): + """Apply the sale invoice policy to the products + + This method must be called with lines sharing the same invoice policy + """ + invoice_policy = set(lines.mapped("order_id.invoice_policy")) + if len(invoice_policy) > 1: + raise Exception( + "The method _sale_invoice_policy() must be called with lines " + "sharing the same invoice policy" + ) + invoice_policy = next(iter(invoice_policy)) + invoice_policy_field = self.env["product.product"]._fields["invoice_policy"] + products = lines.product_id + with self.env.protecting([invoice_policy_field], products): + old_values = {} + for product in products: + old_values[product] = product.invoice_policy + product.invoice_policy = invoice_policy + yield + for product, invoice_policy in old_values.items(): + product.invoice_policy = invoice_policy + + @api.depends("order_id.invoice_policy") def _compute_qty_to_invoice(self): + """ + Exclude lines that have their order invoice policy filled in + """ other_lines = self.filtered( lambda l: l.product_id.type == "service" - or not l.order_id.invoice_policy - or not l.order_id.invoice_policy_required + or l.order_id.invoice_policy == "product" ) super(SaleOrderLine, other_lines)._compute_qty_to_invoice() for line in self - other_lines: @@ -29,74 +50,16 @@ def _compute_qty_to_invoice(self): line.qty_to_invoice = line.qty_delivered - line.qty_invoiced return True - @api.depends( - "state", - "price_reduce", - "product_id", - "untaxed_amount_invoiced", - "qty_delivered", - "product_uom_qty", - "order_id.invoice_policy", - ) - def _compute_untaxed_amount_to_invoice(self): + @api.depends("order_id.invoice_policy") + def _compute_untaxed_amount_to_invoice(self) -> None: other_lines = self.filtered( lambda line: line.product_id.type == "service" - or not line.order_id.invoice_policy + or line.order_id.invoice_policy == "product" or line.order_id.invoice_policy == line.product_id.invoice_policy or line.state not in ["sale", "done"] - or not line.order_id.invoice_policy_required ) super(SaleOrderLine, other_lines)._compute_untaxed_amount_to_invoice() - for line in self - other_lines: - invoice_policy = line.order_id.invoice_policy - amount_to_invoice = 0.0 - price_subtotal = 0.0 - uom_qty_to_consider = ( - line.qty_delivered - if invoice_policy == "delivery" - else line.product_uom_qty - ) - price_reduce = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - price_subtotal = price_reduce * uom_qty_to_consider - if len(line.tax_id.filtered(lambda tax: tax.price_include)) > 0: - price_subtotal = line.tax_id.compute_all( - price_reduce, - currency=line.currency_id, - quantity=uom_qty_to_consider, - product=line.product_id, - partner=line.order_id.partner_shipping_id, - )["total_excluded"] - inv_lines = line._get_invoice_lines() - if any(inv_lines.mapped(lambda l: l.discount != line.discount)): - amount = 0 - for inv_line in inv_lines: - if ( - len(inv_line.tax_ids.filtered(lambda tax: tax.price_include)) - > 0 - ): - amount += inv_line.tax_ids.compute_all( - inv_line.currency_id._convert( - inv_line.price_unit, - line.currency_id, - line.company_id, - inv_line.date or fields.Date.today(), - round=False, - ) - * inv_line.quantity - )["total_excluded"] - else: - amount += ( - inv_line.currency_id._convert( - inv_line.price_unit, - line.currency_id, - line.company_id, - inv_line.date or fields.Date.today(), - round=False, - ) - * inv_line.quantity - ) - amount_to_invoice = max(price_subtotal - amount, 0) - else: - amount_to_invoice = price_subtotal - line.untaxed_amount_invoiced - line.untaxed_amount_to_invoice = amount_to_invoice - return True + for lines in (self - other_lines).partition("order_id.invoice_policy").values(): + with self._sale_invoice_policy(lines): + super(SaleOrderLine, lines)._compute_untaxed_amount_to_invoice() + return diff --git a/sale_invoice_policy/readme/CONFIGURE.rst b/sale_invoice_policy/readme/CONFIGURE.rst new file mode 100644 index 00000000000..b6d3330f669 --- /dev/null +++ b/sale_invoice_policy/readme/CONFIGURE.rst @@ -0,0 +1,2 @@ +* Go to Sale > Configuration > Settings > Sale Invoice Policy +* Choose the one that fits your needs. diff --git a/sale_invoice_policy/readme/CONTEXT.rst b/sale_invoice_policy/readme/CONTEXT.rst new file mode 100644 index 00000000000..0807e810ac4 --- /dev/null +++ b/sale_invoice_policy/readme/CONTEXT.rst @@ -0,0 +1,13 @@ +In Odoo, products have their own invoicing policy that can be: + +- Invoicing on ordered quantities +- Invoicing on ordered quantities + +Following that configuration, when trying to create invoices from +sale orders, each line of product will apply its invoicing policy. + +In some cases, user needs to apply an invoicing policy on a whole +sale order. + +The solution proposed here is to add an invoicing policy on +sale order level. diff --git a/sale_invoice_policy/readme/CONTRIBUTORS.rst b/sale_invoice_policy/readme/CONTRIBUTORS.rst index c3ac969909f..9a8c5d402bf 100644 --- a/sale_invoice_policy/readme/CONTRIBUTORS.rst +++ b/sale_invoice_policy/readme/CONTRIBUTORS.rst @@ -5,3 +5,4 @@ * Luis J. Salvatierra * Alejandro Ji Cheung * Ioan Galan +* Laurent Mignon \ No newline at end of file diff --git a/sale_invoice_policy/readme/DESCRIPTION.rst b/sale_invoice_policy/readme/DESCRIPTION.rst index 83b3bae0e62..f94c98aa36a 100644 --- a/sale_invoice_policy/readme/DESCRIPTION.rst +++ b/sale_invoice_policy/readme/DESCRIPTION.rst @@ -1,2 +1,17 @@ -This modules helps to get Invoicing Policy on Sale Order Level without -breaking behaviour (as it is defined from >= v10 on product level). +This module adds an invoicing policy on sale order level in order to +apply that invoicing policy on the whole sale order. + +That invoicing policy can take three values: + +- Products Invoicing Policy: The sale order will follow the standard + behavior and apply the policy depending on products configurations. +- Ordered Quantities: The sale order will invoice the ordered quantities. +- Delivered Quantities: The sale order will invoice the delivered quantities. + +Following the chosen policy, the quantity to invoice and the +amount to invoice on each line will be computed accordingly. +Note that the sale order policy will affect only storable products. + +You will be able also to define a default invoicing policy +(globally per company) +that can be different than the default invoicing policy for new products. diff --git a/sale_invoice_policy/readme/USAGE.rst b/sale_invoice_policy/readme/USAGE.rst index b62785e91ad..3796bdeb6eb 100644 --- a/sale_invoice_policy/readme/USAGE.rst +++ b/sale_invoice_policy/readme/USAGE.rst @@ -1,4 +1,3 @@ * Create Sale Order -* Select Invoicing Policy on Sale Order or let it void -* Either the policy selected on Sale Order would be used, either if not - filled in, the policy would be chosen from product configuration +* Select Invoicing Policy on Sale Order or let it on Products Invoicing Policy +* The created invoices will use the configuration on sale order. diff --git a/sale_invoice_policy/static/description/index.html b/sale_invoice_policy/static/description/index.html index e5e502605c7..1caf1b36faa 100644 --- a/sale_invoice_policy/static/description/index.html +++ b/sale_invoice_policy/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -369,32 +370,67 @@

Sale invoice Policy

!! source digest: sha256:c97ec053f6a06fcb824861d16be9e31cc8a53549f47a0f38b6f54f534d09138b !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runboat

-

This modules helps to get Invoicing Policy on Sale Order Level without -breaking behaviour (as it is defined from >= v10 on product level).

+

This module adds an invoicing policy on sale order level in order to +apply that invoicing policy on the whole sale order.

+

That invoicing policy can take three values:

+
    +
  • Products Invoicing Policy: The sale order will follow the standard +behavior and apply the policy depending on products configurations.
  • +
  • Ordered Quantities: The sale order will invoice the ordered quantities.
  • +
  • Delivered Quantities: The sale order will invoice the delivered quantities.
  • +
+

Following the chosen policy, the quantity to invoice and the +amount to invoice on each line will be computed accordingly. +Note that the sale order policy will affect only storable products.

+

You will be able also to define a default invoicing policy +(globally per company) +that can be different than the default invoicing policy for new products.

Table of contents

+
+

Use Cases / Context

+

In Odoo, products have their own invoicing policy that can be:

+
    +
  • Invoicing on ordered quantities
  • +
  • Invoicing on ordered quantities
  • +
+

Following that configuration, when trying to create invoices from +sale orders, each line of product will apply its invoicing policy.

+

In some cases, user needs to apply an invoicing policy on a whole +sale order.

+

The solution proposed here is to add an invoicing policy on +sale order level.

+
+
+

Configuration

+
    +
  • Go to Sale > Configuration > Settings > Sale Invoice Policy
  • +
  • Choose the one that fits your needs.
  • +
+
-

Usage

+

Usage

  • Create Sale Order
  • -
  • Select Invoicing Policy on Sale Order or let it void
  • -
  • Either the policy selected on Sale Order would be used, either if not -filled in, the policy would be chosen from product configuration
  • +
  • Select Invoicing Policy on Sale Order or let it on Products Invoicing Policy
  • +
  • The created invoices will use the configuration on sale order.
-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -402,15 +438,15 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • ACSONE SA/NV
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/sale_invoice_policy/tests/test_sale_invoice_policy.py b/sale_invoice_policy/tests/test_sale_invoice_policy.py index bcd2bebd071..9d94bae1b8d 100644 --- a/sale_invoice_policy/tests/test_sale_invoice_policy.py +++ b/sale_invoice_policy/tests/test_sale_invoice_policy.py @@ -26,9 +26,6 @@ def setUpClass(cls): def test_sale_order_invoice_order(self): """Test invoicing based on ordered quantities""" - settings = self.env["res.config.settings"].create({}) - settings.sale_invoice_policy_required = True - settings.execute() so = self.env["sale.order"].create( { "partner_id": self.env.ref("base.res_partner_2").id, @@ -39,8 +36,10 @@ def test_sale_order_invoice_order(self): "invoice_policy": "order", } ) - self.assertTrue(so.invoice_policy_required) - self.assertTrue(so.invoice_policy == "order") + self.assertEqual(so.invoice_policy, "order") + + # SO is not confirmed yet + self.assertEqual([0.0, 0.0], so.order_line.mapped("untaxed_amount_to_invoice")) so.action_confirm() @@ -61,9 +60,7 @@ def test_sale_order_invoice_order(self): def test_sale_order_invoice_deliver(self): """Test invoicing based on delivered quantities""" - settings = self.env["res.config.settings"].create({}) - settings.sale_invoice_policy_required = True - settings.execute() + self.assertEqual("order", self.product.invoice_policy) so = self.env["sale.order"].create( { "partner_id": self.env.ref("base.res_partner_2").id, @@ -74,11 +71,16 @@ def test_sale_order_invoice_deliver(self): ], } ) - self.assertTrue(so.invoice_policy_required) - self.assertTrue(so.invoice_policy == "delivery") + self.assertEqual(so.invoice_policy, "delivery") + + # SO is not confirmed yet + self.assertEqual([0.0, 0.0], so.order_line.mapped("untaxed_amount_to_invoice")) so.action_confirm() + # SO is not delivered + self.assertEqual([0.0, 0.0], so.order_line.mapped("untaxed_amount_to_invoice")) + self.assertEqual(len(so.picking_ids), 1) picking = so.picking_ids @@ -102,26 +104,65 @@ def test_sale_order_invoice_deliver(self): self.assertEqual(so_line.qty_to_invoice, 2) self.assertEqual(so_line.invoice_status, "to invoice") + self.assertEqual(40.0, so_line.untaxed_amount_to_invoice) + # Check that product has still its original invoice policy + self.assertEqual("order", self.product.invoice_policy) + so_line = so.order_line[1] self.assertEqual(so_line.qty_to_invoice, 3) self.assertEqual(so_line.invoice_status, "to invoice") + self.assertEqual(135.0, so_line.untaxed_amount_to_invoice) + def test_settings(self): # delivery policy is the default settings = self.env["res.config.settings"].create({}) - settings.default_invoice_policy = "delivery" - settings.sale_invoice_policy_required = True + settings.sale_default_invoice_policy = "delivery" settings.execute() so = self.env["sale.order"].create( {"partner_id": self.env.ref("base.res_partner_2").id} ) self.assertEqual(so.invoice_policy, "delivery") - self.assertTrue(so.invoice_policy_required) # order policy is the default - settings.default_invoice_policy = "order" + settings.sale_default_invoice_policy = "order" settings.execute() so = self.env["sale.order"].create( {"partner_id": self.env.ref("base.res_partner_2").id} ) self.assertEqual(so.invoice_policy, "order") - self.assertTrue(so.invoice_policy_required) + + def test_context_manager_exception(self): + """Check the exception is well managed when called with several invoice policies""" + self.assertEqual("order", self.product.invoice_policy) + so = self.env["sale.order"].create( + { + "partner_id": self.env.ref("base.res_partner_2").id, + "invoice_policy": "delivery", + "order_line": [ + (0, 0, {"product_id": self.product.id, "product_uom_qty": 2.0}), + (0, 0, {"product_id": self.product2.id, "product_uom_qty": 3.0}), + ], + } + ) + + so2 = self.env["sale.order"].create( + { + "partner_id": self.env.ref("base.res_partner_2").id, + "invoice_policy": "order", + "order_line": [ + (0, 0, {"product_id": self.product.id, "product_uom_qty": 2.0}), + (0, 0, {"product_id": self.product2.id, "product_uom_qty": 3.0}), + ], + } + ) + lines = (so | so2).order_line + with self.assertRaises(Exception) as exc: + with self.env["sale.order.line"]._sale_invoice_policy( + lines + ): # pragma: no cover + pass + self.assertEqual( + "The method _sale_invoice_policy() must be called with lines sharing " + "the same invoice policy", + exc.exception.args[0], + ) diff --git a/sale_invoice_policy/views/res_config_settings_view.xml b/sale_invoice_policy/views/res_config_settings_view.xml index c79d78633ff..cba89c33cec 100644 --- a/sale_invoice_policy/views/res_config_settings_view.xml +++ b/sale_invoice_policy/views/res_config_settings_view.xml @@ -10,16 +10,18 @@ class="col-12 col-lg-6 o_setting_box" id="sale_invoice_policy_setting" > -
- -
+
diff --git a/sale_invoice_policy/views/sale_view.xml b/sale_invoice_policy/views/sale_view.xml index 85e0c03209a..9fb0e0024a5 100644 --- a/sale_invoice_policy/views/sale_view.xml +++ b/sale_invoice_policy/views/sale_view.xml @@ -7,11 +7,7 @@ 50 - - +