stock_available_mrp: fix BOM qty (multi-units, efficiency)

This commit is contained in:
Cyril Gaudin
2016-03-15 14:08:31 +01:00
committed by Víctor Martínez
parent 93c1dc8f66
commit 3801d3481c
3 changed files with 216 additions and 16 deletions

View File

@@ -3,7 +3,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
'name': 'Consider the production potential is available to promise',
'version': '8.0.3.0.0',
'version': '8.0.3.0.1',
"author": u"Numérigraphe,"
u"Odoo Community Association (OCA)",
'category': 'Hidden',

View File

@@ -33,6 +33,7 @@ class ProductProduct(models.Model):
def _get_potential_qty(self):
"""Compute the potential qty based on the available components."""
bom_obj = self.env['mrp.bom']
uom_obj = self.env['product.uom']
for product in self:
bom_id = bom_obj._bom_find(product_id=product.id)
@@ -40,18 +41,53 @@ class ProductProduct(models.Model):
product.potential_qty = 0.0
continue
bom = bom_obj.browse(bom_id)
# Need by product (same product can be in many BOM lines/levels)
component_needs = Counter()
for component in bom_obj._bom_explode(bom_obj.browse(bom_id),
product, 1.0,)[0]:
component_needs += Counter(
{component['product_id']: component['product_qty']})
component_needs = self._get_components_needs(product, bom)
if not component_needs:
# The BoM has no line we can use
product.potential_qty = 0.0
continue
# Find the lowest quantity we can make with the stock at hand
product.potential_qty = min(
[self.browse(component_id).qty_available // need
for component_id, need in component_needs.items()])
else:
# Find the lowest quantity we can make with the stock at hand
components_potential_qty = min(
[component.qty_available // need
for component, need in component_needs.items()]
)
# Compute with bom quantity
bom_qty = uom_obj._compute_qty_obj(
bom.product_uom,
bom.product_qty,
bom.product_tmpl_id.uom_id
)
product.potential_qty = bom_qty * components_potential_qty
def _get_components_needs(self, product, bom):
""" Return the needed qty of each compoments in the *bom* of *product*.
:type product: product_product
:type bom: mrp_bom
:rtype: collections.Counter
"""
bom_obj = self.env['mrp.bom']
uom_obj = self.env['product.uom']
product_obj = self.env['product.product']
needs = Counter()
for bom_component in bom_obj._bom_explode(bom, product, 1.0)[0]:
product_uom = uom_obj.browse(bom_component['product_uom'])
component = product_obj.browse(bom_component['product_id'])
component_qty = uom_obj._compute_qty_obj(
product_uom,
bom_component['product_qty'],
component.uom_id,
)
needs += Counter(
{component: component_qty}
)
return needs

View File

