Merge PR #916 into 13.0

Signed-off-by simahawk
This commit is contained in:
OCA-git-bot
2020-06-09 07:48:01 +00:00
4 changed files with 188 additions and 61 deletions

View File

@@ -1,43 +1,65 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from collections import namedtuple
from odoo import models
from odoo.tools import float_compare
# Unify records as we mix up w/ UoM
Packaging = namedtuple("Packaging", "id name qty")
class Product(models.Model):
_inherit = "product.product"
def product_qty_by_packaging(self, prod_qty, min_unit=None):
def product_qty_by_packaging(self, prod_qty, with_contained=False):
"""Calculate quantity by packaging.
The minimal quantity is always represented by the UoM of the product.
Limitation: fractional quantities are lost.
:prod_qty: total qty to satisfy.
:min_unit: minimal unit of measure as a tuple (qty, name).
Default: to UoM unit.
:returns: list of tuple in the form [(qty_per_package, package_name)]
:with_contained: include calculation of contained packagings.
eg: 1 pallet contains 4 big boxes and 6 little boxes.
:returns: list of dict in the form
[{id: 1, qty: qty_per_package, name: package_name}]
If `with_contained` is passed, each element will include
the quantity of smaller packaging, like:
{contained: [{id: 1, qty: 4, name: "Big box"}]}
"""
packagings = [(x.qty, x.name) for x in self.packaging_ids]
if min_unit is None:
# You can pass `False` to skip it.
single_unit = self.uom_id
min_unit = (single_unit.factor, single_unit.name)
if min_unit:
packagings.append(min_unit)
packagings = [Packaging(x.id, x.name, x.qty) for x in self.packaging_ids]
# Add minimal unit
packagings.append(
# NOTE: the ID here could clash w/ one of the packaging's.
# If you create a mapping based on IDs, keep this in mind.
Packaging(self.uom_id.id, self.uom_id.name, self.uom_id.factor)
)
return self._product_qty_by_packaging(
sorted(packagings, reverse=True), prod_qty
sorted(packagings, reverse=True, key=lambda x: x.qty),
prod_qty,
with_contained=with_contained,
)
def _product_qty_by_packaging(self, pkg_by_qty, qty):
"""Produce a list of tuple of packaging qty and packaging name."""
def _product_qty_by_packaging(self, pkg_by_qty, qty, with_contained=False):
"""Produce a list of dictionaries of packaging info."""
# TODO: refactor to handle fractional quantities (eg: 0.5 Kg)
res = []
for pkg_qty, pkg in pkg_by_qty:
qty_per_pkg, qty = self._qty_by_pkg(pkg_qty, qty)
for i, pkg in enumerate(pkg_by_qty):
qty_per_pkg, qty = self._qty_by_pkg(pkg.qty, qty)
if qty_per_pkg:
res.append((qty_per_pkg, pkg))
value = {"id": pkg.id, "qty": qty_per_pkg, "name": pkg.name}
if with_contained:
value["contained"] = self._product_qty_by_packaging(
pkg_by_qty[i + 1 :], pkg.qty
)
res.append(value)
if not qty:
break
return res
@@ -45,7 +67,10 @@ class Product(models.Model):
def _qty_by_pkg(self, pkg_qty, qty):
"""Calculate qty needed for given package qty."""
qty_per_pkg = 0
while float_compare(qty - pkg_qty, 0.0, precision_digits=3) >= 0.0:
while (
float_compare(qty - pkg_qty, 0.0, precision_digits=self.uom_id.rounding)
>= 0.0
):
qty -= pkg_qty
qty_per_pkg += 1
return qty_per_pkg, qty

View File

@@ -1,18 +1 @@
Basic module providing an helper method to calculate the quantity of product by packaging.
Imagine you have the following packagings:
* Pallet: 1000 Units
* Big box: 500 Units
* Box: 50 Units
and you have to pick from your warehouse 2860 Units.
Then you can do:
>>> product.product_qty_by_packaging(2860)
[(2, "Pallet"), (1, "Big Box"), (7, "Box"), (10, "Units")]
With this you can show a proper message to warehouse operators to quickly pick the quantity they need.

View File

@@ -0,0 +1,36 @@
Imagine you have the following packagings:
* Pallet: 1000 Units
* Big box: 500 Units
* Box: 50 Units
and you have to pick from your warehouse 2860 Units.
Then you can do:
.. code-block::
>>> product.product_qty_by_packaging(2860)
[
{"id": 1, "qty": 2, "name": "Pallet"},
{"id": 2, "qty": 1, "name": "Big box"},
{"id": 3, "qty": 7, "name": "Box"},
{"id": 100, "qty": 10, "name": "Units"},
]
With this you can show a proper message to warehouse operators to quickly pick the quantity they need.
Optionally you can get contained packaging by passing `with_contained` flag:
.. code-block::
>>> product.product_qty_by_packaging(2860, with_contained=True)
[
{"id": 1, "qty": 2, "name": "Pallet", "contained": [{"id": 2, "qty": 2, "name": "Big box"}]},
{"id": 2, "qty": 1, "name": "Big box", "contained": [{"id": 3, "qty": 10, "name": "Box"}]},
{"id": 3, "qty": 7, "name": "Box", "contained": [{"id": 100, "qty": 50, "name": "Units"}]},
{"id": 100, "qty": 10, "name": "Units", "contained": []},},
]

