From 4f156d449eeee290ba1dd8b759c15c9e6c6192d4 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 11 May 2021 16:09:28 +0200 Subject: [PATCH 1/9] s_packaging_calculator: include barcode --- stock_packaging_calculator/models/product.py | 7 +- .../tests/test_packaging_calc.py | 322 ++++-------------- stock_packaging_calculator/tests/utils.py | 24 ++ 3 files changed, 99 insertions(+), 254 deletions(-) create mode 100644 stock_packaging_calculator/tests/utils.py diff --git a/stock_packaging_calculator/models/product.py b/stock_packaging_calculator/models/product.py index a4327076c..13ae10f3d 100644 --- a/stock_packaging_calculator/models/product.py +++ b/stock_packaging_calculator/models/product.py @@ -9,7 +9,7 @@ from odoo.tools import float_compare from odoo.addons.base_sparse_field.models.fields import Serialized # Unify records as we mix up w/ UoM -Packaging = namedtuple("Packaging", "id name qty is_unit") +Packaging = namedtuple("Packaging", "id name qty barcode is_unit") class Product(models.Model): @@ -83,7 +83,7 @@ class Product(models.Model): name_getter = self.env.context.get("_packaging_name_getter", lambda x: x.name) packagings = sorted( [ - Packaging(x.id, name_getter(x), x.qty, False) + Packaging(x.id, name_getter(x), x.qty, x.barcode, False) for x in self.packaging_ids.filtered(custom_filter) # Exclude the ones w/ zero qty as they are useless for the math if x.qty @@ -96,7 +96,7 @@ class Product(models.Model): # NOTE: the ID here could clash w/ one of the packaging's. # If you create a mapping based on IDs, keep this in mind. # You can use `is_unit` to check this. - Packaging(self.uom_id.id, self.uom_id.name, self.uom_id.factor, True) + Packaging(self.uom_id.id, self.uom_id.name, self.uom_id.factor, None, True) ) return packagings @@ -140,4 +140,5 @@ class Product(models.Model): "qty": qty_per_pkg, "name": packaging.name, "is_unit": packaging.is_unit, + "barcode": packaging.barcode, } diff --git a/stock_packaging_calculator/tests/test_packaging_calc.py b/stock_packaging_calculator/tests/test_packaging_calc.py index 1bc721086..99470229b 100644 --- a/stock_packaging_calculator/tests/test_packaging_calc.py +++ b/stock_packaging_calculator/tests/test_packaging_calc.py @@ -2,11 +2,14 @@ # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) from odoo.tests import SavepointCase +from .utils import make_pkg_values + class TestCalc(SavepointCase): at_install = False post_install = True + maxDiff = None @classmethod def setUpClass(cls): @@ -21,43 +24,32 @@ class TestCalc(SavepointCase): } ) cls.pkg_box = cls.env["product.packaging"].create( - {"name": "Box", "product_id": cls.product_a.id, "qty": 50} + {"name": "Box", "product_id": cls.product_a.id, "qty": 50, "barcode": "BOX"} ) cls.pkg_big_box = cls.env["product.packaging"].create( - {"name": "Big Box", "product_id": cls.product_a.id, "qty": 200} + { + "name": "Big Box", + "product_id": cls.product_a.id, + "qty": 200, + "barcode": "BIGBOX", + } ) cls.pkg_pallet = cls.env["product.packaging"].create( - {"name": "Pallet", "product_id": cls.product_a.id, "qty": 2000} + { + "name": "Pallet", + "product_id": cls.product_a.id, + "qty": 2000, + "barcode": "PALLET", + } ) def test_contained_mapping(self): self.assertEqual( self.product_a.packaging_contained_mapping, { - str(self.pkg_pallet.id): [ - { - "id": self.pkg_big_box.id, - "qty": 10, - "name": self.pkg_big_box.name, - "is_unit": False, - }, - ], - str(self.pkg_big_box.id): [ - { - "id": self.pkg_box.id, - "qty": 4, - "name": self.pkg_box.name, - "is_unit": False, - }, - ], - str(self.pkg_box.id): [ - { - "id": self.uom_unit.id, - "qty": 50, - "name": self.uom_unit.name, - "is_unit": True, - }, - ], + str(self.pkg_pallet.id): [make_pkg_values(self.pkg_big_box, qty=10)], + str(self.pkg_big_box.id): [make_pkg_values(self.pkg_box, qty=4)], + str(self.pkg_box.id): [make_pkg_values(self.uom_unit, qty=50)], }, ) # Update pkg qty @@ -65,132 +57,51 @@ class TestCalc(SavepointCase): self.assertEqual( self.product_a.packaging_contained_mapping, { - str(self.pkg_pallet.id): [ - { - "id": self.pkg_big_box.id, - "qty": 20, - "name": self.pkg_big_box.name, - "is_unit": False, - }, - ], - str(self.pkg_big_box.id): [ - { - "id": self.pkg_box.id, - "qty": 4, - "name": self.pkg_box.name, - "is_unit": False, - }, - ], - str(self.pkg_box.id): [ - { - "id": self.uom_unit.id, - "qty": 50, - "name": self.uom_unit.name, - "is_unit": True, - }, - ], + str(self.pkg_pallet.id): [make_pkg_values(self.pkg_big_box, qty=20)], + str(self.pkg_big_box.id): [make_pkg_values(self.pkg_box, qty=4)], + str(self.pkg_box.id): [make_pkg_values(self.uom_unit, qty=50)], }, ) def test_calc_1(self): """Test easy behavior 1.""" expected = [ - { - "id": self.pkg_pallet.id, - "qty": 1, - "name": self.pkg_pallet.name, - "is_unit": False, - }, - { - "id": self.pkg_big_box.id, - "qty": 3, - "name": self.pkg_big_box.name, - "is_unit": False, - }, - { - "id": self.pkg_box.id, - "qty": 1, - "name": self.pkg_box.name, - "is_unit": False, - }, - { - "id": self.uom_unit.id, - "qty": 5, - "name": self.uom_unit.name, - "is_unit": True, - }, + make_pkg_values(self.pkg_pallet, qty=1), + make_pkg_values(self.pkg_big_box, qty=3), + make_pkg_values(self.pkg_box, qty=1), + make_pkg_values(self.uom_unit, qty=5), ] self.assertEqual(self.product_a.product_qty_by_packaging(2655), expected) def test_calc_2(self): """Test easy behavior 2.""" expected = [ - { - "id": self.pkg_big_box.id, - "qty": 1, - "name": self.pkg_big_box.name, - "is_unit": False, - }, - { - "id": self.pkg_box.id, - "qty": 3, - "name": self.pkg_box.name, - "is_unit": False, - }, + make_pkg_values(self.pkg_big_box, qty=1), + make_pkg_values(self.pkg_box, qty=3), ] self.assertEqual(self.product_a.product_qty_by_packaging(350), expected) def test_calc_3(self): """Test easy behavior 3.""" expected = [ - { - "id": self.pkg_box.id, - "qty": 1, - "name": self.pkg_box.name, - "is_unit": False, - }, - { - "id": self.uom_unit.id, - "qty": 30, - "name": self.uom_unit.name, - "is_unit": True, - }, + make_pkg_values(self.pkg_box, qty=1), + make_pkg_values(self.uom_unit, qty=30), ] self.assertEqual(self.product_a.product_qty_by_packaging(80), expected) def test_calc_6(self): """Test fractional qty is lost.""" expected = [ - { - "id": self.pkg_box.id, - "qty": 1, - "name": self.pkg_box.name, - "is_unit": False, - }, + make_pkg_values(self.pkg_box, qty=1), ] self.assertEqual(self.product_a.product_qty_by_packaging(50.5), expected) def test_calc_filter(self): """Test packaging filter.""" expected = [ - { - "id": self.pkg_big_box.id, - "qty": 13, - "name": self.pkg_big_box.name, - "is_unit": False, - }, - { - "id": self.pkg_box.id, - "qty": 1, - "name": self.pkg_box.name, - "is_unit": False, - }, - { - "id": self.uom_unit.id, - "qty": 5, - "name": self.uom_unit.name, - "is_unit": True, - }, + make_pkg_values(self.pkg_big_box, qty=13), + make_pkg_values(self.pkg_box, qty=1), + make_pkg_values(self.uom_unit, qty=5), ] self.assertEqual( self.product_a.with_context( @@ -202,30 +113,12 @@ class TestCalc(SavepointCase): def test_calc_name_get(self): """Test custom name getter.""" expected = [ - { - "id": self.pkg_pallet.id, - "qty": 1, - "name": "FOO " + self.pkg_pallet.name, - "is_unit": False, - }, - { - "id": self.pkg_big_box.id, - "qty": 3, - "name": "FOO " + self.pkg_big_box.name, - "is_unit": False, - }, - { - "id": self.pkg_box.id, - "qty": 1, - "name": "FOO " + self.pkg_box.name, - "is_unit": False, - }, - { - "id": self.uom_unit.id, - "qty": 5, - "name": self.uom_unit.name, - "is_unit": True, - }, + make_pkg_values(self.pkg_pallet, qty=1, name="FOO " + self.pkg_pallet.name), + make_pkg_values( + self.pkg_big_box, qty=3, name="FOO " + self.pkg_big_box.name + ), + make_pkg_values(self.pkg_box, qty=1, name="FOO " + self.pkg_box.name), + make_pkg_values(self.uom_unit, qty=5, name=self.uom_unit.name), ] self.assertEqual( self.product_a.with_context( @@ -255,55 +148,20 @@ class TestCalc(SavepointCase): def test_calc_sub1(self): """Test contained packaging behavior 1.""" expected = [ - { - "id": self.pkg_pallet.id, - "qty": 1, - "name": self.pkg_pallet.name, - "is_unit": False, - "contained": [ - { - "id": self.pkg_big_box.id, - "qty": 10, - "name": self.pkg_big_box.name, - "is_unit": False, - }, - ], - }, - { - "id": self.pkg_big_box.id, - "qty": 3, - "name": self.pkg_big_box.name, - "is_unit": False, - "contained": [ - { - "id": self.pkg_box.id, - "qty": 4, - "name": self.pkg_box.name, - "is_unit": False, - }, - ], - }, - { - "id": self.pkg_box.id, - "qty": 1, - "name": self.pkg_box.name, - "is_unit": False, - "contained": [ - { - "id": self.uom_unit.id, - "qty": 50, - "name": self.uom_unit.name, - "is_unit": True, - }, - ], - }, - { - "id": self.uom_unit.id, - "qty": 5, - "name": self.uom_unit.name, - "is_unit": True, - "contained": None, - }, + make_pkg_values( + self.pkg_pallet, + qty=1, + contained=[make_pkg_values(self.pkg_big_box, qty=10)], + ), + make_pkg_values( + self.pkg_big_box, + qty=3, + contained=[make_pkg_values(self.pkg_box, qty=4)], + ), + make_pkg_values( + self.pkg_box, qty=1, contained=[make_pkg_values(self.uom_unit, qty=50)], + ), + make_pkg_values(self.uom_unit, qty=5, contained=None), ] self.assertEqual( self.product_a.product_qty_by_packaging(2655, with_contained=True), @@ -311,64 +169,26 @@ class TestCalc(SavepointCase): ) def test_calc_sub2(self): - """Test contained packaging behavior 1.""" + """Test contained packaging behavior 2.""" self.pkg_box.qty = 30 expected = [ - { - "id": self.pkg_pallet.id, - "qty": 1, - "name": self.pkg_pallet.name, - "is_unit": False, - "contained": [ - { - "id": self.pkg_big_box.id, - "qty": 10, - "name": self.pkg_big_box.name, - "is_unit": False, - }, + make_pkg_values( + self.pkg_pallet, + qty=1, + contained=[make_pkg_values(self.pkg_big_box, qty=10)], + ), + make_pkg_values( + self.pkg_big_box, + qty=3, + contained=[ + make_pkg_values(self.pkg_box, qty=6), + make_pkg_values(self.uom_unit, qty=20), ], - }, - { - "id": self.pkg_big_box.id, - "qty": 3, - "name": self.pkg_big_box.name, - "is_unit": False, - "contained": [ - { - "id": self.pkg_box.id, - "qty": 6, - "name": self.pkg_box.name, - "is_unit": False, - }, - { - "id": self.uom_unit.id, - "qty": 20, - "name": self.uom_unit.name, - "is_unit": True, - }, - ], - }, - { - "id": self.pkg_box.id, - "qty": 1, - "name": self.pkg_box.name, - "is_unit": False, - "contained": [ - { - "id": self.uom_unit.id, - "qty": 30, - "name": self.uom_unit.name, - "is_unit": True, - }, - ], - }, - { - "id": self.uom_unit.id, - "qty": 25, - "name": self.uom_unit.name, - "is_unit": True, - "contained": None, - }, + ), + make_pkg_values( + self.pkg_box, qty=1, contained=[make_pkg_values(self.uom_unit, qty=30)], + ), + make_pkg_values(self.uom_unit, qty=25, contained=None), ] self.assertEqual( self.product_a.product_qty_by_packaging(2655, with_contained=True), diff --git a/stock_packaging_calculator/tests/utils.py b/stock_packaging_calculator/tests/utils.py new file mode 100644 index 000000000..cd9e12560 --- /dev/null +++ b/stock_packaging_calculator/tests/utils.py @@ -0,0 +1,24 @@ +# Copyright 2021 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) + + +def make_pkg_values(record, **kw): + """Helper to generate test values for packaging. + """ + if record._name == "uom.uom": + is_unit = True + barcode = None + qty = record.factor + elif record._name == "product.packaging": + qty = record.qty + is_unit = False + barcode = record.barcode + values = { + "id": record.id, + "name": record.name, + "qty": qty, + "barcode": barcode, + "is_unit": is_unit, + } + values.update(kw) + return values From 7b7cacc7f65a662c5e88b6632ff4868b6b3c0df5 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 28 May 2021 09:03:14 +0200 Subject: [PATCH 2/9] s_packaging_calculator: ease override of packaging name --- stock_packaging_calculator/models/product.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/stock_packaging_calculator/models/product.py b/stock_packaging_calculator/models/product.py index 13ae10f3d..cdf83edf9 100644 --- a/stock_packaging_calculator/models/product.py +++ b/stock_packaging_calculator/models/product.py @@ -80,7 +80,9 @@ class Product(models.Model): the display name of the packaging. """ custom_filter = self.env.context.get("_packaging_filter", lambda x: x) - name_getter = self.env.context.get("_packaging_name_getter", lambda x: x.name) + name_getter = self.env.context.get( + "_packaging_name_getter", self._packaging_name_getter + ) packagings = sorted( [ Packaging(x.id, name_getter(x), x.qty, x.barcode, False) @@ -100,6 +102,9 @@ class Product(models.Model): ) return packagings + def _packaging_name_getter(self, packaging): + return 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) From 5d44d558f644eff09929548b515abd87af7a7ed0 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 28 May 2021 09:04:51 +0200 Subject: [PATCH 3/9] s_packaging_calculator: add product_qty_by_packaging_as_str Retrieve quickly packagin bty qty as a string. --- stock_packaging_calculator/models/product.py | 58 +++++++++++++++++++ stock_packaging_calculator/tests/__init__.py | 1 + stock_packaging_calculator/tests/common.py | 42 ++++++++++++++ .../tests/test_packaging_calc.py | 42 +------------- .../tests/test_pkg_qty_str.py | 50 ++++++++++++++++ 5 files changed, 153 insertions(+), 40 deletions(-) create mode 100644 stock_packaging_calculator/tests/common.py create mode 100644 stock_packaging_calculator/tests/test_pkg_qty_str.py diff --git a/stock_packaging_calculator/models/product.py b/stock_packaging_calculator/models/product.py index cdf83edf9..026c7971b 100644 --- a/stock_packaging_calculator/models/product.py +++ b/stock_packaging_calculator/models/product.py @@ -1,6 +1,8 @@ # Copyright 2020 Camptocamp SA +# @author: Simone Orsi # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) +import unicodedata from collections import namedtuple from odoo import api, models @@ -11,6 +13,8 @@ from odoo.addons.base_sparse_field.models.fields import Serialized # Unify records as we mix up w/ UoM Packaging = namedtuple("Packaging", "id name qty barcode is_unit") +NO_BREAK_SPACE_CHAR = unicodedata.lookup("NO-BREAK SPACE") + class Product(models.Model): _inherit = "product.product" @@ -147,3 +151,57 @@ class Product(models.Model): "is_unit": packaging.is_unit, "barcode": packaging.barcode, } + + def product_qty_by_packaging_as_str(self, prod_qty, include_total_units=False): + """Return a string representing the qty of each packaging. + + :param prod_qty: the qty of current product to translate to pkg qty + :param include_total_units: includes total qty required initially + """ + self.ensure_one() + if not prod_qty: + return "" + + qty_by_packaging = self.product_qty_by_packaging(prod_qty) + if not qty_by_packaging: + return "" + + # Exclude unit qty and reuse it later + unit_qty = None + has_only_units = True + _qty_by_packaging = [] + for pkg_qty in qty_by_packaging: + if pkg_qty["is_unit"]: + unit_qty = pkg_qty["qty"] + continue + has_only_units = False + _qty_by_packaging.append(pkg_qty) + # Browse them all at once + records = self.env["product.packaging"].browse( + [x["id"] for x in _qty_by_packaging] + ) + _qty_by_packaging_as_str = self.env.context.get( + "_qty_by_packaging_as_str", self._qty_by_packaging_as_str + ) + # Collect all strings representations + as_string = [] + for record, info in zip(records, _qty_by_packaging): + bit = _qty_by_packaging_as_str(record, info["qty"]) + if bit: + as_string.append(bit) + # Restore unit information if any. + # Skip it if we get only units in the count. + if unit_qty and not has_only_units: + as_string.append(f"{unit_qty} {self.uom_id.name}") + # We want to avoid line break here as this string + # can be used by reports + res = f",{NO_BREAK_SPACE_CHAR}".join(as_string) + if include_total_units and not has_only_units: + res += " " + self._qty_by_packaging_total_units(prod_qty) + return res + + def _qty_by_packaging_as_str(self, packaging, qty): + return f"{qty} {packaging.name}" + + def _qty_by_packaging_total_units(self, prod_qty): + return f"({prod_qty} {self.uom_id.name})" diff --git a/stock_packaging_calculator/tests/__init__.py b/stock_packaging_calculator/tests/__init__.py index 04c47e711..441177cdc 100644 --- a/stock_packaging_calculator/tests/__init__.py +++ b/stock_packaging_calculator/tests/__init__.py @@ -1 +1,2 @@ from . import test_packaging_calc +from . import test_pkg_qty_str diff --git a/stock_packaging_calculator/tests/common.py b/stock_packaging_calculator/tests/common.py new file mode 100644 index 000000000..c528958ce --- /dev/null +++ b/stock_packaging_calculator/tests/common.py @@ -0,0 +1,42 @@ +# Copyright 2020 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) +from odoo.tests import SavepointCase + + +class TestCommon(SavepointCase): + + at_install = False + post_install = True + maxDiff = None + + @classmethod + def setUpClass(cls): + 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.product_a = cls.env["product.product"].create( + { + "name": "Product A", + "uom_id": cls.uom_unit.id, + "uom_po_id": cls.uom_unit.id, + } + ) + cls.pkg_box = cls.env["product.packaging"].create( + {"name": "Box", "product_id": cls.product_a.id, "qty": 50, "barcode": "BOX"} + ) + cls.pkg_big_box = cls.env["product.packaging"].create( + { + "name": "Big Box", + "product_id": cls.product_a.id, + "qty": 200, + "barcode": "BIGBOX", + } + ) + cls.pkg_pallet = cls.env["product.packaging"].create( + { + "name": "Pallet", + "product_id": cls.product_a.id, + "qty": 2000, + "barcode": "PALLET", + } + ) diff --git a/stock_packaging_calculator/tests/test_packaging_calc.py b/stock_packaging_calculator/tests/test_packaging_calc.py index 99470229b..c51347e40 100644 --- a/stock_packaging_calculator/tests/test_packaging_calc.py +++ b/stock_packaging_calculator/tests/test_packaging_calc.py @@ -1,48 +1,10 @@ # Copyright 2020 Camptocamp SA # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) -from odoo.tests import SavepointCase - +from .common import TestCommon from .utils import make_pkg_values -class TestCalc(SavepointCase): - - at_install = False - post_install = True - maxDiff = None - - @classmethod - def setUpClass(cls): - 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.product_a = cls.env["product.product"].create( - { - "name": "Product A", - "uom_id": cls.uom_unit.id, - "uom_po_id": cls.uom_unit.id, - } - ) - cls.pkg_box = cls.env["product.packaging"].create( - {"name": "Box", "product_id": cls.product_a.id, "qty": 50, "barcode": "BOX"} - ) - cls.pkg_big_box = cls.env["product.packaging"].create( - { - "name": "Big Box", - "product_id": cls.product_a.id, - "qty": 200, - "barcode": "BIGBOX", - } - ) - cls.pkg_pallet = cls.env["product.packaging"].create( - { - "name": "Pallet", - "product_id": cls.product_a.id, - "qty": 2000, - "barcode": "PALLET", - } - ) - +class TestCalc(TestCommon): def test_contained_mapping(self): self.assertEqual( self.product_a.packaging_contained_mapping, diff --git a/stock_packaging_calculator/tests/test_pkg_qty_str.py b/stock_packaging_calculator/tests/test_pkg_qty_str.py new file mode 100644 index 000000000..93998846c --- /dev/null +++ b/stock_packaging_calculator/tests/test_pkg_qty_str.py @@ -0,0 +1,50 @@ +# Copyright 2021 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) +from .common import TestCommon + + +class TestAsStr(TestCommon): + def test_as_str(self): + self.assertEqual(self.product_a.product_qty_by_packaging_as_str(10), "") + self.assertEqual(self.product_a.product_qty_by_packaging_as_str(100), "2 Box") + self.assertEqual( + self.product_a.product_qty_by_packaging_as_str(250), "1 Big Box,\xa01 Box" + ) + self.assertEqual( + self.product_a.product_qty_by_packaging_as_str(255), + "1 Big Box,\xa01 Box,\xa05 Units", + ) + + def test_as_str_w_units(self): + self.assertEqual( + self.product_a.product_qty_by_packaging_as_str( + 10, include_total_units=True + ), + "", + ) + self.assertEqual( + self.product_a.product_qty_by_packaging_as_str( + 100, include_total_units=True + ), + "2 Box (100 Units)", + ) + self.assertEqual( + self.product_a.product_qty_by_packaging_as_str( + 250, include_total_units=True + ), + "1 Big Box,\xa01 Box (250 Units)", + ) + self.assertEqual( + self.product_a.product_qty_by_packaging_as_str( + 255, include_total_units=True + ), + "1 Big Box,\xa01 Box,\xa05 Units (255 Units)", + ) + + def test_as_str_custom_name(self): + self.assertEqual( + self.product_a.with_context( + _qty_by_packaging_as_str=lambda pkg, qty: f"{pkg.name} {qty} FOO" + ).product_qty_by_packaging_as_str(250), + "Big Box 1 FOO,\xa0Box 1 FOO", + ) From 6abf21292f056fab888633179810b0de31b99bb1 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 28 May 2021 08:59:14 +0200 Subject: [PATCH 4/9] s_packaging_calculator: add handy mixin This mixin can be used to provide qty by packaging features to any model. --- stock_packaging_calculator/models/__init__.py | 1 + .../models/product_qty_by_packaging_mixin.py | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 stock_packaging_calculator/models/product_qty_by_packaging_mixin.py diff --git a/stock_packaging_calculator/models/__init__.py b/stock_packaging_calculator/models/__init__.py index 9649db77a..fb73bb4d1 100644 --- a/stock_packaging_calculator/models/__init__.py +++ b/stock_packaging_calculator/models/__init__.py @@ -1 +1,2 @@ from . import product +from . import product_qty_by_packaging_mixin diff --git a/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py b/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py new file mode 100644 index 000000000..4721fe862 --- /dev/null +++ b/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py @@ -0,0 +1,48 @@ +# Copyright 2021 Camptocamp SA +# @author: Simone Orsi +# @author: Sébastien Alix +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) +from odoo import api, fields, models + + +class ProductQtyByPackagingMixin(models.AbstractModel): + """Allow displaying product qty by packaging. + """ + + _name = "product.qty_by_packaging.mixin" + _description = "Product Qty By Packaging (Mixin)" + + _qty_by_pkg__product_field_name = "product_id" + _qty_by_pkg__qty_field_name = None + + product_qty_by_packaging_display = fields.Char( + compute="_compute_product_qty_by_packaging_display", string="Qty by packaging" + ) + + def _product_qty_by_packaging_display_depends(self): + depends = [] + if self._qty_by_pkg__product_field_name: + depends.append(self._qty_by_pkg__product_field_name) + if self._qty_by_pkg__qty_field_name: + depends.append(self._qty_by_pkg__qty_field_name) + return depends + + @api.depends_context("lang", "qty_by_pkg_total_units") + @api.depends(lambda self: self._product_qty_by_packaging_display_depends()) + def _compute_product_qty_by_packaging_display(self): + include_total_units = self.env.context.get("qty_by_pkg_total_units", False) + for record in self: + value = "" + product = record._qty_by_packaging_get_product() + if product: + value = product.product_qty_by_packaging_as_str( + record._qty_by_packaging_get_qty(), + include_total_units=include_total_units, + ) + record.product_qty_by_packaging_display = value + + def _qty_by_packaging_get_product(self): + return self[self._qty_by_pkg__product_field_name] + + def _qty_by_packaging_get_qty(self): + return self[self._qty_by_pkg__qty_field_name] From df53d03f49ceda5994a569b5275af06299dbf5d5 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 3 Jun 2021 13:03:04 +0200 Subject: [PATCH 5/9] s_packaging_calculator: contained mapping depends on lang Otherwise translations won't be taken into account. --- stock_packaging_calculator/models/product.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stock_packaging_calculator/models/product.py b/stock_packaging_calculator/models/product.py index 026c7971b..57fc4dbdc 100644 --- a/stock_packaging_calculator/models/product.py +++ b/stock_packaging_calculator/models/product.py @@ -24,6 +24,7 @@ class Product(models.Model): help="Technical field to store contained packaging. ", ) + @api.depends_context("lang") @api.depends("packaging_ids.qty") def _compute_packaging_contained_mapping(self): for rec in self: From 6ce14df9c6bb32c940c01b9ec49dfa445ee16dec Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 11 Jun 2021 13:08:24 +0200 Subject: [PATCH 6/9] s_packaging_calculator: include units when units only on demand --- stock_packaging_calculator/models/product.py | 11 ++++++++--- .../models/product_qty_by_packaging_mixin.py | 4 +++- .../tests/test_pkg_qty_str.py | 19 +++++++++++++++++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/stock_packaging_calculator/models/product.py b/stock_packaging_calculator/models/product.py index 57fc4dbdc..71d706368 100644 --- a/stock_packaging_calculator/models/product.py +++ b/stock_packaging_calculator/models/product.py @@ -153,11 +153,16 @@ class Product(models.Model): "barcode": packaging.barcode, } - def product_qty_by_packaging_as_str(self, prod_qty, include_total_units=False): + def product_qty_by_packaging_as_str( + self, prod_qty, include_total_units=False, only_packaging=False + ): """Return a string representing the qty of each packaging. :param prod_qty: the qty of current product to translate to pkg qty :param include_total_units: includes total qty required initially + :param only_packaging: exclude units if you have only units. + IOW: if the qty does not match any packaging and this flag is true + you'll get an empty string instead of `N units`. """ self.ensure_one() if not prod_qty: @@ -191,8 +196,8 @@ class Product(models.Model): if bit: as_string.append(bit) # Restore unit information if any. - # Skip it if we get only units in the count. - if unit_qty and not has_only_units: + include_units = (has_only_units and not only_packaging) or not has_only_units + if unit_qty and include_units: as_string.append(f"{unit_qty} {self.uom_id.name}") # We want to avoid line break here as this string # can be used by reports diff --git a/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py b/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py index 4721fe862..9065a7b1f 100644 --- a/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py +++ b/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py @@ -27,10 +27,11 @@ class ProductQtyByPackagingMixin(models.AbstractModel): depends.append(self._qty_by_pkg__qty_field_name) return depends - @api.depends_context("lang", "qty_by_pkg_total_units") + @api.depends_context("lang", "qty_by_pkg_total_units", "qty_by_pkg_only_packaging") @api.depends(lambda self: self._product_qty_by_packaging_display_depends()) def _compute_product_qty_by_packaging_display(self): include_total_units = self.env.context.get("qty_by_pkg_total_units", False) + only_packaging = self.env.context.get("qty_by_pkg_only_packaging", False) for record in self: value = "" product = record._qty_by_packaging_get_product() @@ -38,6 +39,7 @@ class ProductQtyByPackagingMixin(models.AbstractModel): value = product.product_qty_by_packaging_as_str( record._qty_by_packaging_get_qty(), include_total_units=include_total_units, + only_packaging=only_packaging, ) record.product_qty_by_packaging_display = value diff --git a/stock_packaging_calculator/tests/test_pkg_qty_str.py b/stock_packaging_calculator/tests/test_pkg_qty_str.py index 93998846c..7fca487de 100644 --- a/stock_packaging_calculator/tests/test_pkg_qty_str.py +++ b/stock_packaging_calculator/tests/test_pkg_qty_str.py @@ -5,7 +5,10 @@ from .common import TestCommon class TestAsStr(TestCommon): def test_as_str(self): - self.assertEqual(self.product_a.product_qty_by_packaging_as_str(10), "") + self.assertEqual(self.product_a.product_qty_by_packaging_as_str(10), "10 Units") + self.assertEqual( + self.product_a.product_qty_by_packaging_as_str(10, only_packaging=True), "" + ) self.assertEqual(self.product_a.product_qty_by_packaging_as_str(100), "2 Box") self.assertEqual( self.product_a.product_qty_by_packaging_as_str(250), "1 Big Box,\xa01 Box" @@ -14,13 +17,18 @@ class TestAsStr(TestCommon): self.product_a.product_qty_by_packaging_as_str(255), "1 Big Box,\xa01 Box,\xa05 Units", ) + # only_packaging has no impact if we get not only units + self.assertEqual( + self.product_a.product_qty_by_packaging_as_str(255, only_packaging=True), + "1 Big Box,\xa01 Box,\xa05 Units", + ) def test_as_str_w_units(self): self.assertEqual( self.product_a.product_qty_by_packaging_as_str( 10, include_total_units=True ), - "", + "10 Units", ) self.assertEqual( self.product_a.product_qty_by_packaging_as_str( @@ -40,6 +48,13 @@ class TestAsStr(TestCommon): ), "1 Big Box,\xa01 Box,\xa05 Units (255 Units)", ) + # only_packaging has no impact if we get not only units + self.assertEqual( + self.product_a.product_qty_by_packaging_as_str( + 255, include_total_units=True, only_packaging=True + ), + "1 Big Box,\xa01 Box,\xa05 Units (255 Units)", + ) def test_as_str_custom_name(self): self.assertEqual( From 72997319a252b97d5f51dedb2accfcafbfdac837 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 15 Jul 2021 16:57:16 +0200 Subject: [PATCH 7/9] s_packaging_calculator: improve test util This way no matter who's changing the behavior of the name getter we'll get the right name. --- stock_packaging_calculator/tests/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stock_packaging_calculator/tests/utils.py b/stock_packaging_calculator/tests/utils.py index cd9e12560..8c885894c 100644 --- a/stock_packaging_calculator/tests/utils.py +++ b/stock_packaging_calculator/tests/utils.py @@ -5,6 +5,7 @@ def make_pkg_values(record, **kw): """Helper to generate test values for packaging. """ + name = record.name if record._name == "uom.uom": is_unit = True barcode = None @@ -13,9 +14,11 @@ def make_pkg_values(record, **kw): qty = record.qty is_unit = False barcode = record.barcode + if record.product_id: + name = record.product_id._packaging_name_getter(record) values = { "id": record.id, - "name": record.name, + "name": name, "qty": qty, "barcode": barcode, "is_unit": is_unit, From aa3bed30d732e4dadecec98fb6e36a64421e0466 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Mon, 7 Jun 2021 12:22:57 +0000 Subject: [PATCH 8/9] [UPD] Update stock_packaging_calculator.pot --- .../i18n/stock_packaging_calculator.pot | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 stock_packaging_calculator/i18n/stock_packaging_calculator.pot diff --git a/stock_packaging_calculator/i18n/stock_packaging_calculator.pot b/stock_packaging_calculator/i18n/stock_packaging_calculator.pot deleted file mode 100644 index 664b39319..000000000 --- a/stock_packaging_calculator/i18n/stock_packaging_calculator.pot +++ /dev/null @@ -1,44 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * stock_packaging_calculator -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 14.0\n" -"Report-Msgid-Bugs-To: \n" -"Last-Translator: \n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: stock_packaging_calculator -#: model:ir.model.fields,field_description:stock_packaging_calculator.field_product_product__display_name -msgid "Display Name" -msgstr "" - -#. module: stock_packaging_calculator -#: model:ir.model.fields,field_description:stock_packaging_calculator.field_product_product__id -msgid "ID" -msgstr "" - -#. module: stock_packaging_calculator -#: model:ir.model.fields,field_description:stock_packaging_calculator.field_product_product____last_update -msgid "Last Modified on" -msgstr "" - -#. module: stock_packaging_calculator -#: model:ir.model.fields,field_description:stock_packaging_calculator.field_product_product__packaging_contained_mapping -msgid "Packaging Contained Mapping" -msgstr "" - -#. module: stock_packaging_calculator -#: model:ir.model,name:stock_packaging_calculator.model_product_product -msgid "Product" -msgstr "" - -#. module: stock_packaging_calculator -#: model:ir.model.fields,help:stock_packaging_calculator.field_product_product__packaging_contained_mapping -msgid "Technical field to store contained packaging. " -msgstr "" From da79f32ca5d97eb63d383d924e9c98c7307ad97d Mon Sep 17 00:00:00 2001 From: nguyen hoang hiep Date: Tue, 14 Sep 2021 05:57:42 +0000 Subject: [PATCH 9/9] [IMP] stock_packaging_calculator: black, isort, prettier --- .../i18n/stock_packaging_calculator.pot | 54 +++++++++++++++++++ .../models/product_qty_by_packaging_mixin.py | 3 +- .../tests/test_packaging_calc.py | 8 ++- stock_packaging_calculator/tests/utils.py | 3 +- 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 stock_packaging_calculator/i18n/stock_packaging_calculator.pot diff --git a/stock_packaging_calculator/i18n/stock_packaging_calculator.pot b/stock_packaging_calculator/i18n/stock_packaging_calculator.pot new file mode 100644 index 000000000..1cfae5477 --- /dev/null +++ b/stock_packaging_calculator/i18n/stock_packaging_calculator.pot @@ -0,0 +1,54 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_packaging_calculator +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_packaging_calculator +#: model:ir.model.fields,field_description:stock_packaging_calculator.field_product_qty_by_packaging_mixin__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_packaging_calculator +#: model:ir.model.fields,field_description:stock_packaging_calculator.field_product_qty_by_packaging_mixin__id +msgid "ID" +msgstr "" + +#. module: stock_packaging_calculator +#: model:ir.model.fields,field_description:stock_packaging_calculator.field_product_qty_by_packaging_mixin____last_update +msgid "Last Modified on" +msgstr "" + +#. module: stock_packaging_calculator +#: model:ir.model.fields,field_description:stock_packaging_calculator.field_product_product__packaging_contained_mapping +msgid "Packaging Contained Mapping" +msgstr "" + +#. module: stock_packaging_calculator +#: model:ir.model,name:stock_packaging_calculator.model_product_product +msgid "Product" +msgstr "" + +#. module: stock_packaging_calculator +#: model:ir.model,name:stock_packaging_calculator.model_product_qty_by_packaging_mixin +msgid "Product Qty By Packaging (Mixin)" +msgstr "" + +#. module: stock_packaging_calculator +#: model:ir.model.fields,field_description:stock_packaging_calculator.field_product_qty_by_packaging_mixin__product_qty_by_packaging_display +msgid "Qty by packaging" +msgstr "" + +#. module: stock_packaging_calculator +#: model:ir.model.fields,help:stock_packaging_calculator.field_product_product__packaging_contained_mapping +msgid "Technical field to store contained packaging. " +msgstr "" diff --git a/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py b/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py index 9065a7b1f..693ad561a 100644 --- a/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py +++ b/stock_packaging_calculator/models/product_qty_by_packaging_mixin.py @@ -6,8 +6,7 @@ from odoo import api, fields, models class ProductQtyByPackagingMixin(models.AbstractModel): - """Allow displaying product qty by packaging. - """ + """Allow displaying product qty by packaging.""" _name = "product.qty_by_packaging.mixin" _description = "Product Qty By Packaging (Mixin)" diff --git a/stock_packaging_calculator/tests/test_packaging_calc.py b/stock_packaging_calculator/tests/test_packaging_calc.py index c51347e40..3d4bbfa85 100644 --- a/stock_packaging_calculator/tests/test_packaging_calc.py +++ b/stock_packaging_calculator/tests/test_packaging_calc.py @@ -121,7 +121,9 @@ class TestCalc(TestCommon): contained=[make_pkg_values(self.pkg_box, qty=4)], ), make_pkg_values( - self.pkg_box, qty=1, contained=[make_pkg_values(self.uom_unit, qty=50)], + self.pkg_box, + qty=1, + contained=[make_pkg_values(self.uom_unit, qty=50)], ), make_pkg_values(self.uom_unit, qty=5, contained=None), ] @@ -148,7 +150,9 @@ class TestCalc(TestCommon): ], ), make_pkg_values( - self.pkg_box, qty=1, contained=[make_pkg_values(self.uom_unit, qty=30)], + self.pkg_box, + qty=1, + contained=[make_pkg_values(self.uom_unit, qty=30)], ), make_pkg_values(self.uom_unit, qty=25, contained=None), ] diff --git a/stock_packaging_calculator/tests/utils.py b/stock_packaging_calculator/tests/utils.py index 8c885894c..26c4b357b 100644 --- a/stock_packaging_calculator/tests/utils.py +++ b/stock_packaging_calculator/tests/utils.py @@ -3,8 +3,7 @@ def make_pkg_values(record, **kw): - """Helper to generate test values for packaging. - """ + """Helper to generate test values for packaging.""" name = record.name if record._name == "uom.uom": is_unit = True