@@ -12,6 +12,14 @@ class TestPotentialQty(TransactionCase):
def setUp(self):
super(TestPotentialQty, self).setUp()
self.product_model = self.env["product.product"]
self.bom_model = self.env["mrp.bom"]
self.bom_line_model = self.env["mrp.bom.line"]
self.stock_quant_model = self.env["stock.quant"]
self.setup_demo_data()
def setup_demo_data(self):
#  An interesting product (multi-line BoM, variants)
self.tmpl = self.browse_ref(
'product.product_product_4_product_template')
@@ -57,6 +65,25 @@ class TestPotentialQty(TransactionCase):
self.wh_main = self.browse_ref('stock.warehouse0')
self.wh_ch = self.browse_ref('stock.stock_warehouse_shop0')
def create_inventory(self, product_id, qty, location_id=None):
if location_id is None:
location_id = self.wh_main.lot_stock_id.id
inventory = self.env['stock.inventory'].create({
'name': 'Test inventory',
'location_id': location_id,
'filter': 'partial'
})
inventory.prepare_inventory()
self.env['stock.inventory.line'].create({
'inventory_id': inventory.id,
'product_id': product_id,
'location_id': location_id,
'product_qty': qty
})
inventory.action_done()
def assertPotentialQty(self, record, qty, msg):
record.refresh()
# Check the potential
@@ -186,7 +213,9 @@ class TestPotentialQty(TransactionCase):
"Receiving variant 1's component should not change "
"variant 2's potential")
# Receive enough components to make 500x the 2nd variant at Chicago
# Receive enough components to make 42X the 2nd variant at Chicago
# need 13 dozens of HDD with 50% efficiency to build 42 RAM
# So 313 HDD (with rounding) for 42 RAM
inventory = self.env['stock.inventory'].create(
{'name': 'components for 2nd variant',
'location_id': self.wh_ch.lot_stock_id.id,
@@ -201,7 +230,7 @@ class TestPotentialQty(TransactionCase):
{'inventory_id': inventory.id,
'product_id': self.ref('product.product_product_18'),
'location_id': self.wh_ch.lot_stock_id.id,
'product_qty': 310.0})
'product_qty': 313.0})
inventory.action_done()
self.assertPotentialQty(
self.tmpl, 1000.0,
@@ -211,14 +240,14 @@ class TestPotentialQty(TransactionCase):
"Receiving variant 2's component should not change "
"variant 1's potential")
self.assertPotentialQty(
self.var2, 500.0,
self.var2, 42.0,
"Wrong variant 2 potential after receiving components")
# Check by warehouse
self.assertPotentialQty(
self.tmpl.with_context(warehouse=self.wh_main.id), 1000.0,
"Wrong potential quantity in main WH")
self.assertPotentialQty(
self.tmpl.with_context(warehouse=self.wh_ch.id), 500.0,
self.tmpl.with_context(warehouse=self.wh_ch.id), 42.0,
"Wrong potential quantity in Chicago WH")
# Check by location
self.assertPotentialQty(
@@ -228,5 +257,140 @@ class TestPotentialQty(TransactionCase):
self.assertPotentialQty(
self.tmpl.with_context(
location=self.wh_ch.lot_stock_id.id),
500.0,
42.0,
"Wrong potential quantity in Chicago WH location")
def test_multi_unit_recursive_bom(self):
# Test multi-level and multi-units BOM
p1 = self.product_model.create({
'name': 'Test product with BOM',
})
p2 = self.product_model.create({
'name': 'Test sub product with BOM',
})
p3 = self.product_model.create({
'name': 'Test component'
})
bom_p1 = self.bom_model.create({
'product_tmpl_id': p1.product_tmpl_id.id,
'product_id': p1.id,
})
# 1 dozen of component
self.bom_line_model.create({
'bom_id': bom_p1.id,
'product_id': p3.id,
'product_qty': 1,
'product_uom': self.ref('product.product_uom_dozen'),
})
# Two p2 which have a bom
self.bom_line_model.create({
'bom_id': bom_p1.id,
'product_id': p2.id,
'product_qty': 2,
'product_uom': self.ref('product.product_uom_unit'),
'type': 'phantom',
})
bom_p2 = self.bom_model.create({
'product_tmpl_id': p2.product_tmpl_id.id,
'product_id': p2.id,
})
# p2 need 2 unit of component
self.bom_line_model.create({
'bom_id': bom_p2.id,
'product_id': p3.id,
'product_qty': 2,
'product_uom': self.ref('product.product_uom_unit'),
})
p1.refresh()
# Need a least 1 dozen + 2 * 2 = 16 units for one P1
self.assertEqual(0, p1.potential_qty)
self.create_inventory(p3.id, 1)
p1.refresh()
self.assertEqual(0, p1.potential_qty)
self.create_inventory(p3.id, 15)
p1.refresh()
self.assertEqual(0, p1.potential_qty)
self.create_inventory(p3.id, 16)
p1.refresh()
self.assertEqual(1.0, p1.potential_qty)
self.create_inventory(p3.id, 25)
p1.refresh()
self.assertEqual(1.0, p1.potential_qty)
self.create_inventory(p3.id, 32)
p1.refresh()
self.assertEqual(2.0, p1.potential_qty)
def test_bom_qty_and_efficiency(self):
p1 = self.product_model.create({
'name': 'Test product with BOM',
})
p2 = self.product_model.create({
'name': 'Test sub product with BOM',
})
p3 = self.product_model.create({
'name': 'Test component'
})
# A bom produce 2 dozen of P1
bom_p1 = self.bom_model.create({
'product_tmpl_id': p1.product_tmpl_id.id,
'product_id': p1.id,
'product_qty': 2,
'product_uom': self.ref('product.product_uom_dozen'),
})
# Need 5 p2 for that
self.bom_line_model.create({
'bom_id': bom_p1.id,
'product_id': p2.id,
'product_qty': 5,
'product_uom': self.ref('product.product_uom_unit'),
'product_efficiency': 0.8,
})
# Which need 1 dozen of P3
bom_p2 = self.bom_model.create({
'product_tmpl_id': p2.product_tmpl_id.id,
'product_id': p2.id,
'type': 'phantom',
})
self.bom_line_model.create({
'bom_id': bom_p2.id,
'product_id': p3.id,
'product_qty': 1,
'product_uom': self.ref('product.product_uom_dozen'),
})
p1.refresh()
self.assertEqual(0, p1.potential_qty)
self.create_inventory(p3.id, 60)
p1.refresh()
self.assertEqual(0, p1.potential_qty)
# Need 5 * 1 dozen => 60
# But 80% lost each dozen, need 3 more by dozen => 60 + 5 *3 => 75
self.create_inventory(p3.id, 75)
p1.refresh()
self.assertEqual(24, p1.potential_qty)