-
-
Notifications
You must be signed in to change notification settings - Fork 552
[FIX] mrp_multi_level : Fix MRP wizard logic #1683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 16.0
Are you sure you want to change the base?
Conversation
|
Hi @ChrisOForgeFlow, @LoisRForgeFlow, @JordiBForgeFlow, |
AdrianaSaiz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review Changes Summary:
-
Editable mrp_action field: Users can now change the procurement method
(buy, manufacture, pull, pull_push) before executing. The selected method
is respected via ProcurementGroup._get_rule() override in stock_rule.py. -
Context-aware quantity handling: Added 'source_context' and 'original_qty'
fields to prevent quantity reset when opening wizard from planned orders.- From inventory: recalculates qty from mrp_inventory.to_procure
- From planned order: preserves original planned quantity
-
Company and currency support: Added company_id and currency_id to
procurement values to avoid errors during PO/MO creation and to support
multi-currency purchases. -
Vendor pre-fill for purchases: When mrp_action='buy', the wizard now
pre-fills supplier_id and currency_id from product's seller info. -
Tests added (test_mrp_multi_level.py):
- test_26: Override to 'buy' creates PO
- test_31: _get_rule respects mrp_action parameter
- test_32: UoM onchange fallback to original_qty
- test_33: Override to 'manufacture' creates MO
- test_34: Override to 'pull' creates picking
- test_35: Override to 'pull_push' creates picking
Note: 'push' action not tested as it works on existing moves, not procurements.
| domain = [ | ||
| ("action", "=", self.mrp_action), | ||
| ("location_dest_id", "=", location.id), | ||
| ] | ||
|
|
||
| rule = self.env["stock.rule"].search(domain, limit=1) | ||
|
|
||
| if not rule: | ||
| domain2 = [ | ||
| ("action", "=", self.mrp_action), | ||
| ("warehouse_id", "=", warehouse.id), | ||
| ] | ||
| rule = self.env["stock.rule"].search(domain2, limit=1) | ||
| return rule |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| domain = [ | |
| ("action", "=", self.mrp_action), | |
| ("location_dest_id", "=", location.id), | |
| ] | |
| rule = self.env["stock.rule"].search(domain, limit=1) | |
| if not rule: | |
| domain2 = [ | |
| ("action", "=", self.mrp_action), | |
| ("warehouse_id", "=", warehouse.id), | |
| ] | |
| rule = self.env["stock.rule"].search(domain2, limit=1) | |
| return rule | |
| company = self.warehouse_id.company_id or self.env.company | |
| domain = [ | |
| ("action", "=", self.mrp_action), | |
| ("location_dest_id", "=", location.id), | |
| ("company_id", "in", [company.id, False]), | |
| ] | |
| rule = self.env["stock.rule"].search(domain, order="sequence", limit=1) | |
| if not rule: | |
| domain2 = [ | |
| ("action", "=", self.mrp_action), | |
| ("warehouse_id", "=", self.warehouse_id.id), | |
| ("company_id", "in", [company.id, False]), | |
| ] | |
| rule = self.env["stock.rule"].search(domain2, order="sequence", limit=1) |
Si hay varias reglas con el mismo action (ej: dos reglas "buy"), limit=1 sin order puede devolver cualquiera. Mejor añadir order="sequence" para que nos devuelva la de mayor prioridad.
En entorno multi-company podría devolver una regla de otra empresa. Por eso habría que añadir filtro por company_id.
| company = self.warehouse_id.company_id or self.env.company | ||
| currency = company.currency_id | ||
|
|
||
| res = { | ||
| "date_planned": self.date_planned, | ||
| "warehouse_id": self.warehouse_id, | ||
| "group_id": group, | ||
| "planned_order_id": self.planned_order_id.id, | ||
| "company_id": company, | ||
| "currency_id": currency.id, | ||
| } | ||
| should_add_supplier = (self.mrp_action == "buy") or ( | ||
| not self.mrp_action and self.supply_method == "buy" | ||
| ) | ||
|
|
||
| if should_add_supplier: | ||
| qty = self.qty or 0.0 | ||
| uom = self.uom_id or self.product_id.uom_id | ||
| supplier_info = self.product_id._select_seller( | ||
| quantity=qty, | ||
| date=self.date_planned, | ||
| uom_id=uom, | ||
| partner_id=self.supplier_id, | ||
| ) | ||
| if not supplier_info: | ||
| supplier_info = self.product_id.seller_ids.filtered( | ||
| lambda s: (not s.company_id or s.company_id == company) | ||
| ).sorted(lambda s: (s.sequence, s.min_qty, s.price))[:1] | ||
|
|
||
| if supplier_info: | ||
| res["supplier_id"] = supplier_info.partner_id.id | ||
| res["currency_id"] = ( | ||
| supplier_info.currency_id or company.currency_id | ||
| ).id | ||
| else: | ||
| res["currency_id"] = company.currency_id.id | ||
|
|
||
| if hasattr(self, "mrp_action") and self.mrp_action: | ||
| res["mrp_action"] = self.mrp_action | ||
|
|
||
| rule = self._get_procurement_rule() | ||
| if rule: | ||
| res["rule_id"] = rule.id | ||
| return res |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| company = self.warehouse_id.company_id or self.env.company | |
| currency = company.currency_id | |
| res = { | |
| "date_planned": self.date_planned, | |
| "warehouse_id": self.warehouse_id, | |
| "group_id": group, | |
| "planned_order_id": self.planned_order_id.id, | |
| "company_id": company, | |
| "currency_id": currency.id, | |
| } | |
| should_add_supplier = (self.mrp_action == "buy") or ( | |
| not self.mrp_action and self.supply_method == "buy" | |
| ) | |
| if should_add_supplier: | |
| qty = self.qty or 0.0 | |
| uom = self.uom_id or self.product_id.uom_id | |
| supplier_info = self.product_id._select_seller( | |
| quantity=qty, | |
| date=self.date_planned, | |
| uom_id=uom, | |
| partner_id=self.supplier_id, | |
| ) | |
| if not supplier_info: | |
| supplier_info = self.product_id.seller_ids.filtered( | |
| lambda s: (not s.company_id or s.company_id == company) | |
| ).sorted(lambda s: (s.sequence, s.min_qty, s.price))[:1] | |
| if supplier_info: | |
| res["supplier_id"] = supplier_info.partner_id.id | |
| res["currency_id"] = ( | |
| supplier_info.currency_id or company.currency_id | |
| ).id | |
| else: | |
| res["currency_id"] = company.currency_id.id | |
| if hasattr(self, "mrp_action") and self.mrp_action: | |
| res["mrp_action"] = self.mrp_action | |
| rule = self._get_procurement_rule() | |
| if rule: | |
| res["rule_id"] = rule.id | |
| return res | |
| company = self.warehouse_id.company_id or self.env.company | |
| currency = company.currency_id | |
| res = { | |
| "date_planned": self.date_planned, | |
| "warehouse_id": self.warehouse_id, | |
| "group_id": group, | |
| "planned_order_id": self.planned_order_id.id, | |
| "company_id": company, | |
| "currency_id": currency.id, | |
| } | |
| # Añadir mrp_action para que _get_rule lo use | |
| if self.mrp_action: | |
| res["mrp_action"] = self.mrp_action | |
| should_add_supplier = (self.mrp_action == "buy") or ( | |
| not self.mrp_action and self.supply_method == "buy" | |
| ) | |
| if should_add_supplier: | |
| qty = self.qty or 0.0 | |
| uom = self.uom_id or self.product_id.uom_id | |
| supplier_info = self.product_id._select_seller( | |
| quantity=qty, | |
| date=self.date_planned, | |
| uom_id=uom, | |
| partner_id=self.supplier_id, | |
| ) | |
| if not supplier_info: | |
| supplier_info = self.product_id.seller_ids.filtered( | |
| lambda s: (not s.company_id or s.company_id == company) | |
| ).sorted(lambda s: (s.sequence, s.min_qty, s.price))[:1] | |
| if supplier_info: | |
| res["supplierinfo_id"] = supplier_info | |
| res["currency_id"] = ( | |
| supplier_info.currency_id or company.currency_id | |
| ).id | |
| else: | |
| res["currency_id"] = company.currency_id.id | |
| return res |
En este punto la llamada a _get_procurement_rule() y la asignación de rule_id en los values no son necesarias. Solo necesitamots añadir mrp_action a los values para que _get_rule lo use.
Ya no necesitamos buscar la regla aquí ni pasarla en los values. Con la herencia de _get_rule, la regla correcta se selecciona automáticamente dentro del flujo de procurement cuando lee el mrp_action. Solo tenemos que pasarle el mrp_action y Odoo hace el resto.
| def _get_procurement_rule(self): | ||
| self.ensure_one() | ||
|
|
||
| if not self.mrp_action or self.mrp_action == "none": | ||
| return False | ||
|
|
||
| location = self.location_id | ||
| warehouse = self.warehouse_id | ||
|
|
||
| domain = [ | ||
| ("action", "=", self.mrp_action), | ||
| ("location_dest_id", "=", location.id), | ||
| ] | ||
|
|
||
| rule = self.env["stock.rule"].search(domain, limit=1) | ||
|
|
||
| if not rule: | ||
| domain2 = [ | ||
| ("action", "=", self.mrp_action), | ||
| ("warehouse_id", "=", warehouse.id), | ||
| ] | ||
| rule = self.env["stock.rule"].search(domain2, limit=1) | ||
| return rule |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| def _get_procurement_rule(self): | |
| self.ensure_one() | |
| if not self.mrp_action or self.mrp_action == "none": | |
| return False | |
| location = self.location_id | |
| warehouse = self.warehouse_id | |
| domain = [ | |
| ("action", "=", self.mrp_action), | |
| ("location_dest_id", "=", location.id), | |
| ] | |
| rule = self.env["stock.rule"].search(domain, limit=1) | |
| if not rule: | |
| domain2 = [ | |
| ("action", "=", self.mrp_action), | |
| ("warehouse_id", "=", warehouse.id), | |
| ] | |
| rule = self.env["stock.rule"].search(domain2, limit=1) | |
| return rule |
Esta método hay que eliminarlo, ya no es necesario porque la lógica va a estar en _get_rule del procurement group.
| def test_31_procure_wizard_forced_rule_pull_calls_run_pull(self): | ||
| """Covers the branch where a forced rule triggers `_run_pull`.""" | ||
| mrp_inv = self.mrp_inventory_obj.search( | ||
| [("product_mrp_area_id.product_id", "=", self.fp_1.id)], limit=1 | ||
| ) | ||
| self.assertTrue(mrp_inv) | ||
|
|
||
| wiz = self.mrp_inventory_procure_wiz.with_context( | ||
| active_model="mrp.inventory", | ||
| active_ids=mrp_inv.ids, | ||
| active_id=mrp_inv.id, | ||
| ).create({}) | ||
| self.assertTrue(wiz.item_ids) | ||
| item = wiz.item_ids[0] | ||
| item.qty = 1.0 | ||
|
|
||
| route = self.env["stock.route"].create( | ||
| { | ||
| "name": "Test route (mrp_multi_level)", | ||
| "product_selectable": False, | ||
| "warehouse_selectable": False, | ||
| "company_id": self.company.id, | ||
| } | ||
| ) | ||
|
|
||
| picking_type = item.warehouse_id.in_type_id | ||
| self.assertTrue(picking_type) | ||
|
|
||
| rule = self.env["stock.rule"].create( | ||
| { | ||
| "name": "Test pull rule (mrp_multi_level)", | ||
| "route_id": route.id, | ||
| "action": "pull", | ||
| "location_src_id": item.location_id.id, | ||
| "location_dest_id": item.location_id.id, | ||
| "picking_type_id": picking_type.id, | ||
| "company_id": self.company.id, | ||
| } | ||
| ) | ||
|
|
||
| calls = {"count": 0, "args": None} | ||
|
|
||
| ItemModel = type(item) | ||
| RuleModel = type(rule) | ||
| PGModel = type(self.env["procurement.group"]) | ||
|
|
||
| original_prepare = ItemModel._prepare_procurement_values | ||
| original_run_pull = RuleModel._run_pull | ||
| original_pg_run = PGModel.run | ||
|
|
||
| def _fake_prepare_procurement_values(_self): | ||
| vals = original_prepare(_self) | ||
| vals["rule_id"] = rule.id | ||
| return vals | ||
|
|
||
| def _fake_run_pull(_self, procurements): | ||
| calls["count"] += 1 | ||
| calls["args"] = procurements | ||
| return True | ||
|
|
||
| def _fake_pg_run(_self, procurements): | ||
| raise AssertionError("Shouldn't call procurement.group.run()") | ||
|
|
||
| try: | ||
| ItemModel._prepare_procurement_values = _fake_prepare_procurement_values | ||
| RuleModel._run_pull = _fake_run_pull | ||
| PGModel.run = _fake_pg_run | ||
| wiz.make_procurement() | ||
| finally: | ||
| ItemModel._prepare_procurement_values = original_prepare | ||
| RuleModel._run_pull = original_run_pull | ||
| PGModel.run = original_pg_run | ||
|
|
||
| self.assertEqual(calls["count"], 1) | ||
| self.assertTrue(calls["args"]) | ||
| self.assertEqual(calls["args"][0][1].id, rule.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| def test_31_procure_wizard_forced_rule_pull_calls_run_pull(self): | |
| """Covers the branch where a forced rule triggers `_run_pull`.""" | |
| mrp_inv = self.mrp_inventory_obj.search( | |
| [("product_mrp_area_id.product_id", "=", self.fp_1.id)], limit=1 | |
| ) | |
| self.assertTrue(mrp_inv) | |
| wiz = self.mrp_inventory_procure_wiz.with_context( | |
| active_model="mrp.inventory", | |
| active_ids=mrp_inv.ids, | |
| active_id=mrp_inv.id, | |
| ).create({}) | |
| self.assertTrue(wiz.item_ids) | |
| item = wiz.item_ids[0] | |
| item.qty = 1.0 | |
| route = self.env["stock.route"].create( | |
| { | |
| "name": "Test route (mrp_multi_level)", | |
| "product_selectable": False, | |
| "warehouse_selectable": False, | |
| "company_id": self.company.id, | |
| } | |
| ) | |
| picking_type = item.warehouse_id.in_type_id | |
| self.assertTrue(picking_type) | |
| rule = self.env["stock.rule"].create( | |
| { | |
| "name": "Test pull rule (mrp_multi_level)", | |
| "route_id": route.id, | |
| "action": "pull", | |
| "location_src_id": item.location_id.id, | |
| "location_dest_id": item.location_id.id, | |
| "picking_type_id": picking_type.id, | |
| "company_id": self.company.id, | |
| } | |
| ) | |
| calls = {"count": 0, "args": None} | |
| ItemModel = type(item) | |
| RuleModel = type(rule) | |
| PGModel = type(self.env["procurement.group"]) | |
| original_prepare = ItemModel._prepare_procurement_values | |
| original_run_pull = RuleModel._run_pull | |
| original_pg_run = PGModel.run | |
| def _fake_prepare_procurement_values(_self): | |
| vals = original_prepare(_self) | |
| vals["rule_id"] = rule.id | |
| return vals | |
| def _fake_run_pull(_self, procurements): | |
| calls["count"] += 1 | |
| calls["args"] = procurements | |
| return True | |
| def _fake_pg_run(_self, procurements): | |
| raise AssertionError("Shouldn't call procurement.group.run()") | |
| try: | |
| ItemModel._prepare_procurement_values = _fake_prepare_procurement_values | |
| RuleModel._run_pull = _fake_run_pull | |
| PGModel.run = _fake_pg_run | |
| wiz.make_procurement() | |
| finally: | |
| ItemModel._prepare_procurement_values = original_prepare | |
| RuleModel._run_pull = original_run_pull | |
| PGModel.run = original_pg_run | |
| self.assertEqual(calls["count"], 1) | |
| self.assertTrue(calls["args"]) | |
| self.assertEqual(calls["args"][0][1].id, rule.id) | |
| def test_31_get_rule_respects_mrp_action(self): | |
| """Test that _get_rule returns rule matching mrp_action when provided.""" | |
| mrp_inv = self.mrp_inventory_obj.search( | |
| [("product_mrp_area_id.product_id", "=", self.fp_1.id)], limit=1 | |
| ) | |
| self.assertTrue(mrp_inv) | |
| wiz = self.mrp_inventory_procure_wiz.with_context( | |
| active_model="mrp.inventory", | |
| active_ids=mrp_inv.ids, | |
| active_id=mrp_inv.id, | |
| ).create({}) | |
| self.assertTrue(wiz.item_ids) | |
| item = wiz.item_ids[0] | |
| route = self.env["stock.route"].create( | |
| { | |
| "name": "Test route (mrp_multi_level)", | |
| "product_selectable": False, | |
| "warehouse_selectable": False, | |
| "company_id": self.company.id, | |
| } | |
| ) | |
| picking_type = item.warehouse_id.in_type_id | |
| pull_rule = self.env["stock.rule"].create( | |
| { | |
| "name": "Test pull rule (mrp_multi_level)", | |
| "route_id": route.id, | |
| "action": "pull", | |
| "location_src_id": self.supplier_location.id, | |
| "location_dest_id": item.location_id.id, | |
| "picking_type_id": picking_type.id, | |
| "company_id": self.company.id, | |
| } | |
| ) | |
| # Veriffy _get_rule returns pull rule when mrp_action='pull' | |
| pg = self.env["procurement.group"] | |
| values = { | |
| "mrp_action": "pull", | |
| "company_id": self.company, | |
| "warehouse_id": item.warehouse_id, | |
| } | |
| found_rule = pg._get_rule(item.product_id, item.location_id, values) | |
| self.assertEqual(found_rule.id, pull_rule.id) | |
| self.assertEqual(found_rule.action, "pull") | |
Reescribir test para verificar el nuevo comportamiento: que _get_rule respeta el mrp_action pasado en los values y devuelve la regla correcta. El test anterior verificaba que se llamaba directamente a _run_pull, lo cual ya no aplica.
275e80e to
23a38cf
Compare
| def _get_rule(self, product_id, location_id, values): | ||
| """Override to respect mrp_action from MRP Multi Level wizard.""" | ||
| mrp_action = values.get("mrp_action") | ||
| if mrp_action and mrp_action not in ("none", False): | ||
| company = values.get("company_id", self.env.company) | ||
| if hasattr(company, "id"): | ||
| company_id = company.id | ||
| else: | ||
| company_id = company | ||
| rule = self.env["stock.rule"].search( | ||
| [ | ||
| ("action", "=", mrp_action), | ||
| ("location_dest_id", "=", location_id.id), | ||
| ("company_id", "in", [company_id, False]), | ||
| ], | ||
| order="sequence", | ||
| limit=1, | ||
| ) | ||
| if not rule: | ||
| warehouse = values.get("warehouse_id") | ||
| if warehouse: | ||
| warehouse_id = ( | ||
| warehouse.id if hasattr(warehouse, "id") else warehouse | ||
| ) | ||
| rule = self.env["stock.rule"].search( | ||
| [ | ||
| ("action", "=", mrp_action), | ||
| ("warehouse_id", "=", warehouse_id), | ||
| ("company_id", "in", [company_id, False]), | ||
| ], | ||
| order="sequence", | ||
| limit=1, | ||
| ) | ||
| if rule: | ||
| return rule | ||
| return super()._get_rule(product_id, location_id, values) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| def _get_rule(self, product_id, location_id, values): | |
| """Override to respect mrp_action from MRP Multi Level wizard.""" | |
| mrp_action = values.get("mrp_action") | |
| if mrp_action and mrp_action not in ("none", False): | |
| company = values.get("company_id", self.env.company) | |
| if hasattr(company, "id"): | |
| company_id = company.id | |
| else: | |
| company_id = company | |
| rule = self.env["stock.rule"].search( | |
| [ | |
| ("action", "=", mrp_action), | |
| ("location_dest_id", "=", location_id.id), | |
| ("company_id", "in", [company_id, False]), | |
| ], | |
| order="sequence", | |
| limit=1, | |
| ) | |
| if not rule: | |
| warehouse = values.get("warehouse_id") | |
| if warehouse: | |
| warehouse_id = ( | |
| warehouse.id if hasattr(warehouse, "id") else warehouse | |
| ) | |
| rule = self.env["stock.rule"].search( | |
| [ | |
| ("action", "=", mrp_action), | |
| ("warehouse_id", "=", warehouse_id), | |
| ("company_id", "in", [company_id, False]), | |
| ], | |
| order="sequence", | |
| limit=1, | |
| ) | |
| if rule: | |
| return rule | |
| return super()._get_rule(product_id, location_id, values) | |
| def _get_rule(self, product_id, location_id, values): | |
| """Override to respect mrp_action from MRP Multi Level wizard.""" | |
| mrp_action = values.get("mrp_action") | |
| # If there is no valid mrp_action (None, False, 'none', ''), delegate to super | |
| if not mrp_action or mrp_action in ("none", False): | |
| return super()._get_rule(product_id, location_id, values) | |
| # Normalize company (accept record or id) | |
| company = values.get("company_id", self.env.company) | |
| company_id = company.id if hasattr(company, "id") else company | |
| domain = [ | |
| ("action", "=", mrp_action), | |
| ("company_id", "in", [company_id, False]), | |
| ] | |
| rule = self.env["stock.rule"].search( | |
| domain + [("location_dest_id", "=", location_id.id)], | |
| order="sequence", | |
| limit=1, | |
| ) | |
| if not rule: | |
| warehouse = values.get("warehouse_id") | |
| if warehouse: | |
| warehouse_id = ( | |
| warehouse.id if hasattr(warehouse, "id") else warehouse | |
| ) | |
| rule = self.env["stock.rule"].search( | |
| domain + [("warehouse_id", "=", warehouse_id)], | |
| order="sequence", | |
| limit=1, | |
| ) | |
| return rule or super()._get_rule(product_id, location_id, values) |
23a38cf to
8dc22e2
Compare
58a656a to
395fc70
Compare
…antity reset, and missing company/currency data
395fc70 to
204d148
Compare
[FIX] mrp_multi_level: Fix MRP wizard logic (PR #1683)
Why
Previously, the MRP Multi Level procurement wizard did not reliably allow users to explicitly choose the procurement method (manufacture/buy/pull/push/pull_push): the actual rule/action used at execution time could be driven by routes/rules and end up different from what the user intended.
This change makes the resulting procurement behavior consistent with the user’s explicit selection.
What changed
1) Wizard: propagate the selected method + keep better context
In
mrp_multi_level/wizards/mrp_inventory_procure.py:source_context(planned order vs inventory)original_qty(used to keep quantities stable when changing the UoM)values["mrp_action"]so the downstream rule selection can use it.2) New file: procurement group override (new flow)
Added
mrp_multi_level/models/procurement_group.py:procurement.group._get_rule()to respectmrp_actionwhen it is provided invalues.company_id in [company, False]order="sequence"to pick the highest priority rule3) Multi-currency POs: avoid cross-currency draft reuse
In
mrp_multi_level/models/stock_rule.py:currency_idin the draft PO lookup domain when present invalues.currency_idis provided (supports both a singledictand groupedvalues).4) UI: make the method clearly editable
In
mrp_multi_level/wizards/mrp_inventory_procure_views.xml:editable="top").buy.5) Planned orders: clearer action + direct entrypoint
In
mrp_multi_level/views/mrp_planned_order_views.xml:mrp_actionvisibility using a badge + decorations.6) Change supply_method string:
7) Tests
In
mrp_multi_level/tests/test_mrp_multi_level.py:_get_rule()respectsmrp_action.Modified files
mrp_multi_level/models/__init__.pymrp_multi_level/models/procurement_group.py(new)mrp_multi_level/models/mrp_inventory.pymrp_multi_level/models/stock_rule.pymrp_multi_level/models/product_mrp_area.pymrp_multi_level/wizards/mrp_inventory_procure.pymrp_multi_level/wizards/mrp_inventory_procure_views.xmlmrp_multi_level/views/mrp_planned_order_views.xmlmrp_multi_level/tests/test_mrp_multi_level.py