diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c51e4deb6d1..69db50ec5dc7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,6 @@ exclude: | (?x) # NOT INSTALLABLE ADDONS - ^sale_invoice_policy/| # END NOT INSTALLABLE ADDONS # Files and folders generated by bots, to avoid loops ^setup/|/static/description/index\.html$| diff --git a/sale_invoice_policy/README.rst b/sale_invoice_policy/README.rst index d5635692f60b..844708c3e391 100644 --- a/sale_invoice_policy/README.rst +++ b/sale_invoice_policy/README.rst @@ -7,7 +7,7 @@ Sale invoice Policy !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:3c1505e28f76dfda1fccdabb3cce63bab46df277823c4236d77da275a16a5a3c + !! source digest: sha256:f7cef4d695f93f0893a61a5db0dc5ea532311d04314a81189214b7a49d43faed !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -70,6 +70,7 @@ Contributors * Denis Roussel * Alexei Rivera * Luis J. Salvatierra +* Alejandro Ji Cheung Maintainers ~~~~~~~~~~~ diff --git a/sale_invoice_policy/__init__.py b/sale_invoice_policy/__init__.py index a0cb2972d9e8..0650744f6bc6 100644 --- a/sale_invoice_policy/__init__.py +++ b/sale_invoice_policy/__init__.py @@ -1,2 +1 @@ from . import models -from .post_init_hook import post_init_hook diff --git a/sale_invoice_policy/__manifest__.py b/sale_invoice_policy/__manifest__.py index 654dfbbef0c2..be67a458f41b 100644 --- a/sale_invoice_policy/__manifest__.py +++ b/sale_invoice_policy/__manifest__.py @@ -12,10 +12,7 @@ "license": "AGPL-3", "depends": ["sale_stock"], "data": [ - "views/product_template_view.xml", "views/res_config_settings_view.xml", "views/sale_view.xml", ], - "installable": False, - "post_init_hook": "post_init_hook", } diff --git a/sale_invoice_policy/models/product_template.py b/sale_invoice_policy/models/product_template.py index 381cf99d38a5..7785f68b8e27 100644 --- a/sale_invoice_policy/models/product_template.py +++ b/sale_invoice_policy/models/product_template.py @@ -1,54 +1,18 @@ # Copyright 2017 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 ProductTemplate(models.Model): _inherit = "product.template" - default_invoice_policy = fields.Selection( - [("order", "Ordered quantities"), ("delivery", "Delivered quantities")], - string="Default Invoicing Policy", - 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).", - default=lambda self: self.env["ir.default"].get( - "res.config.settings", "default_invoice_policy" - ), - ) + def _default_invoice_policy(self): + return ( + self.env["res.config.settings"] + .sudo() + .default_get(["default_invoice_policy"]) + .get("default_invoice_policy", False) + ) - invoice_policy = fields.Selection( - compute="_compute_invoice_policy", - store=False, - readonly=True, - search="_search_invoice_policy", - inverse="_inverse_invoice_policy", - precompute=False, - ) - - def _inverse_invoice_policy(self): - for template in self.filtered("invoice_policy"): - template.default_invoice_policy = template.invoice_policy - - @api.depends("detailed_type", "default_invoice_policy") - @api.depends_context("invoice_policy") - def _compute_invoice_policy(self): - """ - Apply the invoice_policy given by context (if exist) otherwise use the - default invoice policy given by the field with this same name. - If the product is type = 'service', we don't have to apply the invoice - policy given by the context. - :return: - """ - invoice_policy = self.env.context.get("invoice_policy") - for tmpl in self: - if tmpl.type != "service" and invoice_policy: - tmpl.invoice_policy = invoice_policy - else: - tmpl.invoice_policy = tmpl.default_invoice_policy - - @api.model - def _search_invoice_policy(self, operator, value): - return [("default_invoice_policy", operator, value)] + invoice_policy = fields.Selection(default=_default_invoice_policy) diff --git a/sale_invoice_policy/models/res_config_settings.py b/sale_invoice_policy/models/res_config_settings.py index e81cfc1d37c6..7e5a6c9539af 100644 --- a/sale_invoice_policy/models/res_config_settings.py +++ b/sale_invoice_policy/models/res_config_settings.py @@ -7,11 +7,6 @@ class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" - sale_default_invoice_policy = fields.Selection( - related="default_invoice_policy", - string="Default Sale Invoice Policy", - readonly=True, - ) sale_invoice_policy_required = fields.Boolean( help="This makes Invoice Policy required on Sale Orders" ) @@ -36,9 +31,4 @@ def set_values(self): "sale_invoice_policy_required", self.sale_invoice_policy_required, ) - ir_default_obj.set( - "res.config.settings", - "sale_default_invoice_policy", - self.sale_default_invoice_policy, - ) return True diff --git a/sale_invoice_policy/models/sale_order.py b/sale_invoice_policy/models/sale_order.py index 00f3ffca4bf8..b9c2a5b1296f 100644 --- a/sale_invoice_policy/models/sale_order.py +++ b/sale_invoice_policy/models/sale_order.py @@ -27,17 +27,23 @@ class SaleOrder(models.Model): @api.model def default_get(self, fields_list): res = super().default_get(fields_list) - default_sale_invoice_policy = self.env["ir.default"].get( - "res.config.settings", "sale_default_invoice_policy" + 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_sale_invoice_policy}) + res.update({"invoice_policy": default_invoice_policy}) return res @api.depends("partner_id") def _compute_invoice_policy_required(self): - invoice_policy_required = self.env["ir.default"].get( - "res.config.settings", "sale_invoice_policy_required" + 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 diff --git a/sale_invoice_policy/models/sale_order_line.py b/sale_invoice_policy/models/sale_order_line.py index 98ca923e0fdc..92d13d70ef8d 100644 --- a/sale_invoice_policy/models/sale_order_line.py +++ b/sale_invoice_policy/models/sale_order_line.py @@ -5,64 +5,25 @@ class SaleOrderLine(models.Model): - _inherit = "sale.order.line" @api.depends( "qty_invoiced", "qty_delivered", "product_uom_qty", - "order_id.state", - "order_id.invoice_policy", - ) - def _compute_qty_to_invoice(self): - invoice_policies = set(self.mapped("order_id.invoice_policy")) - line_by_id = {line.id: line for line in self} - done_lines = self.env["sale.order.line"].browse() - for invoice_policy in invoice_policies: - so_lines = ( - self.with_context(invoice_policy=invoice_policy) - .filtered(lambda x, p=invoice_policy: x.order_id.invoice_policy == p) - .with_prefetch() - ) - if so_lines: - done_lines |= so_lines - super(SaleOrderLine, so_lines)._compute_qty_to_invoice() - for line in so_lines: - # due to the change of context in compute methods, - # assign the value in the modified context to self - line_by_id[line.id].qty_to_invoice = line.qty_to_invoice - # Not to break function if (it could not happen) some records - # were not in so_lines - super(SaleOrderLine, self - done_lines)._compute_qty_to_invoice() - return True - - @api.depends( "state", - "product_uom_qty", - "qty_delivered", - "qty_to_invoice", - "qty_invoiced", "order_id.invoice_policy", ) - def _compute_invoice_status(self): - invoice_policies = set(self.mapped("order_id.invoice_policy")) - line_by_id = {line.id: line for line in self} - done_lines = self.env["sale.order.line"].browse() - for invoice_policy in invoice_policies: - so_lines = ( - self.with_context(invoice_policy=invoice_policy) - .filtered(lambda x, p=invoice_policy: x.order_id.invoice_policy == p) - .with_prefetch() - ) - done_lines |= so_lines - if so_lines: - super(SaleOrderLine, so_lines)._compute_invoice_status() - for line in so_lines: - # due to the change of context in compute methods, - # assign the value in the modified context to self - line_by_id[line.id].invoice_status = line.invoice_status - # Not to break function if (it could not happen) some records - # were not in so_lines - super(SaleOrderLine, self - done_lines)._compute_invoice_status() + def _compute_qty_to_invoice(self): + other_lines = self.env["sale.order.line"] + for line in self: + if line.product_id.type == "service" or not line.order_id.invoice_policy: + other_lines |= line + super(SaleOrderLine, other_lines)._compute_qty_to_invoice() + for line in self - other_lines: + invoice_policy = line.order_id.invoice_policy + if invoice_policy == "order": + line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced + else: + line.qty_to_invoice = line.qty_delivered - line.qty_invoiced return True diff --git a/sale_invoice_policy/post_init_hook.py b/sale_invoice_policy/post_init_hook.py deleted file mode 100644 index b40b4e50cd67..000000000000 --- a/sale_invoice_policy/post_init_hook.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2017 ACSONE SA/NV () -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - - -def post_init_hook(cr, registry): - """ - As we change the invoice policy to computed field, we must initialize - the default policy with original values - """ - - query = """ - UPDATE product_template - SET default_invoice_policy = invoice_policy - WHERE invoice_policy IS NOT NULL""" - cr.execute(query) diff --git a/sale_invoice_policy/readme/CONTRIBUTORS.rst b/sale_invoice_policy/readme/CONTRIBUTORS.rst index 453e9308379d..6fa497dc41b9 100644 --- a/sale_invoice_policy/readme/CONTRIBUTORS.rst +++ b/sale_invoice_policy/readme/CONTRIBUTORS.rst @@ -3,3 +3,4 @@ * Denis Roussel * Alexei Rivera * Luis J. Salvatierra +* Alejandro Ji Cheung diff --git a/sale_invoice_policy/static/description/index.html b/sale_invoice_policy/static/description/index.html index f587604e06aa..42760a096674 100644 --- a/sale_invoice_policy/static/description/index.html +++ b/sale_invoice_policy/static/description/index.html @@ -367,7 +367,7 @@

