Skip to content

Commit 8a987ad

Browse files
committed
[ADD] sale_product_multiple_qty
1 parent 1c4cb48 commit 8a987ad

File tree

14 files changed

+900
-0
lines changed

14 files changed

+900
-0
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
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%2Fsale--workflow-lightgray.png?logo=github
24+
:target: https://github.com/OCA/sale-workflow/tree/19.0/sale_product_multiple_qty
25+
:alt: OCA/sale-workflow
26+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
27+
:target: https://translation.odoo-community.org/projects/sale-workflow-19-0/sale-workflow-19-0-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/sale-workflow&target_branch=19.0
31+
:alt: Try me on Runboat
32+
33+
|badge1| |badge2| |badge3| |badge4| |badge5|
34+
35+
Sales Product Multiple Quantity
36+
===============================
37+
38+
This module adds a **Sales Multiple** unit of measure on products.
39+
40+
When a Sales Multiple is set, sales order line quantities are
41+
automatically rounded **UP** to the nearest multiple of the selected
42+
unit of measure. This is useful for products that must be sold in fixed
43+
pack sizes (boxes, bundles, pallets, etc.).
44+
45+
The rounding is performed by converting the entered quantity to the
46+
Sales Multiple UoM, rounding the number of packs **UP**, and converting
47+
the result back to the order line UoM.
48+
49+
For example, with a Sales Multiple of *Pack of 100*:
50+
51+
- ordering 15 packs of 5 units (75 units) is rounded to 20 packs (100
52+
units);
53+
- ordering 55 packs of 5 units (275 units) is rounded to 60 packs (300
54+
units).
55+
56+
If the Sales Multiple UoM is not divisible by the order line UoM, the
57+
rounded quantity may be fractional. It is the user's responsibility to
58+
configure compatible units of measure.
59+
60+
**Table of contents**
61+
62+
.. contents::
63+
:local:
64+
65+
Usage
66+
=====
67+
68+
Usage
69+
=====
70+
71+
To use this module:
72+
73+
1. Create one or more packaging UoMs (for example, *Pack of 100*), using
74+
the same reference unit as the product base UoM.
75+
2. On the product form, set the **Sales Multiple** field to the desired
76+
UoM.
77+
3. When entering quantities on a sales order line, the quantity is
78+
automatically rounded **UP** to the nearest valid multiple.
79+
80+
If no Sales Multiple is set on the product, no rounding is applied.
81+
82+
Bug Tracker
83+
===========
84+
85+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-workflow/issues>`_.
86+
In case of trouble, please check there if your issue has already been reported.
87+
If you spotted it first, help us to smash it by providing a detailed and welcomed
88+
`feedback <https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_product_multiple_qty%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
89+
90+
Do not contact contributors directly about support or help with technical issues.
91+
92+
Credits
93+
=======
94+
95+
Authors
96+
-------
97+
98+
* Camptocamp SA
99+
100+
Contributors
101+
------------
102+
103+
- `Camptocamp <https://www.camptocamp.com>`__:
104+
105+
- Maksym Yankin <maksym.yankin@camptocamp.com>
106+
- Ivan Todorovich <ivan.todorovich@camptocamp.com>
107+
- Gaëtan Vaujour <gaetan.vaujour@camptocamp.com>
108+
109+
Maintainers
110+
-----------
111+
112+
This module is maintained by the OCA.
113+
114+
.. image:: https://odoo-community.org/logo.png
115+
:alt: Odoo Community Association
116+
:target: https://odoo-community.org
117+
118+
OCA, or the Odoo Community Association, is a nonprofit organization whose
119+
mission is to support the collaborative development of Odoo features and
120+
promote its widespread use.
121+
122+
.. |maintainer-yankinmax| image:: https://github.com/yankinmax.png?size=40px
123+
:target: https://github.com/yankinmax
124+
:alt: yankinmax
125+
126+
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
127+
128+
|maintainer-yankinmax|
129+
130+
This module is part of the `OCA/sale-workflow <https://github.com/OCA/sale-workflow/tree/19.0/sale_product_multiple_qty>`_ project on GitHub.
131+
132+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
{
4+
"name": "Sale Product Multiple Qty",
5+
"summary": "Allows defining a sales quantity multiple on products.",
6+
"version": "19.0.1.0.0",
7+
"category": "Sales",
8+
"website": "https://github.com/OCA/sale-workflow",
9+
"author": "Camptocamp SA, Odoo Community Association (OCA)",
10+
"license": "AGPL-3",
11+
"installable": True,
12+
"depends": ["sale"],
13+
"maintainers": ["yankinmax"],
14+
"data": [
15+
# Views
16+
"views/product_view.xml",
17+
],
18+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import product_product
2+
from . import sale_order_line
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
from odoo import fields, models
4+
5+
6+
class ProductProduct(models.Model):
7+
_inherit = "product.product"
8+
9+
sale_multiple_uom_id = fields.Many2one(
10+
comodel_name="uom.uom",
11+
string="Sales Multiple",
12+
help="When set, sale order quantities are rounded up to an "
13+
"multiple number of this unit.",
14+
)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
from odoo import api, fields, models
4+
5+
6+
class SaleOrderLine(models.Model):
7+
_inherit = "sale.order.line"
8+
9+
def _round_sale_qty_to_multiple(self, qty_to_order: float) -> float:
10+
"""Round the order line quantity to a multiple of the sales multiple UoM.
11+
12+
:param qty_to_order: quantity expressed in the order line UoM (product_uom_id).
13+
:return: rounded quantity expressed in the order line UoM (product_uom_id).
14+
15+
This method is inspired by
16+
``stock.warehouse.orderpoint::_get_multiple_rounded_qty``.
17+
Round using "UP" strategy to the nearest multiple of
18+
product_id.sale_multiple_uom_id:
19+
20+
- Convert qty_to_order from the product_uom_id to the
21+
product_id.sale_multiple_uom_id
22+
- Round the quantity using "UP" strategy
23+
- Convert back to the product_uom_id quantity
24+
25+
Being said, for the UoMs which share common reference unit (e.g. Units)
26+
the sales multiple UoM is divisible by the order line UoM
27+
and the division result is usually an integer.
28+
But for dimensional UoMs like (e.g. Kg(s), Meter(s)) the rounding result
29+
can be a floating point number which is perfectly acceptable.
30+
31+
For example (compatible UoMs: 100 is divisible by 5):
32+
- order line UoM: Pack of 5 (5 units)
33+
- sales multiple UoM: Pack of 100 (100 units)
34+
- qty_to_order = 15 packs are rounded to 20 packs (100 units):
35+
15 packs of 5 units = 75 units
36+
-> 1 pack of 100 = 100 units -> 20 packs of 5 units
37+
- qty_to_order = 55 packs are rounded to 60 packs (300 units):
38+
55 packs of 5 units = 275 units
39+
-> 3 packs of 100 = 300 units -> 60 packs of 5 units
40+
41+
For example (fractional result: 100 is not divisible by 6):
42+
- order line UoM: Pack of 6 (6 units)
43+
- sales multiple UoM: Pack of 100 (100 units)
44+
- qty_to_order = 13 packs are rounded to 16.67 packs (100.02 units):
45+
13 packs of 6 units = 78 units -> 1.0 pack of 100 = 100 units
46+
-> 16.6667 packs of 6 units
47+
"""
48+
self.ensure_one()
49+
multiple_uom = self.product_id.sale_multiple_uom_id
50+
packs = self.product_uom_id._compute_quantity(qty_to_order, multiple_uom)
51+
packs = fields.Float.round(packs, precision_digits=0, rounding_method="UP")
52+
qty_rounded = multiple_uom._compute_quantity(packs, self.product_uom_id)
53+
return qty_rounded
54+
55+
@api.onchange("product_id", "product_uom_qty", "product_uom_id")
56+
def _onchange_product_uom_qty_round_multiple(self):
57+
"""Round product_uom_qty to a multiple of the sales multiple UoM.
58+
59+
If sales multiple UoM is set on the product, this onchange rounds
60+
the order line quantity to the nearest multiple of that UoM.
61+
"""
62+
for line in self:
63+
multiple_uom = line.product_id.sale_multiple_uom_id
64+
if not multiple_uom:
65+
continue
66+
67+
qty_to_order = line.product_uom_qty or 0.0
68+
if qty_to_order <= 0:
69+
continue
70+
rounded_qty = line._round_sale_qty_to_multiple(qty_to_order)
71+
# ``uom.uom::compare`` return -1, 0 or 1, if ``rounded_qty``
72+
# is lower than, equal to, or greater than ``qty_to_order``
73+
qty_diff = line.product_uom_id.compare(rounded_qty, qty_to_order)
74+
if qty_diff != 0:
75+
line.product_uom_qty = rounded_qty
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>\>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Sales Product Multiple Quantity
2+
===============================
3+
4+
This module adds a **Sales Multiple** unit of measure on products.
5+
6+
When a Sales Multiple is set, sales order line quantities are automatically
7+
rounded **UP** to the nearest multiple of the selected unit of measure.
8+
This is useful for products that must be sold in fixed pack sizes
9+
(boxes, bundles, pallets, etc.).
10+
11+
The rounding is performed by converting the entered quantity to the Sales
12+
Multiple UoM, rounding the number of packs **UP**, and converting the result back to the order line UoM.
13+
14+
For example, with a Sales Multiple of *Pack of 100*:
15+
- ordering 15 packs of 5 units (75 units) is rounded to 20 packs (100 units);
16+
- ordering 55 packs of 5 units (275 units) is rounded to 60 packs (300 units).
17+
18+
If the Sales Multiple UoM is not divisible by the order line UoM, the rounded
19+
quantity may be fractional. It is the user's responsibility to
20+
configure compatible units of measure.
21+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Usage
2+
=====
3+
4+
To use this module:
5+
6+
1. Create one or more packaging UoMs (for example, *Pack of 100*), using the
7+
same reference unit as the product base UoM.
8+
2. On the product form, set the **Sales Multiple** field to the desired UoM.
9+
3. When entering quantities on a sales order line, the quantity is
10+
automatically rounded **UP** to the nearest valid multiple.
11+
12+
If no Sales Multiple is set on the product, no rounding is applied.

0 commit comments

Comments
 (0)