Skip to content

Commit b74f486

Browse files
committed
[19.0][ADD] website_sale_product_multiple_qty
1 parent 2b0d048 commit b74f486

23 files changed

Lines changed: 2043 additions & 0 deletions
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
.. image:: https://odoo-community.org/readme-banner-image
2+
:target: https://odoo-community.org/get-involved?utm_source=readme
3+
:alt: Odoo Community Association
4+
5+
=================================
6+
Website Sale Product Multiple Qty
7+
=================================
8+
9+
..
10+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11+
!! This file is generated by oca-gen-addon-readme !!
12+
!! changes will be overwritten. !!
13+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14+
!! source digest: sha256:fae80e9f1799cbaea5d031c91bf0456af4741e0d2cf9edd5af4f30424377956f
15+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
16+
17+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
18+
:target: https://odoo-community.org/page/development-status
19+
:alt: Beta
20+
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
21+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
22+
:alt: License: AGPL-3
23+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fe--commerce-lightgray.png?logo=github
24+
:target: https://github.com/OCA/e-commerce/tree/19.0/website_sale_product_multiple_qty
25+
:alt: OCA/e-commerce
26+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
27+
:target: https://translation.odoo-community.org/projects/e-commerce-19-0/e-commerce-19-0-website_sale_product_multiple_qty
28+
:alt: Translate me on Weblate
29+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
30+
:target: https://runboat.odoo-community.org/builds?repo=OCA/e-commerce&target_branch=19.0
31+
:alt: Try me on Runboat
32+
33+
|badge1| |badge2| |badge3| |badge4| |badge5|
34+
35+
Website Sale Product Multiple Quantity
36+
======================================
37+
38+
This module extends the eCommerce flow to support **Sales Multiples**
39+
(packaging quantities) directly on the product page, in the cart, and in
40+
the product configurator.
41+
42+
When a product (or variant) has a *Sales Multiple* configured, the
43+
quantity entered by the customer on the website is automatically rounded
44+
to a valid multiple according to the interaction type.
45+
46+
The rounding logic is applied dynamically when the customer:
47+
48+
- Opens the product page
49+
- Changes the product variant
50+
- Clicks the "+" (increase) button
51+
- Clicks the "–" (decrease) button
52+
- Manually enters a quantity
53+
- Presses **Enter** inside the quantity input
54+
- Changes quantities in the cart
55+
56+
Rounding Rules
57+
--------------
58+
59+
The behavior is designed to be predictable and consistent with
60+
packaging-based sales.
61+
62+
Product Page
63+
~~~~~~~~~~~~
64+
65+
- On page load (or variant switch):
66+
67+
- If the product is a multiple product, the default quantity is set to
68+
at least one valid multiple.
69+
- Otherwise, the standard minimum quantity is used.
70+
71+
- When clicking "+":
72+
73+
- The quantity increases by one full multiple step.
74+
75+
- When clicking "–":
76+
77+
- The quantity decreases by one full multiple step.
78+
- The quantity never goes below the minimum allowed value.
79+
80+
- When manually entering a quantity:
81+
82+
- The value is rounded **UP** to the nearest valid multiple.
83+
84+
- When pressing **Enter**:
85+
86+
- The value is processed like a manual change (no form submission).
87+
- Rounding logic is applied before any RPC call.
88+
89+
Cart
90+
~~~~
91+
92+
- When clicking "+":
93+
94+
- The quantity increases by one full multiple step.
95+
96+
- When clicking "–":
97+
98+
- The quantity decreases by one full multiple step.
99+
- If it goes below the first multiple, it becomes ``0`` (line removal
100+
behavior).
101+
102+
- When manually entering a quantity:
103+
104+
- The value is rounded **UP** to the nearest valid multiple.
105+
- ``0`` remains allowed to preserve standard cart removal behavior.
106+
107+
Example
108+
-------
109+
110+
If a product is sold in multiples of 500:
111+
112+
- Entering ``1`` → becomes ``500``
113+
- Entering ``499`` → becomes ``500``
114+
- Entering ``501`` → becomes ``1000``
115+
- Clicking "–" from ``500`` (product page) → becomes ``500`` (minimum)
116+
- Clicking "–" from ``500`` (cart) → becomes ``0``
117+
- Clicking "+" from ``0`` (cart) → becomes ``500``
118+
119+
Configuration
120+
-------------
121+
122+
It is the responsibility of the user to configure compatible Units of
123+
Measure.
124+
125+
The Sales Multiple UoM must belong to the same UoM category as the
126+
product's sales UoM. Incorrect configuration (for example, mixing
127+
unrelated UoM categories) may lead to unexpected quantity conversions
128+
and rounding results.
129+
130+
The module assumes that Units of Measure are properly defined and
131+
conversion ratios are accurate.
132+
133+
**Table of contents**
134+
135+
.. contents::
136+
:local:
137+
138+
Usage
139+
=====
140+
141+
Usage
142+
=====
143+
144+
Configuration
145+
-------------
146+
147+
1. Go to *Sales → Products*.
148+
2. Open a product.
149+
3. Set a **Sales Multiple UoM** (for example, *Pack of 500*).
150+
151+
The selected UoM must belong to the same UoM category as the product's
152+
sales unit of measure.
153+
154+
Important
155+
---------
156+
157+
Ensure that the Sales Multiple UoM is correctly configured:
158+
159+
- It must belong to the same UoM category as the product's sales UoM.
160+
- Conversion ratios must be accurate.
161+
- The multiple should reflect the real packaging quantity.
162+
163+
Incorrect UoM configuration may result in unexpected rounding behavior.
164+
165+
Bug Tracker
166+
===========
167+
168+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/e-commerce/issues>`_.
169+
In case of trouble, please check there if your issue has already been reported.
170+
If you spotted it first, help us to smash it by providing a detailed and welcomed
171+
`feedback <https://github.com/OCA/e-commerce/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**>`_.
172+
173+
Do not contact contributors directly about support or help with technical issues.
174+
175+
Credits
176+
=======
177+
178+
Authors
179+
-------
180+
181+
* Camptocamp SA
182+
183+
Contributors
184+
------------
185+
186+
- `Camptocamp <https://www.camptocamp.com>`__:
187+
188+
- Maksym Yankin <maksym.yankin@camptocamp.com>
189+
- Ivan Todorovich <ivan.todorovich@camptocamp.com>
190+
- Gaëtan Vaujour <gaetan.vaujour@camptocamp.com>
191+
192+
Maintainers
193+
-----------
194+
195+
This module is maintained by the OCA.
196+
197+
.. image:: https://odoo-community.org/logo.png
198+
:alt: Odoo Community Association
199+
:target: https://odoo-community.org
200+
201+
OCA, or the Odoo Community Association, is a nonprofit organization whose
202+
mission is to support the collaborative development of Odoo features and
203+
promote its widespread use.
204+
205+
.. |maintainer-yankinmax| image:: https://github.com/yankinmax.png?size=40px
206+
:target: https://github.com/yankinmax
207+
:alt: yankinmax
208+
209+
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
210+
211+
|maintainer-yankinmax|
212+
213+
This module is part of the `OCA/e-commerce <https://github.com/OCA/e-commerce/tree/19.0/website_sale_product_multiple_qty>`_ project on GitHub.
214+
215+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import controllers
2+
from . import models
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
{
4+
"name": "Website Sale Product Multiple Qty",
5+
"summary": "Allows setting a multiple quantity for products on the website.",
6+
"version": "19.0.1.0.0",
7+
"category": "Sales",
8+
"website": "https://github.com/OCA/e-commerce",
9+
"author": "Camptocamp SA, Odoo Community Association (OCA)",
10+
"license": "AGPL-3",
11+
"installable": True,
12+
"depends": [
13+
# Odoo/core
14+
"website_sale",
15+
# OCA/sale-workflow
16+
"sale_product_multiple_qty",
17+
],
18+
"maintainers": ["yankinmax"],
19+
"data": [
20+
# Views
21+
"views/templates.xml",
22+
],
23+
"assets": {
24+
"web.assets_backend": [
25+
"website_sale_product_multiple_qty/static/src/js/product/**/*",
26+
"website_sale_product_multiple_qty/static/src/js/quantity_buttons/**/*",
27+
],
28+
"web.assets_frontend": [
29+
"website_sale_product_multiple_qty/static/src/js/interactions/**/*",
30+
"website_sale_product_multiple_qty/static/src/js/product/**/*",
31+
"website_sale_product_multiple_qty/static/src/js/quantity_buttons/**/*",
32+
],
33+
"web.assets_tests": [
34+
"website_sale_product_multiple_qty/static/src/tests/tours/**/*",
35+
],
36+
},
37+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import product_configurator
2+
from . import variant
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
4+
from odoo.addons.website_sale.controllers.product_configurator import (
5+
WebsiteSaleProductConfiguratorController,
6+
)
7+
8+
9+
class WebsiteSaleProductConfiguratorMultipleController(
10+
WebsiteSaleProductConfiguratorController
11+
):
12+
def _get_basic_product_information(
13+
self,
14+
product_or_template,
15+
pricelist,
16+
combination,
17+
currency=None,
18+
date=None,
19+
**kwargs,
20+
):
21+
product_info = super()._get_basic_product_information(
22+
product_or_template,
23+
pricelist,
24+
combination,
25+
currency=currency,
26+
date=date,
27+
**kwargs,
28+
)
29+
get_sale_multiple_vals = self.env["product.template"]._get_sale_multiple_vals
30+
product_info.update(get_sale_multiple_vals(product_or_template))
31+
return product_info
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
4+
from odoo.http import route
5+
6+
from odoo.addons.website_sale.controllers.variant import WebsiteSaleVariantController
7+
8+
9+
class WebsiteSaleRoundingVariantController(WebsiteSaleVariantController):
10+
@route(
11+
"/website_sale/get_combination_info",
12+
type="jsonrpc",
13+
auth="public",
14+
methods=["POST"],
15+
website=True,
16+
readonly=True,
17+
)
18+
def get_combination_info_website(
19+
self,
20+
product_template_id,
21+
product_id,
22+
combination,
23+
add_qty,
24+
uom_id=None,
25+
**kwargs,
26+
):
27+
combination_info = super().get_combination_info_website(
28+
product_template_id=product_template_id,
29+
product_id=product_id,
30+
combination=combination,
31+
add_qty=add_qty,
32+
uom_id=uom_id,
33+
**kwargs,
34+
)
35+
incoming_pid = int(product_id or 0)
36+
resolved_pid = int(combination_info.get("product_id") or 0)
37+
38+
# Detect if the variant has changed to be able
39+
# to reset the quantity to the default value for the new variant if needed.
40+
combination_info["variant_switched"] = bool(
41+
resolved_pid and resolved_pid != incoming_pid
42+
)
43+
return combination_info
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import product_template
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
from odoo import api, models
4+
5+
6+
class ProductTemplate(models.Model):
7+
_inherit = "product.template"
8+
9+
def _get_sale_multiple_vals(self, product_or_template):
10+
# Get product variant if we got a single variant template
11+
product = product_or_template
12+
if product._name == "product.template":
13+
product = product.product_variant_id
14+
15+
if multiple_uom := product.sale_multiple_uom_id:
16+
return {
17+
"is_multiple": 1,
18+
"sale_multiple_qty": multiple_uom.factor,
19+
}
20+
return {
21+
"is_multiple": 0,
22+
"sale_multiple_qty": 1,
23+
}
24+
25+
@api.model
26+
def _get_additionnal_combination_info(
27+
self, product_or_template, quantity, uom, date, website
28+
):
29+
# OVERRIDE: to update the combination info with the multiple related info
30+
combination_info = super()._get_additionnal_combination_info(
31+
product_or_template, quantity, uom, date, website
32+
)
33+
combination_info.update(self._get_sale_multiple_vals(product_or_template))
34+
return combination_info
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- [Camptocamp](https://www.camptocamp.com):
2+
- Maksym Yankin \<<maksym.yankin@camptocamp.com>\>
3+
- Ivan Todorovich \<<ivan.todorovich@camptocamp.com>\>
4+
- Gaëtan Vaujour \<<gaetan.vaujour@camptocamp.com>\>

0 commit comments

Comments
 (0)