diff --git a/setup/stock_packaging_calculator/odoo/addons/stock_packaging_calculator b/setup/stock_packaging_calculator/odoo/addons/stock_packaging_calculator new file mode 120000 index 000000000..d3e128c8d --- /dev/null +++ b/setup/stock_packaging_calculator/odoo/addons/stock_packaging_calculator @@ -0,0 +1 @@ +../../../../stock_packaging_calculator \ No newline at end of file diff --git a/setup/stock_packaging_calculator/setup.py b/setup/stock_packaging_calculator/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/stock_packaging_calculator/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_packaging_calculator/README.rst b/stock_packaging_calculator/README.rst new file mode 100644 index 000000000..d6cc846cd --- /dev/null +++ b/stock_packaging_calculator/README.rst @@ -0,0 +1,126 @@ +========================== +Stock packaging calculator +========================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_packaging_calculator + :alt: OCA/stock-logistics-warehouse +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-13-0/stock-logistics-warehouse-13-0-stock_packaging_calculator + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/153/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Basic module providing an helper method to calculate the quantity of product by packaging. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +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": []},}, + ] + +Known issues / Roadmap +====================== + +TODO + +1. Fractional quantities (eg: 0.5 Kg) are lost when counting units +2. Maybe rely on `packaging_uom` + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Simone Orsi + +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. + +This module is part of the `OCA/stock-logistics-warehouse `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_packaging_calculator/__init__.py b/stock_packaging_calculator/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/stock_packaging_calculator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_packaging_calculator/__manifest__.py b/stock_packaging_calculator/__manifest__.py new file mode 100644 index 000000000..856e147c3 --- /dev/null +++ b/stock_packaging_calculator/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2020 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) +{ + "name": "Stock packaging calculator", + "summary": "Compute product quantity to pick by packaging", + "version": "14.0.1.0.0", + "development_status": "Alpha", + "category": "Warehouse Management", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "author": "Camptocamp, Odoo Community Association (OCA)", + "license": "LGPL-3", + "application": False, + "installable": True, + "depends": ["product"], +} 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..92a274c4a --- /dev/null +++ b/stock_packaging_calculator/i18n/stock_packaging_calculator.pot @@ -0,0 +1,29 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_packaging_calculator +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.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__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 "" diff --git a/stock_packaging_calculator/models/__init__.py b/stock_packaging_calculator/models/__init__.py new file mode 100644 index 000000000..9649db77a --- /dev/null +++ b/stock_packaging_calculator/models/__init__.py @@ -0,0 +1 @@ +from . import product diff --git a/stock_packaging_calculator/models/product.py b/stock_packaging_calculator/models/product.py new file mode 100644 index 000000000..a4327076c --- /dev/null +++ b/stock_packaging_calculator/models/product.py @@ -0,0 +1,143 @@ +# Copyright 2020 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) + +from collections import namedtuple + +from odoo import api, models +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") + + +class Product(models.Model): + _inherit = "product.product" + + packaging_contained_mapping = Serialized( + compute="_compute_packaging_contained_mapping", + help="Technical field to store contained packaging. ", + ) + + @api.depends("packaging_ids.qty") + def _compute_packaging_contained_mapping(self): + for rec in self: + rec.packaging_contained_mapping = rec._packaging_contained_mapping() + + def _packaging_contained_mapping(self): + """Produce a mapping of packaging and contained packagings. + + Used mainly for `product_qty_by_packaging` but can be used + to display info as you prefer. + + :returns: a dictionary in the form {pkg.id: [contained packages]} + """ + res = {} + packaging = self._ordered_packaging() + for i, pkg in enumerate(packaging): + if pkg.is_unit: + # skip minimal unit + continue + res[pkg.id] = self._product_qty_by_packaging(packaging[i + 1 :], pkg.qty) + return res + + 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. + :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"}]} + """ + self.ensure_one() + return self._product_qty_by_packaging( + self._ordered_packaging(), + prod_qty, + with_contained=with_contained, + ) + + def _ordered_packaging(self): + """Prepare packaging ordered by qty and exclude empty ones. + + Use ctx key `_packaging_filter` to pass a function to filter packaging + to be considered. + + Use ctx key `_packaging_name_getter` to pass a function to change + 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) + packagings = sorted( + [ + Packaging(x.id, name_getter(x), x.qty, 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 + ], + reverse=True, + key=lambda x: x.qty, + ) + # 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. + # You can use `is_unit` to check this. + Packaging(self.uom_id.id, self.uom_id.name, self.uom_id.factor, True) + ) + return packagings + + 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 = [] + prepare_values = self.env.context.get( + "_packaging_values_handler", self._prepare_qty_by_packaging_values + ) + for pkg in pkg_by_qty: + qty_per_pkg, qty = self._qty_by_pkg(pkg.qty, qty) + if qty_per_pkg: + value = prepare_values(pkg, qty_per_pkg) + if with_contained: + contained = None + if not pkg.is_unit: + mapping = self.packaging_contained_mapping + # integer keys are serialized as strings :/ + contained = mapping.get(str(pkg.id)) + value["contained"] = contained + res.append(value) + if not qty: + break + return res + + 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=self.uom_id.rounding) + >= 0.0 + ): + qty -= pkg_qty + qty_per_pkg += 1 + return qty_per_pkg, qty + + def _prepare_qty_by_packaging_values(self, packaging, qty_per_pkg): + return { + "id": packaging.id, + "qty": qty_per_pkg, + "name": packaging.name, + "is_unit": packaging.is_unit, + } diff --git a/stock_packaging_calculator/readme/CONTRIBUTORS.rst b/stock_packaging_calculator/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..f583948be --- /dev/null +++ b/stock_packaging_calculator/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Simone Orsi diff --git a/stock_packaging_calculator/readme/DESCRIPTION.rst b/stock_packaging_calculator/readme/DESCRIPTION.rst new file mode 100644 index 000000000..1d6fceb5c --- /dev/null +++ b/stock_packaging_calculator/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Basic module providing an helper method to calculate the quantity of product by packaging. diff --git a/stock_packaging_calculator/readme/ROADMAP.rst b/stock_packaging_calculator/readme/ROADMAP.rst new file mode 100644 index 000000000..e4881d5e6 --- /dev/null +++ b/stock_packaging_calculator/readme/ROADMAP.rst @@ -0,0 +1,4 @@ +TODO + +1. Fractional quantities (eg: 0.5 Kg) are lost when counting units +2. Maybe rely on `packaging_uom` diff --git a/stock_packaging_calculator/readme/USAGE.rst b/stock_packaging_calculator/readme/USAGE.rst new file mode 100644 index 000000000..73f05a708 --- /dev/null +++ b/stock_packaging_calculator/readme/USAGE.rst @@ -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": []},}, + ] diff --git a/stock_packaging_calculator/static/description/icon.png b/stock_packaging_calculator/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/stock_packaging_calculator/static/description/icon.png differ diff --git a/stock_packaging_calculator/static/description/index.html b/stock_packaging_calculator/static/description/index.html new file mode 100644 index 000000000..ebff58a67 --- /dev/null +++ b/stock_packaging_calculator/static/description/index.html @@ -0,0 +1,472 @@ + + + + + + +Stock packaging calculator + + + +
+

