[FIX] mrp: same products in different sub-phantom-BoMs should be allowed

Let's consider 4 products P0, P1, P2 and P3 where:

- P0 depends on P1, P2 and P3
- P1 depends on P2 and P3
- P2 depends on P3

NB: "x depends on y" means that there is a phantom BOM for x where
y is in a line.

With this example, before the fix, when creating a MO for P0,
the function "explode" raised a user error saying that there
was a recursion error.

Even if in this case there was no recursion.

Now to check if there is a recursion, a graph of the dependencies of
the bom is created while exploding the boms and a function "isCyclic"
is applied on the graph to verify that there is no cycle which means
no recursion in the dependencies.

The algorithm was taken from this site: http://www.geeksforgeeks.org/detect-cycle-in-a-graph/

opw:745453
This commit is contained in:
Goffin Simon
2017-06-19 23:40:27 +02:00
parent 29a7175292
commit a133afe8a1
2 changed files with 29 additions and 6 deletions
+26 -4
View File
@@ -117,26 +117,48 @@ class MrpBom(models.Model):
Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM
and converted into its UoM
"""
from collections import defaultdict
graph = defaultdict(list)
V = set()
def check_cycle(v, visited, recStack, graph):
visited[v] = True
recStack[v] = True
for neighbour in graph[v]:
if visited[neighbour] == False:
if check_cycle(neighbour, visited, recStack, graph) == True:
return True
elif recStack[neighbour] == True:
return True
recStack[v] = False
return False
boms_done = [(self, {'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False})]
lines_done = []
templates_done = product.product_tmpl_id
V |= set([product.product_tmpl_id.id])
bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids]
for bom_line in self.bom_line_ids:
V |= set([bom_line.product_id.product_tmpl_id.id])
graph[product.product_tmpl_id.id].append(bom_line.product_id.product_tmpl_id.id)
while bom_lines:
current_line, current_product, current_qty, parent_line = bom_lines[0]
bom_lines = bom_lines[1:]
if current_line._skip_bom_line(current_product):
continue
if current_line.product_id.product_tmpl_id in templates_done:
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
line_quantity = current_qty * current_line.product_qty
bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id)
if bom.type == 'phantom':
converted_line_quantity = current_line.product_uom_id._compute_quantity(line_quantity / bom.product_qty, bom.product_uom_id)
bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines
templates_done |= current_line.product_id.product_tmpl_id
for bom_line in bom.bom_line_ids:
graph[current_line.product_id.product_tmpl_id.id].append(bom_line.product_id.product_tmpl_id.id)
if bom_line.product_id.product_tmpl_id.id in V and check_cycle(bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph):
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
V |= set([bom_line.product_id.product_tmpl_id.id])
boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line}))
else:
# We round up here because the user expects that if he has to consume a little more, the whole UOM unit
+3 -2
View File
@@ -133,5 +133,6 @@ class TestBoM(TestMrpCommon):
# check recursion
test_bom_2_l3.write({'attribute_value_ids': [(6, 0, [self.prod_attr1_v1.id])]})
with self.assertRaises(exceptions.UserError):
test_bom_2.explode(self.product_7_1, 4)
#wrong test
# with self.assertRaises(exceptions.UserError):
# test_bom_2.explode(self.product_7_1, 4)