-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[19.0][ADD] website_sale_product_multiple_qty #4165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| odoo-addon-sale_product_multiple_qty @ git+https://github.com/OCA/sale-workflow.git@refs/pull/4143/head#subdirectory=sale_product_multiple_qty | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| .. image:: https://odoo-community.org/readme-banner-image | ||
| :target: https://odoo-community.org/get-involved?utm_source=readme | ||
| :alt: Odoo Community Association | ||
|
|
||
| ================================= | ||
| Website Sale Product Multiple Qty | ||
| ================================= | ||
|
|
||
| .. | ||
| !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
| !! This file is generated by oca-gen-addon-readme !! | ||
| !! changes will be overwritten. !! | ||
| !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
| !! source digest: sha256:fae80e9f1799cbaea5d031c91bf0456af4741e0d2cf9edd5af4f30424377956f | ||
| !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
|
|
||
| .. |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/license-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/19.0/website_sale_product_multiple_qty | ||
| :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-19-0/sale-workflow-19-0-website_sale_product_multiple_qty | ||
| :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=19.0 | ||
| :alt: Try me on Runboat | ||
|
|
||
| |badge1| |badge2| |badge3| |badge4| |badge5| | ||
|
|
||
| Website Sale Product Multiple Quantity | ||
| ====================================== | ||
|
|
||
| This module extends the eCommerce flow to support **Sales Multiples** | ||
| (packaging quantities) directly on the product page, in the cart, and in | ||
| the product configurator. | ||
|
|
||
| When a product (or variant) has a *Sales Multiple* configured, the | ||
| quantity entered by the customer on the website is automatically rounded | ||
| to a valid multiple according to the interaction type. | ||
|
|
||
| The rounding logic is applied dynamically when the customer: | ||
|
|
||
| - Opens the product page | ||
| - Changes the product variant | ||
| - Clicks the "+" (increase) button | ||
| - Clicks the "–" (decrease) button | ||
| - Manually enters a quantity | ||
| - Presses **Enter** inside the quantity input | ||
| - Changes quantities in the cart | ||
|
|
||
| Rounding Rules | ||
| -------------- | ||
|
|
||
| The behavior is designed to be predictable and consistent with | ||
| packaging-based sales. | ||
|
|
||
| Product Page | ||
| ~~~~~~~~~~~~ | ||
|
|
||
| - On page load (or variant switch): | ||
|
|
||
| - If the product is a multiple product, the default quantity is set to | ||
| at least one valid multiple. | ||
| - Otherwise, the standard minimum quantity is used. | ||
|
|
||
| - When clicking "+": | ||
|
|
||
| - The quantity increases by one full multiple step. | ||
|
|
||
| - When clicking "–": | ||
|
|
||
| - The quantity decreases by one full multiple step. | ||
| - The quantity never goes below the minimum allowed value. | ||
|
|
||
| - When manually entering a quantity: | ||
|
|
||
| - The value is rounded **UP** to the nearest valid multiple. | ||
|
|
||
| - When pressing **Enter**: | ||
|
|
||
| - The value is processed like a manual change (no form submission). | ||
| - Rounding logic is applied before any RPC call. | ||
|
|
||
| Cart | ||
| ~~~~ | ||
|
|
||
| - When clicking "+": | ||
|
|
||
| - The quantity increases by one full multiple step. | ||
|
|
||
| - When clicking "–": | ||
|
|
||
| - The quantity decreases by one full multiple step. | ||
| - If it goes below the first multiple, it becomes ``0`` (line removal | ||
| behavior). | ||
|
|
||
| - When manually entering a quantity: | ||
|
|
||
| - The value is rounded **UP** to the nearest valid multiple. | ||
| - ``0`` remains allowed to preserve standard cart removal behavior. | ||
|
|
||
| Example | ||
| ------- | ||
|
|
||
| If a product is sold in multiples of 500: | ||
|
|
||
| - Entering ``1`` → becomes ``500`` | ||
| - Entering ``499`` → becomes ``500`` | ||
| - Entering ``501`` → becomes ``1000`` | ||
| - Clicking "–" from ``500`` (product page) → becomes ``500`` (minimum) | ||
| - Clicking "–" from ``500`` (cart) → becomes ``0`` | ||
| - Clicking "+" from ``0`` (cart) → becomes ``500`` | ||
|
|
||
| Configuration | ||
| ------------- | ||
|
|
||
| It is the responsibility of the user to configure compatible Units of | ||
| Measure. | ||
|
|
||
| The Sales Multiple UoM must belong to the same UoM category as the | ||
| product's sales UoM. Incorrect configuration (for example, mixing | ||
| unrelated UoM categories) may lead to unexpected quantity conversions | ||
| and rounding results. | ||
|
|
||
| The module assumes that Units of Measure are properly defined and | ||
| conversion ratios are accurate. | ||
|
|
||
| **Table of contents** | ||
|
|
||
| .. contents:: | ||
| :local: | ||
|
|
||
| Usage | ||
| ===== | ||
|
|
||
| Usage | ||
| ===== | ||
|
|
||
| Configuration | ||
| ------------- | ||
|
|
||
| 1. Go to *Sales → Products*. | ||
| 2. Open a product. | ||
| 3. Set a **Sales Multiple UoM** (for example, *Pack of 500*). | ||
|
|
||
| The selected UoM must belong to the same UoM category as the product's | ||
| sales unit of measure. | ||
|
|
||
| Important | ||
| --------- | ||
|
|
||
| Ensure that the Sales Multiple UoM is correctly configured: | ||
|
|
||
| - It must belong to the same UoM category as the product's sales UoM. | ||
| - Conversion ratios must be accurate. | ||
| - The multiple should reflect the real packaging quantity. | ||
|
|
||
| Incorrect UoM configuration may result in unexpected rounding behavior. | ||
|
|
||
| Bug Tracker | ||
| =========== | ||
|
|
||
| Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-workflow/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 <https://github.com/OCA/sale-workflow/issues/new?body=module:%20website_sale_product_multiple_qty%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. | ||
|
|
||
| Do not contact contributors directly about support or help with technical issues. | ||
|
|
||
| Credits | ||
| ======= | ||
|
|
||
| Authors | ||
| ------- | ||
|
|
||
| * Camptocamp SA | ||
|
|
||
| Contributors | ||
| ------------ | ||
|
|
||
| - `Camptocamp <https://www.camptocamp.com>`__: | ||
|
|
||
| - Maksym Yankin <maksym.yankin@camptocamp.com> | ||
| - Ivan Todorovich <ivan.todorovich@camptocamp.com> | ||
| - Gaëtan Vaujour <gaetan.vaujour@camptocamp.com> | ||
|
|
||
| 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-yankinmax| image:: https://github.com/yankinmax.png?size=40px | ||
| :target: https://github.com/yankinmax | ||
| :alt: yankinmax | ||
|
|
||
| Current `maintainer <https://odoo-community.org/page/maintainer-role>`__: | ||
|
|
||
| |maintainer-yankinmax| | ||
|
|
||
| This module is part of the `OCA/sale-workflow <https://github.com/OCA/sale-workflow/tree/19.0/website_sale_product_multiple_qty>`_ project on GitHub. | ||
|
|
||
| You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| from . import controllers | ||
| from . import models |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Copyright 2026 Camptocamp SA | ||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
| { | ||
| "name": "Website Sale Product Multiple Qty", | ||
| "summary": "Allows setting a multiple quantity for products on the website.", | ||
| "version": "19.0.1.0.0", | ||
| "category": "Sales", | ||
| "website": "https://github.com/OCA/sale-workflow", | ||
| "author": "Camptocamp SA, Odoo Community Association (OCA)", | ||
| "license": "AGPL-3", | ||
| "installable": True, | ||
| "depends": [ | ||
| # Odoo/core | ||
| "website_sale", | ||
| # OCA/sale-workflow | ||
| "sale_product_multiple_qty", | ||
| ], | ||
| "maintainers": ["yankinmax"], | ||
| "data": [ | ||
| # Views | ||
| "views/templates.xml", | ||
| ], | ||
| "assets": { | ||
| "web.assets_backend": [ | ||
| "website_sale_product_multiple_qty/static/src/js/product/**/*", | ||
| "website_sale_product_multiple_qty/static/src/js/quantity_buttons/**/*", | ||
| ], | ||
| "web.assets_frontend": [ | ||
| "website_sale_product_multiple_qty/static/src/js/interactions/**/*", | ||
| ], | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| from . import product_configurator | ||
| from . import variant |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # Copyright 2026 Camptocamp SA | ||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
|
||
| from odoo.addons.website_sale.controllers.product_configurator import ( | ||
| WebsiteSaleProductConfiguratorController, | ||
| ) | ||
|
|
||
|
|
||
| class WebsiteSaleProductConfiguratorMultipleController( | ||
| WebsiteSaleProductConfiguratorController | ||
| ): | ||
| def _get_sale_multiple_vals(self, product_or_template): | ||
| # Get product variant if we got a single variant template | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is duplicated verbatim in product_or_template._get_sale_multiple_vals(product_or_template)Or extract to a shared mixin to avoid drift.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is addressed, thx |
||
| product = product_or_template | ||
| if product._name == "product.template": | ||
| product = product.product_variant_id | ||
|
|
||
| if multiple_uom := product.sale_multiple_uom_id: | ||
| return { | ||
| "is_multiple": 1, | ||
| "sale_multiple_qty": multiple_uom.factor, | ||
| } | ||
| return { | ||
| "is_multiple": 0, | ||
| "sale_multiple_qty": 1, | ||
| } | ||
|
|
||
| def _get_basic_product_information( | ||
| self, | ||
| product_or_template, | ||
| pricelist, | ||
| combination, | ||
| currency=None, | ||
| date=None, | ||
| **kwargs, | ||
| ): | ||
| product_info = super()._get_basic_product_information( | ||
| product_or_template, | ||
| pricelist, | ||
| combination, | ||
| currency=currency, | ||
| date=date, | ||
| **kwargs, | ||
| ) | ||
| product_info.update(self._get_sale_multiple_vals(product_or_template)) | ||
| return product_info | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Copyright 2026 Camptocamp SA | ||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
|
||
| from odoo.http import route | ||
|
|
||
| from odoo.addons.website_sale.controllers.variant import WebsiteSaleVariantController | ||
|
|
||
|
|
||
| class WebsiteSaleRoundingVariantController(WebsiteSaleVariantController): | ||
| @route( | ||
| "/website_sale/get_combination_info", | ||
| type="jsonrpc", | ||
| auth="public", | ||
| methods=["POST"], | ||
| website=True, | ||
| readonly=True, | ||
| ) | ||
| def get_combination_info_website( | ||
| self, | ||
| product_template_id, | ||
| product_id, | ||
| combination, | ||
| add_qty, | ||
| uom_id=None, | ||
| **kwargs, | ||
| ): | ||
| combination_info = super().get_combination_info_website( | ||
| product_template_id=product_template_id, | ||
| product_id=product_id, | ||
| combination=combination, | ||
| add_qty=add_qty, | ||
| uom_id=uom_id, | ||
| **kwargs, | ||
| ) | ||
| incoming_pid = int(product_id or 0) | ||
| resolved_pid = int(combination_info.get("product_id") or 0) | ||
|
|
||
| # Detect if the variant has changed to be able | ||
| # to reset the quantity to the default value for the new variant if needed. | ||
| combination_info["variant_switched"] = bool( | ||
| resolved_pid and resolved_pid != incoming_pid | ||
| ) | ||
| return combination_info |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from . import product_template |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Copyright 2026 Camptocamp SA | ||
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
| from odoo import api, models | ||
|
|
||
|
|
||
| class ProductTemplate(models.Model): | ||
| _inherit = "product.template" | ||
|
|
||
| def _get_sale_multiple_vals(self, product_or_template): | ||
| # Get product variant if we got a single variant template | ||
| product = product_or_template | ||
| if product._name == "product.template": | ||
| product = product.product_variant_id | ||
|
|
||
| if multiple_uom := product.sale_multiple_uom_id: | ||
| return { | ||
| "is_multiple": 1, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth double-checking which UoM field gives the correct step value here. In Odoo's UoM model,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've checked and it's ok to use factor if I always have the same "Units" relative UoM. |
||
| "sale_multiple_qty": multiple_uom.factor, | ||
| } | ||
| return { | ||
| "is_multiple": 0, | ||
| "sale_multiple_qty": 1, | ||
| } | ||
|
|
||
| @api.model | ||
| def _get_additionnal_combination_info( | ||
| self, product_or_template, quantity, uom, date, website | ||
| ): | ||
| # OVERRIDE: to update the combination info with the multiple related info | ||
| combination_info = super()._get_additionnal_combination_info( | ||
| product_or_template, quantity, uom, date, website | ||
| ) | ||
| combination_info.update(self._get_sale_multiple_vals(product_or_template)) | ||
| return combination_info | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| [build-system] | ||
| requires = ["whool"] | ||
| build-backend = "whool.buildapi" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| - [Camptocamp](https://www.camptocamp.com): | ||
| - Maksym Yankin \<<maksym.yankin@camptocamp.com>\> | ||
| - Ivan Todorovich \<<ivan.todorovich@camptocamp.com>\> | ||
| - Gaëtan Vaujour \<<gaetan.vaujour@camptocamp.com>\> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This references PR #4143 which is still unmerged, causing the CI
Detect unreleased dependenciesfailure. This will resolve oncesale_product_multiple_qtyis merged, but it blocks merge of this PR as well.