diff --git a/sale_optional_product_quantity/README.rst b/sale_optional_product_quantity/README.rst new file mode 100644 index 00000000000..b1fb3d92ffe --- /dev/null +++ b/sale_optional_product_quantity/README.rst @@ -0,0 +1,87 @@ +============================== +Sale Optional Product Quantity +============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:52d79306a3f06f4364ef7cd0f71946b5ca0a85376f8ed40d664832d29e7b7220 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.0/sale_optional_product_quantity + :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-16-0/sale-workflow-16-0-sale_optional_product_quantity + :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=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to use quantities for optional products in quotations +and sales orders. When a product that has optional products is added to +a quotation optional product quantities will be updated accordingly. +Please check description of the product_optional_product_quantity module +for more details. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Check the configuration manual of the +producty_optional_product_quantity. module for details. + +Usage +===== + +Add a product that has optional products to quotation. Optional product +quantities will be updated accordingly. + +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 +------- + +* Cetmix + +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. + +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_optional_product_quantity/__init__.py b/sale_optional_product_quantity/__init__.py new file mode 100644 index 00000000000..88b2aa93281 --- /dev/null +++ b/sale_optional_product_quantity/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models diff --git a/sale_optional_product_quantity/__manifest__.py b/sale_optional_product_quantity/__manifest__.py new file mode 100644 index 00000000000..7a31653b4d8 --- /dev/null +++ b/sale_optional_product_quantity/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Sale Optional Product Quantity", + "version": "16.0.1.0.0", + "category": "Sales Management", + "summary": "Use optional product quantities in quotations and sales orders", + "author": "Cetmix, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sale-workflow", + "license": "AGPL-3", + "depends": [ + "product_optional_product_quantity", + "sale_management", + ], + "data": [], + "installable": True, + "auto_install": False, +} diff --git a/sale_optional_product_quantity/models/__init__.py b/sale_optional_product_quantity/models/__init__.py new file mode 100644 index 00000000000..687c85832f9 --- /dev/null +++ b/sale_optional_product_quantity/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import sale_order_option +from . import sale_order_line +from . import sale_order diff --git a/sale_optional_product_quantity/models/sale_order.py b/sale_optional_product_quantity/models/sale_order.py new file mode 100644 index 00000000000..c3abaefdbbc --- /dev/null +++ b/sale_optional_product_quantity/models/sale_order.py @@ -0,0 +1,32 @@ +# Copyright 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _create_optional_line_if_not_exists(self, product_template, price_unit): + """Create optional product line if not exists""" + self.ensure_one() + sale_order_option_obj = self.env["sale.order.option"] + if sale_order_option_obj.search_count( + [ + ("order_id", "=", self.id), + ("product_id.product_tmpl_id", "=", product_template.id), + ] + ): + return + return sale_order_option_obj.create( + { + "order_id": self.id, + "price_unit": price_unit, + "product_id": self.env["product.product"] + .search( + [ + ("product_tmpl_id", "=", product_template.id), + ] + )[0] + .id, + } + ) diff --git a/sale_optional_product_quantity/models/sale_order_line.py b/sale_optional_product_quantity/models/sale_order_line.py new file mode 100644 index 00000000000..ac34c66a33a --- /dev/null +++ b/sale_optional_product_quantity/models/sale_order_line.py @@ -0,0 +1,94 @@ +# Copyright 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + @api.model_create_multi + def create(self, vals_list): + optional_quantity_enabled = self.env.user.has_group( + "product_optional_product_quantity.group_product_optional_quantity" + ) + if not optional_quantity_enabled: + return super().create(vals_list) + res = super().create(vals_list) + for line in res: + for product_tmpl in line._get_optional_products(): + product = self.env["product.product"].search( + [("product_tmpl_id", "=", product_tmpl.id)] + )[0] + option = line.order_id._create_optional_line_if_not_exists( + product_tmpl, + product._get_tax_included_unit_price( + line.company_id, + line.order_id.currency_id, + line.order_id.date_order, + "sale", + fiscal_position=line.order_id.fiscal_position_id, + product_currency=line.currency_id, + ), + ) + option._compute_quantity() + if line._is_optional_product(): + line.order_id._create_optional_line_if_not_exists( + line.product_template_id, line.price_unit + ) + return res + + @api.depends("product_template_id", "order_id.order_line") + def _compute_product_uom_qty(self): + optional_quantity_enabled = self.env.user.has_group( + "product_optional_product_quantity.group_product_optional_quantity" + ) + if not optional_quantity_enabled: + return super()._compute_product_uom_qty() + for line in self: + order = line.order_id + line_product_id = line.product_template_id.id + order_lines = order.order_line.filtered( + lambda x: line_product_id + in (x.product_template_id.optional_product_ids.ids) + ) + if not order_lines: + line.product_uom_qty = line.product_uom_qty + continue + + multiplier = sum( + order_lines.mapped("product_template_id") + .mapped("product_optional_line_ids") + .filtered(lambda x: x.optional_product_tmpl_id.id == line_product_id) + .mapped("quantity") + ) + qty = sum(order_lines.mapped("product_uom_qty")) + line.product_uom_qty = qty * multiplier + + def _is_optional_product(self): + """ + Check if product is optional. + + Returns: + bool: True if product is optional, False otherwise + """ + self.ensure_one() + return bool( + self.env["product.template"].search_count( + [("optional_product_ids", "in", self.product_template_id.id)] + ) + ) + + def _get_optional_products(self): + """ + Get product.template recordset with product templates + related to current line's product as optional products. + + Returns: + product.template recordset: Optional products + """ + self.ensure_one() + return ( + self.env["product.optional.line"] + .search([("product_tmpl_id", "=", self.product_template_id.id)]) + .mapped("optional_product_tmpl_id") + ) diff --git a/sale_optional_product_quantity/models/sale_order_option.py b/sale_optional_product_quantity/models/sale_order_option.py new file mode 100644 index 00000000000..e190d2c9737 --- /dev/null +++ b/sale_optional_product_quantity/models/sale_order_option.py @@ -0,0 +1,51 @@ +# Copyright 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models + + +class SaleOrderOption(models.Model): + _inherit = "sale.order.option" + + quantity = fields.Float( + compute="_compute_quantity", + readonly=False, + store=True, + ) + + @api.depends( + "order_id.order_line", + "order_id.order_line.product_template_id", + "order_id.order_line.product_uom_qty", + ) + def _compute_quantity(self): + """ + Compute quantity based on optional product + quantities configured in product templates + from lines of Quotation/Sale Order. + """ + optional_quantity_enabled = self.env.user.has_group( + "product_optional_product_quantity.group_product_optional_quantity" + ) + if not optional_quantity_enabled: + for option in self: + option.quantity = option.quantity + return + for option in self: + order = option.order_id + option_product_id = option.product_id.product_tmpl_id.id + order_lines = order.order_line.filtered( + lambda x: option_product_id + in (x.product_template_id.optional_product_ids.ids) + ) + if not order_lines: + option.quantity = 1 + continue + + multiplier = sum( + order_lines.mapped("product_template_id") + .mapped("product_optional_line_ids") + .filtered(lambda x: x.optional_product_tmpl_id.id == option_product_id) + .mapped("quantity") + ) + qty = sum(order_lines.mapped("product_uom_qty")) + option.quantity = qty * multiplier diff --git a/sale_optional_product_quantity/readme/CONFIGURE.md b/sale_optional_product_quantity/readme/CONFIGURE.md new file mode 100644 index 00000000000..03110da80d7 --- /dev/null +++ b/sale_optional_product_quantity/readme/CONFIGURE.md @@ -0,0 +1 @@ +Check the configuration manual of the producty_optional_product_quantity. module for details. \ No newline at end of file diff --git a/sale_optional_product_quantity/readme/DESCRIPTION.md b/sale_optional_product_quantity/readme/DESCRIPTION.md new file mode 100644 index 00000000000..070e6ec3113 --- /dev/null +++ b/sale_optional_product_quantity/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module allows to use quantities for optional products in quotations and sales orders. +When a product that has optional products is added to a quotation optional product quantities will be updated accordingly. +Please check description of the product_optional_product_quantity module for more details. \ No newline at end of file diff --git a/sale_optional_product_quantity/readme/USAGE.md b/sale_optional_product_quantity/readme/USAGE.md new file mode 100644 index 00000000000..5a83407a51e --- /dev/null +++ b/sale_optional_product_quantity/readme/USAGE.md @@ -0,0 +1 @@ +Add a product that has optional products to quotation. Optional product quantities will be updated accordingly. \ No newline at end of file diff --git a/sale_optional_product_quantity/static/description/icon.png b/sale_optional_product_quantity/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/sale_optional_product_quantity/static/description/icon.png differ diff --git a/sale_optional_product_quantity/static/description/index.html b/sale_optional_product_quantity/static/description/index.html new file mode 100644 index 00000000000..f396fedda0a --- /dev/null +++ b/sale_optional_product_quantity/static/description/index.html @@ -0,0 +1,429 @@ + + + + + +Sale Optional Product Quantity + + + +
+

Sale Optional Product Quantity

+ + +

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

+

This module allows to use quantities for optional products in quotations +and sales orders. When a product that has optional products is added to +a quotation optional product quantities will be updated accordingly. +Please check description of the product_optional_product_quantity module +for more details.

+

Table of contents

+ +
+

Configuration

+

Check the configuration manual of the +producty_optional_product_quantity. module for details.

+
+
+

Usage

+

Add a product that has optional products to quotation. Optional product +quantities will be updated accordingly.

+
+
+

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

+
    +
  • Cetmix
  • +
+
+
+

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.

+

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/setup/sale_optional_product_quantity/odoo/addons/sale_optional_product_quantity b/setup/sale_optional_product_quantity/odoo/addons/sale_optional_product_quantity new file mode 120000 index 00000000000..8d3efc49d6a --- /dev/null +++ b/setup/sale_optional_product_quantity/odoo/addons/sale_optional_product_quantity @@ -0,0 +1 @@ +../../../../sale_optional_product_quantity \ No newline at end of file diff --git a/setup/sale_optional_product_quantity/setup.py b/setup/sale_optional_product_quantity/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/sale_optional_product_quantity/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)