Sale invoice Policy

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:3c1505e28f76dfda1fccdabb3cce63bab46df277823c4236d77da275a16a5a3c +!! source digest: sha256:f7cef4d695f93f0893a61a5db0dc5ea532311d04314a81189214b7a49d43faed !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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 @@ -418,6 +418,7 @@

Contributors

  • Denis Roussel <denis.roussel@acsone.eu>
  • Alexei Rivera <arivera@archeti.com>
  • Luis J. Salvatierra <luis.salvatierra@factorlibre.com>
  • +
  • Alejandro Ji Cheung <alejandro.jicheung@factorlibre.com>
  • diff --git a/sale_invoice_policy/tests/test_sale_invoice_policy.py b/sale_invoice_policy/tests/test_sale_invoice_policy.py index f4a250e9ce11..7d939f779933 100644 --- a/sale_invoice_policy/tests/test_sale_invoice_policy.py +++ b/sale_invoice_policy/tests/test_sale_invoice_policy.py @@ -26,6 +26,9 @@ 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, @@ -36,6 +39,9 @@ def test_sale_order_invoice_order(self): "invoice_policy": "order", } ) + so._compute_invoice_policy_required() + self.assertTrue(so.invoice_policy_required) + self.assertTrue(so.invoice_policy == "order") so.action_confirm() @@ -47,12 +53,18 @@ def test_sale_order_invoice_order(self): so_line = so.order_line[0] self.assertEqual(so_line.qty_to_invoice, 2) self.assertEqual(so_line.invoice_status, "to invoice") + self.assertEqual(so_line.product_id.invoice_policy, "order") + so_line = so.order_line[1] self.assertEqual(so_line.qty_to_invoice, 3) self.assertEqual(so_line.invoice_status, "to invoice") + self.assertEqual(so_line.product_id.invoice_policy, "order") 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() so = self.env["sale.order"].create( { "partner_id": self.env.ref("base.res_partner_2").id, @@ -63,6 +75,9 @@ def test_sale_order_invoice_deliver(self): ], } ) + so._compute_invoice_policy_required() + self.assertTrue(so.invoice_policy_required) + self.assertTrue(so.invoice_policy == "delivery") so.action_confirm() @@ -88,101 +103,17 @@ def test_sale_order_invoice_deliver(self): so_line = so.order_line[0] self.assertEqual(so_line.qty_to_invoice, 2) self.assertEqual(so_line.invoice_status, "to invoice") + self.assertEqual(so_line.product_id.invoice_policy, "delivery") so_line = so.order_line[1] self.assertEqual(so_line.qty_to_invoice, 3) self.assertEqual(so_line.invoice_status, "to invoice") - - def test_sale_order_invoice_policy_service1(self): - """ - For this test, we check if the invoice policy is correctly updated - (into the product) when the type is 'service'. - The behaviour should be: - - Get the value of the context but if the type is 'service': use the - default_invoice_policy field value - :return: bool - """ - product = self.product3 - invoice_policy = "delivery" - product.write({"default_invoice_policy": invoice_policy}) - self.assertEqual(product.invoice_policy, invoice_policy) - product = product.with_context( - invoice_policy="order", - ) - # Shouldn't be impacted by the context because the type is service - self.assertEqual(product.invoice_policy, invoice_policy) - return True - - def test_sale_order_invoice_policy_service2(self): - """ - For this test, we check if the invoice policy is correctly updated - (into the product) when the type is 'service'. - The behaviour should be: - - Get the value of the context but if the type is 'service': use the - default_invoice_policy field value - :return: bool - """ - product = self.product3 - invoice_policy = "order" - product.write({"default_invoice_policy": invoice_policy}) - self.assertEqual(product.invoice_policy, invoice_policy) - product = product.with_context( - invoice_policy="delivery", - ) - # Shouldn't be impacted by the context because the type is service - self.assertEqual(product.invoice_policy, invoice_policy) - return True - - def test_sale_order_invoice_policy_service3(self): - """ - For this test, we check if the invoice policy is correctly updated - (into the product) when the type is 'service'. - The behaviour should be: - - Get the value of the context but if the type is 'service': use the - default_invoice_policy field value - :return: bool - """ - product = self.product3 - product2 = self.product2 - products = product - products |= product2 - invoice_policy = "order" - products.write({"default_invoice_policy": invoice_policy}) - self.assertEqual(product.invoice_policy, invoice_policy) - self.assertEqual(product2.invoice_policy, invoice_policy) - new_invoice_policy = "delivery" - product = product.with_context( - invoice_policy=new_invoice_policy, - ) - product2 = product2.with_context( - invoice_policy=new_invoice_policy, - ) - # Shouldn't be impacted by the context because the type is service - self.assertEqual(product.invoice_policy, invoice_policy) - # This one is not a service, so it must be impacted by the context - self.assertEqual(product2.invoice_policy, new_invoice_policy) - product = product.with_context( - invoice_policy=invoice_policy, - ) - product2 = product2.with_context( - invoice_policy=invoice_policy, - ) - # Shouldn't be impacted by the context because the type is service - self.assertEqual(product.invoice_policy, invoice_policy) - # This one is not a service, so it must be impacted by the context - self.assertEqual(product2.invoice_policy, invoice_policy) - return True - - def test_inverse_invoice_policy(self): - self.product.default_invoice_policy = "order" - self.assertEqual("order", self.product.invoice_policy) - self.product.invoice_policy = "delivery" - self.assertEqual("delivery", self.product.default_invoice_policy) + self.assertEqual(so_line.product_id.invoice_policy, "delivery") def test_settings(self): # delivery policy is the default settings = self.env["res.config.settings"].create({}) - settings.sale_default_invoice_policy = "delivery" + settings.default_invoice_policy = "delivery" settings.sale_invoice_policy_required = True settings.execute() so = self.env["sale.order"].create( @@ -191,7 +122,7 @@ def test_settings(self): self.assertEqual(so.invoice_policy, "delivery") self.assertTrue(so.invoice_policy_required) # order policy is the default - settings.sale_default_invoice_policy = "order" + settings.default_invoice_policy = "order" settings.execute() so = self.env["sale.order"].create( {"partner_id": self.env.ref("base.res_partner_2").id} diff --git a/sale_invoice_policy/views/product_template_view.xml b/sale_invoice_policy/views/product_template_view.xml deleted file mode 100644 index d06138e69ec9..000000000000 --- a/sale_invoice_policy/views/product_template_view.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - product.template.invoice.policy - product.template - - - - 1 - - - - - - - diff --git a/sale_invoice_policy/views/res_config_settings_view.xml b/sale_invoice_policy/views/res_config_settings_view.xml index 51931203c361..c79d78633ffa 100644 --- a/sale_invoice_policy/views/res_config_settings_view.xml +++ b/sale_invoice_policy/views/res_config_settings_view.xml @@ -6,18 +6,6 @@ -