diff --git a/sale_order_line_base_price_and_has_discount/README.rst b/sale_order_line_base_price_and_has_discount/README.rst new file mode 100644 index 00000000000..1afaad08c66 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/README.rst @@ -0,0 +1,111 @@ +===================================== +Sale Order Line Base And Has Discount +===================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6c1637150f2c652f2bb11cce96c59bddf6dec18dd6b7c48e1f8e1f466bd0171d + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/18.0/sale_order_line_base_price_and_has_discount + :alt: OCA/sale-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sale_order_line_base_price_and_has_discount + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to display the original price before pricelist +application. This can be completed by the +'sale_report_crossed_out_original_price' module to display crossed +prices on sale order report. + +In some cases, prices are based on several pricelists based on formulas +and we want to display the base price in that case too. This beahaviour +is configurable. + +|image| + +.. |image| image:: https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_order_line_base_price_and_has_discount/static/description/base_price.png + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +To retrieve the same behaviour as on e-commerce website, users want to +display the original price on sale order lines when price list are +computing a discounted price. + +Configuration +============= + +- In the Sale Order form, enable the display of 'Base Price' field. +- Go to Sales > Configuration > Settings > Pricing > Pricelists and + choose a base price computation method. +- By default, the base price will reflect the computation from discount + list price only. + +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 +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Denis Roussel denis.roussel@acsone.eu + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +.. |maintainer-rousseldenis| image:: https://github.com/rousseldenis.png?size=40px + :target: https://github.com/rousseldenis + :alt: rousseldenis + +Current `maintainer `__: + +|maintainer-rousseldenis| + +This module is part of the `OCA/sale-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_order_line_base_price_and_has_discount/__init__.py b/sale_order_line_base_price_and_has_discount/__init__.py new file mode 100644 index 00000000000..6d58305f5dd --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import pre_init_hook diff --git a/sale_order_line_base_price_and_has_discount/__manifest__.py b/sale_order_line_base_price_and_has_discount/__manifest__.py new file mode 100644 index 00000000000..d6e6e7811f8 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sale Order Line Base And Has Discount", + "summary": """This module allows to put base price and has_discount fields on + sale order line""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "maintainers": ["rousseldenis"], + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sale-workflow", + "depends": [ + "sale", + ], + "data": ["views/sale_order.xml", "views/res_config_settings.xml"], + "pre_init_hook": "pre_init_hook", +} diff --git a/sale_order_line_base_price_and_has_discount/hooks.py b/sale_order_line_base_price_and_has_discount/hooks.py new file mode 100644 index 00000000000..0f4b39f605d --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/hooks.py @@ -0,0 +1,20 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from openupgradelib import openupgrade + + +def pre_init_hook(env): + fields_spec = [ + ( + "base_price", + "sale.order.line", + False, + "float", + "float", + "sale_order_line_base_price_and_has_discount", + ) + ] + openupgrade.add_fields(env, field_spec=fields_spec) + + # Don't fill in the base price as it is impossible to be + # sure of the pricelists configuration before. diff --git a/sale_order_line_base_price_and_has_discount/models/__init__.py b/sale_order_line_base_price_and_has_discount/models/__init__.py new file mode 100644 index 00000000000..2bebd74f007 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/models/__init__.py @@ -0,0 +1,4 @@ +from . import sale_order_line +from . import product_pricelist_item +from . import res_company +from . import res_config_settings diff --git a/sale_order_line_base_price_and_has_discount/models/product_pricelist_item.py b/sale_order_line_base_price_and_has_discount/models/product_pricelist_item.py new file mode 100644 index 00000000000..4b6df8a290c --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/models/product_pricelist_item.py @@ -0,0 +1,29 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, models + + +class ProductPricelistItem(models.Model): + _inherit = "product.pricelist.item" + + @api.model + def _get_compute_price_methods_for_base_price_pricelist(self): + return ["percentage", "formula"] + + def _compute_price_before_pricelist(self, *args, **kwargs): + """Compute the base price of the lowest pricelist rule, + taking into account percentage and formula items + """ + pricelist_item = self + methods = self._get_compute_price_methods_for_base_price_pricelist() + while pricelist_item.base == "pricelist": + rule_id = pricelist_item.base_pricelist_id._get_product_rule( + *args, **kwargs + ) + rule_pricelist_item = self.env["product.pricelist.item"].browse(rule_id) + if rule_pricelist_item and rule_pricelist_item.compute_price in methods: + pricelist_item = rule_pricelist_item + else: + break + + return pricelist_item._compute_base_price(*args, **kwargs) diff --git a/sale_order_line_base_price_and_has_discount/models/res_company.py b/sale_order_line_base_price_and_has_discount/models/res_company.py new file mode 100644 index 00000000000..d2edbb8ee4e --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/models/res_company.py @@ -0,0 +1,19 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + display_base_price_method = fields.Selection( + selection=[ + ("discount", "Discounts only"), + ("discount_formula", "Discount and Formula"), + ], + default="discount", + required=True, + help="Choose the method to display the base price " + "(to include or not formula based pricelists)", + ) diff --git a/sale_order_line_base_price_and_has_discount/models/res_config_settings.py b/sale_order_line_base_price_and_has_discount/models/res_config_settings.py new file mode 100644 index 00000000000..6a6f0fcf927 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/models/res_config_settings.py @@ -0,0 +1,13 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + display_base_price_method = fields.Selection( + related="company_id.display_base_price_method", + readonly=False, + ) diff --git a/sale_order_line_base_price_and_has_discount/models/sale_order_line.py b/sale_order_line_base_price_and_has_discount/models/sale_order_line.py new file mode 100644 index 00000000000..30c4a08ad1f --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/models/sale_order_line.py @@ -0,0 +1,81 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models +from odoo.tools import float_compare + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + has_discount_price = fields.Boolean( + compute="_compute_has_discount_price", + ) + base_price = fields.Float( + compute="_compute_base_price", + digits="Product Price", + store=True, + precompute=True, + ) + base_price_discount = fields.Float( + string="Base Price Disc. %", + digits="Discount", + compute="_compute_base_price_discount", + ) + + @api.depends("base_price", "price_unit", "has_discount_price") + def _compute_base_price_discount(self): + lines_with_product = self.filtered("has_discount_price") + for line in lines_with_product: + line.base_price_discount = ( + ((line.base_price - line.price_unit) / line.base_price) * 100 + if line.base_price + else 0.0 + ) + (self - lines_with_product).base_price_discount = 0.0 + + @api.depends("price_unit", "base_price") + def _compute_has_discount_price(self): + """ + Computing the boolean field based on : + + - base price + - price unit + """ + lines_with_product = self.filtered("base_price") + precision = self.env["decimal.precision"].precision_get("Product Price") + for line in lines_with_product: + pricelist_price = line.price_unit + base_price = line.base_price + line.has_discount_price = bool( + float_compare(pricelist_price, base_price, precision_digits=precision) + < 0 + ) + (self - lines_with_product).has_discount_price = False + + @api.depends("price_unit", "product_id") + def _compute_base_price(self): + lines_with_product = self.filtered("product_id") + for line in lines_with_product: + if line.company_id.display_base_price_method == "discount": + base_price = line._get_pricelist_price_before_discount() + else: + base_price = line._get_pricelist_price_before_pricelist() + line.base_price = base_price + (self - lines_with_product).base_price = False + + def _get_pricelist_price_before_pricelist(self): + """Compute the price used as base for the pricelist price computation. + + :return: the product sales price in the order currency (without taxes) + :rtype: float + """ + self.ensure_one() + self.product_id.ensure_one() + + return self.pricelist_item_id._compute_price_before_pricelist( + product=self.product_id.with_context(**self._get_product_price_context()), + quantity=self.product_uom_qty or 1.0, + uom=self.product_uom, + date=self._get_order_date(), + currency=self.currency_id, + ) diff --git a/sale_order_line_base_price_and_has_discount/pyproject.toml b/sale_order_line_base_price_and_has_discount/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/sale_order_line_base_price_and_has_discount/readme/CONFIGURE.md b/sale_order_line_base_price_and_has_discount/readme/CONFIGURE.md new file mode 100644 index 00000000000..5509e3f0800 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/readme/CONFIGURE.md @@ -0,0 +1,5 @@ +- In the Sale Order form, enable the display of 'Base Price' field. +- Go to Sales > Configuration > Settings > Pricing > Pricelists + and choose a base price computation method. +- By default, the base price will reflect the computation from discount + list price only. diff --git a/sale_order_line_base_price_and_has_discount/readme/CONTEXT.md b/sale_order_line_base_price_and_has_discount/readme/CONTEXT.md new file mode 100644 index 00000000000..4a72b0b3be2 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/readme/CONTEXT.md @@ -0,0 +1,3 @@ +To retrieve the same behaviour as on e-commerce website, users want to display +the original price on sale order lines when price list are computing a discounted +price. diff --git a/sale_order_line_base_price_and_has_discount/readme/CONTRIBUTORS.md b/sale_order_line_base_price_and_has_discount/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..c60a45e3b9b --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Denis Roussel \ No newline at end of file diff --git a/sale_order_line_base_price_and_has_discount/readme/DESCRIPTION.md b/sale_order_line_base_price_and_has_discount/readme/DESCRIPTION.md new file mode 100644 index 00000000000..d2487c4d60b --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +This module allows to display the original price before pricelist application. +This can be completed by the 'sale_report_crossed_out_original_price' module to +display crossed prices on sale order report. + +In some cases, prices are based on several pricelists based on formulas and +we want to display the base price in that case too. This beahaviour is +configurable. + +![image](../static/description/base_price.png) diff --git a/sale_order_line_base_price_and_has_discount/static/description/base_price.png b/sale_order_line_base_price_and_has_discount/static/description/base_price.png new file mode 100644 index 00000000000..aa90aeb7e79 Binary files /dev/null and b/sale_order_line_base_price_and_has_discount/static/description/base_price.png differ diff --git a/sale_order_line_base_price_and_has_discount/static/description/icon.png b/sale_order_line_base_price_and_has_discount/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/sale_order_line_base_price_and_has_discount/static/description/icon.png differ diff --git a/sale_order_line_base_price_and_has_discount/static/description/index.html b/sale_order_line_base_price_and_has_discount/static/description/index.html new file mode 100644 index 00000000000..61d8c16baa8 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/static/description/index.html @@ -0,0 +1,447 @@ + + + + + +Sale Order Line Base And Has Discount + + + +
+

