diff --git a/README.md b/README.md index 3c4089d14..de456f151 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ TODO: add repo description. Available addons ---------------- -addon | version | maintainers | summary +addon | version | maintainers | summary. --- | --- | --- | --- [product_pack](product_pack/) | 16.0.1.1.0 | [![ernestotejeda](https://github.com/ernestotejeda.png?size=30px)](https://github.com/ernestotejeda) | This module allows you to set a product as a Pack [purchase_product_pack](purchase_product_pack/) | 16.0.1.0.0 | | This module allows you to buy product packs diff --git a/product_pack/README.rst b/product_pack/README.rst index 07bb9c346..3741ea303 100644 --- a/product_pack/README.rst +++ b/product_pack/README.rst @@ -7,7 +7,7 @@ Product Pack !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:d6d79cfb1448cba3c8cc68f89101f6b53245f8a695effd2058ca881dac328748 + !! source digest: sha256:6412314c32b470ebaa9ba73edd671e2513bf0b8c9faa4703dcb22faf76692670 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -70,6 +70,21 @@ The options of this field are the followings: components prices. The pack product will be the only one that has price and this one will be the price set in the pack product. ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ +| **Pack type** | **Show components on SO?** | **Sale price** | **Discount** | **Can be modified?** | ++=============================+=============================+=================================+=========================================+======================+ +| **Detailed per components** | Yes, with their prices | Components + Pack | Applies to the price of the pack and | Yes, configurable | +| | | | the components | | ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ +| **Detailed - Totalized** | Yes, with their prices at 0 | Components | Applies to the total (pack + components)| No | ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ +| **Detailed - Ignored** | Yes, with their prices at 0 | Only Pack | Applies to the pack | No | ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ +| **No detailed** | No | Components | Applies to the total (pack + components)| No | ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ + +**Note:** If pricelist enabled, Odoo will display the price according to the corresponding pricelist. In the case of a pricelist with discount policy "Show public price & discount to the customer" keep in mind that the "Non Detailed" and "Detailed - Totalized in main product" packs do not display the discount. + Known issues / Roadmap ====================== @@ -106,6 +121,9 @@ Contributors * Juan José Scarafía * Nicolas Mac Rouillon * Katherine Zaoral + * Bruno Zanotti + * Augusto Weiss + * Nicolas Col * `NaN·TIC `_ * `Tecnativa `_: diff --git a/product_pack/models/__init__.py b/product_pack/models/__init__.py index 626eb1a8f..37bee3edc 100644 --- a/product_pack/models/__init__.py +++ b/product_pack/models/__init__.py @@ -3,3 +3,4 @@ from . import product_pack_line from . import product_product from . import product_template +from . import product_pricelist diff --git a/product_pack/models/product_pack_line.py b/product_pack/models/product_pack_line.py index 5a1057770..4d5c72017 100644 --- a/product_pack/models/product_pack_line.py +++ b/product_pack/models/product_pack_line.py @@ -54,6 +54,35 @@ def _check_recursion(self): ) pack_lines = pack_lines.mapped("product_id.pack_line_ids") - def get_price(self): + def _get_pack_line_price(self, pricelist, quantity, uom=None, date=False, **kwargs): self.ensure_one() - return self.product_id.lst_price * self.quantity + if self.product_id._is_pack_to_be_handled(): + price = pricelist._get_product_price( + self.product_id, quantity, uom=uom, date=date, **kwargs + ) + else: + price = pricelist._compute_price_rule( + self.product_id, quantity, uom=uom, date=date, **kwargs + )[self.product_id.id][0] + return price * self.quantity + + def _pack_line_price_compute( + self, price_type, uom=False, currency=False, company=False, date=False + ): + packs, no_packs = self.product_id.split_pack_products() + + pack_prices = {} + # If the component is a pack + for pack in packs: + pack_prices[pack.id] = pack.lst_price + + # else + no_pack_prices = no_packs.price_compute( + price_type, uom, currency, company, date + ) + + prices = {**pack_prices, **no_pack_prices} + for line in self: + prices[line.product_id.id] *= line.quantity + + return prices diff --git a/product_pack/models/product_pricelist.py b/product_pack/models/product_pricelist.py new file mode 100644 index 000000000..12077b50d --- /dev/null +++ b/product_pack/models/product_pricelist.py @@ -0,0 +1,48 @@ +from odoo import models + + +class Pricelist(models.Model): + _inherit = "product.pricelist" + + def _get_product_price(self, product, quantity, uom=None, date=False, **kwargs): + """Compute the pricelist price for the specified pack product, qty & uom. + :returns: unit price of the pack product + components, considering pricelist rules + """ + self.ensure_one() + if product._is_pack_to_be_handled(): + # NOTE: This exception is to avoid adding the list price of the packs + # "totalized" and "non detailed". Should be removed to solve the issue #169. + if ( + product.pack_type == "non_detailed" + or product.pack_component_price == "totalized" + ): + pack_price = 0 + else: + # Para packs "detailed", el precio es SOLO la suma de componentes + # No se suma el precio del pack en sí para evitar duplicación + pack_price = 0 + + for line in product.sudo().pack_line_ids: + pack_price += line._get_pack_line_price( + self, quantity, uom=uom, date=date, **kwargs + ) + return pack_price + else: + return super()._get_product_price( + product=product, quantity=quantity, uom=uom, date=date, **kwargs + ) + + def _get_products_price(self, products, quantity, uom=None, date=False, **kwargs): + """Compute the pricelist price for the specified pack product, qty & uom. + + :returns: unit price of the pack product + components, considering pricelist rules + """ + packs, no_packs = products.split_pack_products() + res = super()._get_products_price( + no_packs, quantity=quantity, uom=uom, date=date, **kwargs + ) + for pack in packs: + res[pack.id] = self._get_product_price( + product=pack, quantity=quantity, uom=uom, date=date, **kwargs + ) + return res diff --git a/product_pack/models/product_product.py b/product_pack/models/product_product.py index 1379281f5..e57faaaec 100644 --- a/product_pack/models/product_product.py +++ b/product_pack/models/product_product.py @@ -26,62 +26,36 @@ def get_pack_lines(self): can be overloaded to introduce filtering function by date, etc...""" return self.mapped("pack_line_ids") + def _is_pack_to_be_handled(self): + return self.product_tmpl_id._is_pack_to_be_handled() + def split_pack_products(self): packs = self.filtered(lambda p: p.product_tmpl_id._is_pack_to_be_handled()) return packs, (self - packs) - def price_compute( - self, price_type, uom=False, currency=False, company=False, date=False - ): - packs, no_packs = self.split_pack_products() - prices = super(ProductProduct, no_packs).price_compute( - price_type, uom, currency, company, date - ) - for product in packs.with_context(prefetch_fields=False): - pack_price = 0.0 - for pack_line in product.sudo().pack_line_ids: - pack_price += pack_line.get_price() - pricelist_id_or_name = self._context.get("pricelist") - # if there is a pricelist on the context the returned prices are on - # that currency but, if the pack product has a different currency - # it will be converted again by pp._compute_price_rule, so if - # that is the case we convert the amounts to the pack currency - if pricelist_id_or_name: - pricelist = None - if isinstance(pricelist_id_or_name, list): - pricelist_id_or_name = pricelist_id_or_name[0] - if isinstance(pricelist_id_or_name, str): - pricelist_name_search = self.env["product.pricelist"].name_search( - pricelist_id_or_name, operator="=", limit=1 - ) - if pricelist_name_search: - pricelist = self.env["product.pricelist"].browse( - [pricelist_name_search[0][0]] - ) - elif isinstance(pricelist_id_or_name, int): - pricelist = self.env["product.pricelist"].browse( - pricelist_id_or_name - ) - if pricelist and pricelist.currency_id != product.currency_id: - pack_price = pricelist.currency_id._convert( - pack_price, - product.currency_id, - self.company_id or self.env.company, - fields.Date.today(), - ) - prices[product.id] = pack_price - return prices - @api.depends("list_price", "price_extra") def _compute_product_lst_price(self): - packs, no_packs = self.split_pack_products() + packs, no_packs = self.with_context(whole_pack_price=True).split_pack_products() ret_val = super(ProductProduct, no_packs)._compute_product_lst_price() - to_uom = None + uom = False if "uom" in self._context: - to_uom = self.env["uom.uom"].browse([self._context["uom"]]) + uom = self.env["uom.uom"].browse([self._context["uom"]]) for product in packs: - list_price = product.price_compute("list_price").get(product.id) - if to_uom: - list_price = product.uom_id._compute_price(list_price, to_uom) + # NOTE: This exception is to avoid adding the list price of the packs + # "totalized" and "non detailed". Should be removed to solve the issue #169. + if ( + product.pack_type == "non_detailed" + or product.pack_component_price == "totalized" + ): + list_price = 0 + else: + list_price = product.price_compute("list_price", uom=uom).get( + product.id + ) + list_price += sum( + product.pack_line_ids._pack_line_price_compute( + "list_price", uom=uom + ).values() + ) product.lst_price = list_price + product.price_extra return ret_val diff --git a/product_pack/readme/CONTRIBUTORS.rst b/product_pack/readme/CONTRIBUTORS.rst index 95a7510c2..ce8814b0a 100644 --- a/product_pack/readme/CONTRIBUTORS.rst +++ b/product_pack/readme/CONTRIBUTORS.rst @@ -3,6 +3,9 @@ * Juan José Scarafía * Nicolas Mac Rouillon * Katherine Zaoral + * Bruno Zanotti + * Augusto Weiss + * Nicolas Col * `NaN·TIC `_ * `Tecnativa `_: diff --git a/product_pack/readme/USAGE.rst b/product_pack/readme/USAGE.rst index 9bee1990c..ce7f542c3 100644 --- a/product_pack/readme/USAGE.rst +++ b/product_pack/readme/USAGE.rst @@ -28,3 +28,18 @@ The options of this field are the followings: * Ignored: will show each components but will not show components prices. The pack product will be the only one that has price and this one will be the price set in the pack product. + ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ +| **Pack type** | **Show components on SO?** | **Sale price** | **Discount** | **Can be modified?** | ++=============================+=============================+=================================+=========================================+======================+ +| **Detailed per components** | Yes, with their prices | Components + Pack | Applies to the price of the pack and | Yes, configurable | +| | | | the components | | ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ +| **Detailed - Totalized** | Yes, with their prices at 0 | Components | Applies to the total (pack + components)| No | ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ +| **Detailed - Ignored** | Yes, with their prices at 0 | Only Pack | Applies to the pack | No | ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ +| **No detailed** | No | Components | Applies to the total (pack + components)| No | ++-----------------------------+-----------------------------+---------------------------------+-----------------------------------------+----------------------+ + +**Note:** If pricelist enabled, Odoo will display the price according to the corresponding pricelist. In the case of a pricelist with discount policy "Show public price & discount to the customer" keep in mind that the "Non Detailed" and "Detailed - Totalized in main product" packs do not display the discount. diff --git a/product_pack/static/description/index.html b/product_pack/static/description/index.html index 040ecd45c..8d0251004 100644 --- a/product_pack/static/description/index.html +++ b/product_pack/static/description/index.html @@ -1,4 +1,3 @@ - @@ -367,7 +366,7 @@

Product Pack

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:d6d79cfb1448cba3c8cc68f89101f6b53245f8a695effd2058ca881dac328748 +!! source digest: sha256:6412314c32b470ebaa9ba73edd671e2513bf0b8c9faa4703dcb22faf76692670 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/product-pack Translate me on Weblate Try me on Runboat

This module allows you to define a product as a Product Pack. Each @@ -422,6 +421,51 @@

Usage

and this one will be the price set in the pack product. + +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Pack typeShow components on SO?Sale priceDiscountCan be modified?
Detailed per componentsYes, with their pricesComponents + PackApplies to the price of the pack and +the componentsYes, configurable
Detailed - TotalizedYes, with their prices at 0ComponentsApplies to the total (pack + components)No
Detailed - IgnoredYes, with their prices at 0Only PackApplies to the packNo
No detailedNoComponentsApplies to the total (pack + components)No
+

Note: If pricelist enabled, Odoo will display the price according to the corresponding pricelist. In the case of a pricelist with discount policy “Show public price & discount to the customer” keep in mind that the “Non Detailed” and “Detailed - Totalized in main product” packs do not display the discount.

Known issues / Roadmap

@@ -457,6 +501,9 @@

Contributors

  • Juan José Scarafía
  • Nicolas Mac Rouillon
  • Katherine Zaoral
  • +
  • Bruno Zanotti
  • +
  • Augusto Weiss
  • +
  • Nicolas Col
  • NaN·TIC
  • diff --git a/product_pack/tests/common.py b/product_pack/tests/common.py index f421b0a31..b16cb4902 100644 --- a/product_pack/tests/common.py +++ b/product_pack/tests/common.py @@ -14,3 +14,20 @@ def setUpClass(cls): "name": "Company Pack 2", } cls.company_2 = cls.env["res.company"].create(vals) + cls.discount_pricelist = cls.env["product.pricelist"].create( + { + "name": "Discount", + "company_id": cls.env.company.id, + "item_ids": [ + ( + 0, + 0, + { + "applied_on": "3_global", + "compute_price": "percentage", + "percent_price": 10, + }, + ) + ], + } + ) diff --git a/product_pack/tests/test_product_pack.py b/product_pack/tests/test_product_pack.py index 050bf2f99..882ed91f2 100644 --- a/product_pack/tests/test_product_pack.py +++ b/product_pack/tests/test_product_pack.py @@ -63,7 +63,7 @@ def test_get_pack_line_price(self): 30.0, self.cpu_detailed.pack_line_ids.filtered( lambda l: l.product_id == component.product_id - ).get_price(), + )._pack_line_price_compute("list_price")[component.product_id.id], ) def test_get_pack_lst_price(self): @@ -118,19 +118,33 @@ def test_pack_modifiable(self): pack.pack_component_price = "totalized" self.assertTrue(pack.pack_modifiable_invisible) - def test_price_compute_with_pricelist_context(self): - # Ensure that the price_compute method correctly handles the - # price list context when the price list is missing. - product_pack = self.env.ref("product_pack.product_pack_cpu_detailed_totalized") - component_1 = self.env.ref("product_pack.pack_cpu_detailed_totalized_1") - component_1.product_id.list_price = 30.0 - component_2 = self.env.ref("product_pack.pack_cpu_detailed_totalized_3") - component_2.product_id.list_price = 15.0 - component_3 = self.env.ref("product_pack.pack_cpu_detailed_components_4") - component_3.product_id.list_price = 5.0 - price = ( - product_pack.with_context(pricelist="pricelist test") - .price_compute("list_price") - .get(product_pack.id) - ) - self.assertEqual(price, 50.0) + def test_pack_price_with_pricelist_context(self): + # Apply pricelist by context only for product packs (no components) + + # Pack Detailed + pack = self.env.ref("product_pack.product_pack_cpu_detailed_components") + price = pack.with_context( + whole_pack_price=True, pricelist=self.discount_pricelist.id + )._get_contextual_price() + self.assertEqual(price, 2601.675) + + # Pack Totalized + pack = self.env.ref("product_pack.product_pack_cpu_detailed_totalized") + price = pack.with_context( + pricelist=self.discount_pricelist.id + )._get_contextual_price() + self.assertEqual(price, 2574.0) + + # Pack Ignored + pack = self.env.ref("product_pack.product_pack_cpu_detailed_ignored") + price = pack.with_context( + pricelist=self.discount_pricelist.id + )._get_contextual_price() + self.assertEqual(price, 27.675) + + # Pack Non detailed + pack = self.env.ref("product_pack.product_pack_cpu_non_detailed") + price = pack.with_context( + pricelist=self.discount_pricelist.id + )._get_contextual_price() + self.assertEqual(price, 2574.0) diff --git a/sale_product_pack/README.rst b/sale_product_pack/README.rst index f8f2266a7..a761a406a 100644 --- a/sale_product_pack/README.rst +++ b/sale_product_pack/README.rst @@ -102,6 +102,12 @@ Contributors * Maxime Franco +* `ADHOC SA `_: + + * Bruno Zanotti + * Augusto Weiss + * Nicolas Col + Maintainers ~~~~~~~~~~~ diff --git a/sale_product_pack/models/product_pack_line.py b/sale_product_pack/models/product_pack_line.py index 522727695..d30205117 100644 --- a/sale_product_pack/models/product_pack_line.py +++ b/sale_product_pack/models/product_pack_line.py @@ -28,24 +28,27 @@ def get_sale_order_line_vals(self, line, order): sol._onchange_product_id_warning() vals = sol._convert_to_write(sol._cache) pack_price_types = {"totalized", "ignored"} - sale_discount = 0.0 - if line.product_id.pack_component_price == "detailed": - sale_discount = 100.0 - ( - (100.0 - sol.discount) * (100.0 - self.sale_discount) / 100.0 - ) - elif ( + if ( line.product_id.pack_type == "detailed" and line.product_id.pack_component_price in pack_price_types ): vals["price_unit"] = 0.0 - vals.update( - { - "discount": sale_discount, - "name": "{}{}".format("> " * (line.pack_depth + 1), sol.name), - } - ) + + vals["name"] = "{}{}".format("> " * (line.pack_depth + 1), sol.name) + return vals - def get_price(self): - self.ensure_one() - return super().get_price() * (1 - self.sale_discount / 100.0) + def _get_pack_line_price(self, pricelist, quantity, uom=None, date=False, **kwargs): + return super()._get_pack_line_price( + pricelist, quantity, uom=uom, date=date, **kwargs + ) * (1 - self.sale_discount / 100.0) + + def _pack_line_price_compute( + self, price_type, uom=False, currency=False, company=False, date=False + ): + pack_line_prices = super()._pack_line_price_compute( + price_type, uom, currency, company, date + ) + for line in self: + pack_line_prices[line.product_id.id] *= 1 - line.sale_discount / 100.0 + return pack_line_prices diff --git a/sale_product_pack/models/sale_order.py b/sale_product_pack/models/sale_order.py index 00f518fe7..ea6ed0639 100644 --- a/sale_product_pack/models/sale_order.py +++ b/sale_product_pack/models/sale_order.py @@ -60,12 +60,7 @@ def write(self, vals): def _get_update_prices_lines(self): res = super()._get_update_prices_lines() - result = self.order_line.browse() - index = 0 - while index < len(res): - line = res[index] - result |= line - index += 1 - if line.product_id.pack_ok and line.pack_type == "detailed": - index += len(line.product_id.pack_line_ids) - return result + return res.filtered( + lambda line: not line.pack_parent_line_id + or line.pack_parent_line_id.pack_component_price == "detailed" + ) diff --git a/sale_product_pack/models/sale_order_line.py b/sale_product_pack/models/sale_order_line.py index 5c2d79c84..df96c1030 100644 --- a/sale_product_pack/models/sale_order_line.py +++ b/sale_product_pack/models/sale_order_line.py @@ -126,3 +126,35 @@ def action_open_parent_pack_product_view(self): "view_mode": "tree,form", "domain": domain, } + + def _get_pricelist_price(self): + """Compute the price given by the pricelist for the given line information. + + :return: the product sales price in the order currency (without taxes) + :rtype: float + """ + price = super()._get_pricelist_price() + + if self.product_id.product_tmpl_id._is_pack_to_be_handled(): + price = self.order_id.pricelist_id._get_product_price( + product=self.product_id.product_tmpl_id, quantity=1.0 + ) + return price + + def _get_pack_line_discount(self): + """returns the discount settled in the parent pack lines""" + self.ensure_one() + discount = 0.0 + if self.pack_parent_line_id.pack_component_price == "detailed": + for pack_line in self.pack_parent_line_id.product_id.pack_line_ids: + if pack_line.product_id == self.product_id: + discount = pack_line.sale_discount + break + return discount + + @api.depends("product_id", "product_uom", "product_uom_qty") + def _compute_discount(self): + res = super()._compute_discount() + for pack_line in self.filtered("pack_parent_line_id"): + pack_line.discount = pack_line._get_pack_line_discount() + return res diff --git a/sale_product_pack/readme/CONTRIBUTORS.rst b/sale_product_pack/readme/CONTRIBUTORS.rst index 25e0559ed..200188019 100644 --- a/sale_product_pack/readme/CONTRIBUTORS.rst +++ b/sale_product_pack/readme/CONTRIBUTORS.rst @@ -14,3 +14,9 @@ * `Acsone `_: * Maxime Franco + +* `ADHOC SA `_: + + * Bruno Zanotti + * Augusto Weiss + * Nicolas Col diff --git a/sale_product_pack/static/description/index.html b/sale_product_pack/static/description/index.html index f16d0d06e..fa1df618c 100644 --- a/sale_product_pack/static/description/index.html +++ b/sale_product_pack/static/description/index.html @@ -1,4 +1,3 @@ - @@ -451,6 +450,12 @@

    Contributors

  • Maxime Franco
  • +
  • ADHOC SA:
      +
    • Bruno Zanotti
    • +
    • Augusto Weiss
    • +
    • Nicolas Col
    • +
    +
  • diff --git a/sale_product_pack/tests/test_sale_product_pack.py b/sale_product_pack/tests/test_sale_product_pack.py index d8de12221..390f8dfd1 100644 --- a/sale_product_pack/tests/test_sale_product_pack.py +++ b/sale_product_pack/tests/test_sale_product_pack.py @@ -98,7 +98,7 @@ def test_create_components_price_order_line(self): self.assertAlmostEqual(line.price_subtotal, 27.68) self.assertEqual( (self.sale_order.order_line - line).mapped("price_subtotal"), - [1755.0, 22.5, 885.0], + [1579.5, 20.25, 796.5], ) def test_create_ignored_price_order_line(self): diff --git a/sale_product_pack/views/product_pack_line_views.xml b/sale_product_pack/views/product_pack_line_views.xml index e5cedcf6d..b4ca38772 100644 --- a/sale_product_pack/views/product_pack_line_views.xml +++ b/sale_product_pack/views/product_pack_line_views.xml @@ -39,7 +39,7 @@