stock_packaging_calculator: make contained mapping computed

Allows to reuse the mapping every time is needed.
This commit is contained in:
Simone Orsi
2020-06-09 10:35:32 +02:00
committed by Sébastien BEAU
parent a9beb26ed9
commit 35d3eb7cc2
2 changed files with 99 additions and 16 deletions

View File

@@ -3,16 +3,45 @@
from collections import namedtuple
from odoo import models
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")
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.
@@ -34,31 +63,42 @@ class Product(models.Model):
{contained: [{id: 1, qty: 4, name: "Big box"}]}
"""
packagings = [Packaging(x.id, x.name, x.qty) for x in self.packaging_ids]
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."""
packagings = [
Packaging(x.id, x.name, x.qty, False)
for x in self.packaging_ids
# Exclude the ones w/ zero qty as they are useless for the math
if 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.
Packaging(self.uom_id.id, self.uom_id.name, self.uom_id.factor)
)
return self._product_qty_by_packaging(
sorted(packagings, reverse=True, key=lambda x: x.qty),
prod_qty,
with_contained=with_contained,
# You can use `is_unit` to check this.
Packaging(self.uom_id.id, self.uom_id.name, self.uom_id.factor, True)
)
return sorted(packagings, reverse=True, key=lambda x: x.qty)
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 i, pkg in enumerate(pkg_by_qty):
for pkg in pkg_by_qty:
qty_per_pkg, qty = self._qty_by_pkg(pkg.qty, qty)
if qty_per_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
)
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

View File

@@ -4,6 +4,10 @@ from odoo.tests import SavepointCase
class TestCalc(SavepointCase):
at_install = False
post_install = True
@classmethod
def setUpClass(cls):
super().setUpClass()
@@ -12,7 +16,6 @@ class TestCalc(SavepointCase):
cls.product_a = cls.env["product.product"].create(
{
"name": "Product A",
"type": "product",
"uom_id": cls.uom_unit.id,
"uom_po_id": cls.uom_unit.id,
}
@@ -27,6 +30,46 @@ class TestCalc(SavepointCase):
{"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,
},
],
str(self.pkg_big_box.id): [
{"id": self.pkg_box.id, "qty": 4, "name": self.pkg_box.name},
],
str(self.pkg_box.id): [
{"id": self.uom_unit.id, "qty": 50, "name": self.uom_unit.name},
],
},
)
# 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,
},
],
str(self.pkg_big_box.id): [
{"id": self.pkg_box.id, "qty": 4, "name": self.pkg_box.name},
],
str(self.pkg_box.id): [
{"id": self.uom_unit.id, "qty": 50, "name": self.uom_unit.name},
],
},
)
def test_calc_1(self):
"""Test easy behavior 1."""
expected = [
@@ -95,7 +138,7 @@ class TestCalc(SavepointCase):
"id": self.uom_unit.id,
"qty": 5,
"name": self.uom_unit.name,
"contained": [],
"contained": None,
},
]
self.assertEqual(
@@ -140,7 +183,7 @@ class TestCalc(SavepointCase):
"id": self.uom_unit.id,
"qty": 25,
"name": self.uom_unit.name,
"contained": [],
"contained": None,
},
]
self.assertEqual(