View File

@@ -9,7 +9,6 @@ class TestCalc(SavepointCase):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
cls.uom_kg = cls.env.ref("uom.product_uom_kgm")
cls.product_a = cls.env["product.product"].create(
{
"name": "Product A",
@@ -30,37 +29,121 @@ class TestCalc(SavepointCase):
def test_calc_1(self):
"""Test easy behavior 1."""
self.assertEqual(
self.product_a.product_qty_by_packaging(2655),
[(1, "Pallet"), (3, "Big Box"), (1, "Box"), (5, self.uom_unit.name)],
)
expected = [
{"id": self.pkg_pallet.id, "qty": 1, "name": self.pkg_pallet.name},
{"id": self.pkg_big_box.id, "qty": 3, "name": self.pkg_big_box.name},
{"id": self.pkg_box.id, "qty": 1, "name": self.pkg_box.name},
{"id": self.uom_unit.id, "qty": 5, "name": self.uom_unit.name},
]
self.assertEqual(self.product_a.product_qty_by_packaging(2655), expected)
def test_calc_2(self):
"""Test easy behavior 2."""
self.assertEqual(
self.product_a.product_qty_by_packaging(350), [(1, "Big Box"), (3, "Box")]
)
expected = [
{"id": self.pkg_big_box.id, "qty": 1, "name": self.pkg_big_box.name},
{"id": self.pkg_box.id, "qty": 3, "name": self.pkg_box.name},
]
self.assertEqual(self.product_a.product_qty_by_packaging(350), expected)
def test_calc_3(self):
"""Test easy behavior 3."""
self.assertEqual(
self.product_a.product_qty_by_packaging(80),
[(1, "Box"), (30, self.uom_unit.name)],
)
def test_calc_4(self):
"""Test minimal unit override."""
self.assertEqual(
self.product_a.product_qty_by_packaging(80, min_unit=(5, "Pack 5")),
[(1, "Box"), (6, "Pack 5")],
)
def test_calc_5(self):
"""Test no minimal unit."""
self.assertEqual(
self.product_a.product_qty_by_packaging(80, min_unit=False), [(1, "Box")]
)
expected = [
{"id": self.pkg_box.id, "qty": 1, "name": self.pkg_box.name},
{"id": self.uom_unit.id, "qty": 30, "name": self.uom_unit.name},
]
self.assertEqual(self.product_a.product_qty_by_packaging(80), expected)
def test_calc_6(self):
"""Test fractional qty is lost."""
self.assertEqual(self.product_a.product_qty_by_packaging(50.5), [(1, "Box")])
expected = [
{"id": self.pkg_box.id, "qty": 1, "name": self.pkg_box.name},
]
self.assertEqual(self.product_a.product_qty_by_packaging(50.5), expected)
def test_calc_sub1(self):
"""Test contained packaging behavior 1."""
expected = [
{
"id": self.pkg_pallet.id,
"qty": 1,
"name": self.pkg_pallet.name,
"contained": [
{
"id": self.pkg_big_box.id,
"qty": 10,
"name": self.pkg_big_box.name,
},
],
},
{
"id": self.pkg_big_box.id,
"qty": 3,
"name": self.pkg_big_box.name,
"contained": [
{"id": self.pkg_box.id, "qty": 4, "name": self.pkg_box.name},
],
},
{
"id": self.pkg_box.id,
"qty": 1,
"name": self.pkg_box.name,
"contained": [
{"id": self.uom_unit.id, "qty": 50, "name": self.uom_unit.name},
],
},
{
"id": self.uom_unit.id,
"qty": 5,
"name": self.uom_unit.name,
"contained": [],
},
]
self.assertEqual(
self.product_a.product_qty_by_packaging(2655, with_contained=True),
expected,
)
def test_calc_sub2(self):
"""Test contained packaging behavior 1."""
self.pkg_box.qty = 30
expected = [
{
"id": self.pkg_pallet.id,
"qty": 1,
"name": self.pkg_pallet.name,
"contained": [
{
"id": self.pkg_big_box.id,
"qty": 10,
"name": self.pkg_big_box.name,
},
],
},
{
"id": self.pkg_big_box.id,
"qty": 3,
"name": self.pkg_big_box.name,
"contained": [
{"id": self.pkg_box.id, "qty": 6, "name": self.pkg_box.name},
{"id": self.uom_unit.id, "qty": 20, "name": self.uom_unit.name},
],
},
{
"id": self.pkg_box.id,
"qty": 1,
"name": self.pkg_box.name,
"contained": [
{"id": self.uom_unit.id, "qty": 30, "name": self.uom_unit.name},
],
},
{
"id": self.uom_unit.id,
"qty": 25,
"name": self.uom_unit.name,
"contained": [],
},
]
self.assertEqual(
self.product_a.product_qty_by_packaging(2655, with_contained=True),
expected,
)