Stock packaging calculator

+ + +

Alpha License: LGPL-3 OCA/stock-logistics-warehouse Translate me on Weblate Try me on Runbot

+

Basic module providing an helper method to calculate the quantity of product by packaging.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Usage

+

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)
+
+[
+    {"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:

+
+
+>>> 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": []},},
+]
+
+
+
+
+

Known issues / Roadmap

+

TODO

+
    +
  1. Fractional quantities (eg: 0.5 Kg) are lost when counting units
  2. +
  3. Maybe rely on packaging_uom
  4. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/stock-logistics-warehouse project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/stock_packaging_calculator/tests/__init__.py b/stock_packaging_calculator/tests/__init__.py new file mode 100644 index 000000000..04c47e711 --- /dev/null +++ b/stock_packaging_calculator/tests/__init__.py @@ -0,0 +1 @@ +from . import test_packaging_calc diff --git a/stock_packaging_calculator/tests/test_packaging_calc.py b/stock_packaging_calculator/tests/test_packaging_calc.py new file mode 100644 index 000000000..1bc721086 --- /dev/null +++ b/stock_packaging_calculator/tests/test_packaging_calc.py @@ -0,0 +1,376 @@ +# Copyright 2020 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl) +from odoo.tests import SavepointCase + + +class TestCalc(SavepointCase): + + at_install = False + post_install = True + + @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} + ) + cls.pkg_big_box = cls.env["product.packaging"].create( + {"name": "Big Box", "product_id": cls.product_a.id, "qty": 200} + ) + cls.pkg_pallet = cls.env["product.packaging"].create( + {"name": "Pallet", "product_id": cls.product_a.id, "qty": 2000} + ) + + 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, + }, + ], + }, + ) + # Update pkg qty + self.pkg_pallet.qty = 4000 + 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, + }, + ], + }, + ) + + 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, + }, + ] + 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, + }, + ] + 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, + }, + ] + 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, + }, + ] + 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, + }, + ] + self.assertEqual( + self.product_a.with_context( + _packaging_filter=lambda x: x != self.pkg_pallet + ).product_qty_by_packaging(2655), + expected, + ) + + 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, + }, + ] + self.assertEqual( + self.product_a.with_context( + _packaging_name_getter=lambda x: "FOO " + x.name + ).product_qty_by_packaging(2655), + expected, + ) + + def test_calc_custom_values(self): + """Test custom values handler.""" + expected = [ + {"my_qty": 1, "foo": self.pkg_pallet.name}, + {"my_qty": 3, "foo": self.pkg_big_box.name}, + {"my_qty": 1, "foo": self.pkg_box.name}, + {"my_qty": 5, "foo": self.uom_unit.name}, + ] + self.assertEqual( + self.product_a.with_context( + _packaging_values_handler=lambda pkg, qty_per_pkg: { + "my_qty": qty_per_pkg, + "foo": pkg.name, + } + ).product_qty_by_packaging(2655), + expected, + ) + + 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, + }, + ] + 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, + "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": 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, + }, + ] + self.assertEqual( + self.product_a.product_qty_by_packaging(2655, with_contained=True), + expected, + )