Sale Order Line Base And Has Discount

+ + +

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

+

This module allows to display the original price before pricelist +application. This can be completed by the +‘sale_report_crossed_out_original_price’ module to display crossed +prices on sale order report.

+

In some cases, prices are based on several pricelists based on formulas +and we want to display the base price in that case too. This beahaviour +is configurable.

+

image

+

Table of contents

+ +
+

Use Cases / Context

+

To retrieve the same behaviour as on e-commerce website, users want to +display the original price on sale order lines when price list are +computing a discounted price.

+
+
+

Configuration

+
    +
  • In the Sale Order form, enable the display of ‘Base Price’ field.
  • +
  • Go to Sales > Configuration > Settings > Pricing > Pricelists and +choose a base price computation method.
  • +
  • By default, the base price will reflect the computation from discount +list price only.
  • +
+
+
+

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 +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+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.

+

Current maintainer:

+

rousseldenis

+

This module is part of the OCA/sale-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/sale_order_line_base_price_and_has_discount/tests/__init__.py b/sale_order_line_base_price_and_has_discount/tests/__init__.py new file mode 100644 index 00000000000..3dafb94a46d --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_line_price diff --git a/sale_order_line_base_price_and_has_discount/tests/test_sale_line_price.py b/sale_order_line_base_price_and_has_discount/tests/test_sale_line_price.py new file mode 100644 index 00000000000..42b53c58714 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/tests/test_sale_line_price.py @@ -0,0 +1,103 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.fields import Command + +from odoo.addons.base.tests.common import BaseCommon + + +class TestSaleOrderLine(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create( + { + "name": "Test", + } + ) + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "list_price": "10.0", + } + ) + + cls.pricelist = cls.env["product.pricelist"].create( + { + "name": "Test", + "item_ids": [ + Command.create( + { + "applied_on": "0_product_variant", + "product_id": cls.product.id, + "compute_price": "formula", + "base": "list_price", + "price_discount": 10, + } + ) + ], + } + ) + + cls.pricelist_pro = cls.env["product.pricelist"].create( + { + "name": "Pro", + "item_ids": [ + Command.create( + { + "applied_on": "3_global", + # "product_id": cls.product.id, + "compute_price": "formula", + "base": "pricelist", + "price_discount": 10, + "base_pricelist_id": cls.pricelist.id, + } + ) + ], + } + ) + + def test_sale_price(self): + order = self.env["sale.order"].create( + { + "pricelist_id": self.pricelist.id, + "partner_id": self.partner.id, + "order_line": [ + Command.create( + { + "product_id": self.product.id, + } + ) + ], + } + ) + self.assertEqual(10.0, order.order_line.base_price) + self.assertEqual( + 9.0, + order.order_line.price_unit, + ) + self.assertTrue(order.order_line.has_discount_price) + self.assertEqual(order.order_line.base_price_discount, 10.0) + + def test_sale_price_on_pricelist_pro(self): + self.env.company.display_base_price_method = "discount_formula" + order = self.env["sale.order"].create( + { + "pricelist_id": self.pricelist_pro.id, + "partner_id": self.partner.id, + "order_line": [ + Command.create( + { + "product_id": self.product.id, + } + ) + ], + } + ) + self.assertEqual(10.0, order.order_line.base_price) + self.assertEqual( + 8.1, + order.order_line.price_unit, + ) + self.assertTrue(order.order_line.has_discount_price) + # Check the dubble discount + self.assertEqual(order.order_line.base_price_discount, 19.0) diff --git a/sale_order_line_base_price_and_has_discount/views/res_config_settings.xml b/sale_order_line_base_price_and_has_discount/views/res_config_settings.xml new file mode 100644 index 00000000000..ce93b5a44f7 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/views/res_config_settings.xml @@ -0,0 +1,25 @@ + + + + + res.config.settings.view.form.inherit.sale + res.config.settings + + + + + + + + diff --git a/sale_order_line_base_price_and_has_discount/views/sale_order.xml b/sale_order_line_base_price_and_has_discount/views/sale_order.xml new file mode 100644 index 00000000000..d79072339a1 --- /dev/null +++ b/sale_order_line_base_price_and_has_discount/views/sale_order.xml @@ -0,0 +1,28 @@ + + + + + sale.order + + + + + + + + +