summaryrefslogtreecommitdiff
path: root/addons/mrp/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mrp/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mrp/tests')
-rw-r--r--addons/mrp/tests/__init__.py15
-rw-r--r--addons/mrp/tests/common.py156
-rw-r--r--addons/mrp/tests/test_backorder.py360
-rw-r--r--addons/mrp/tests/test_bom.py832
-rw-r--r--addons/mrp/tests/test_byproduct.py118
-rw-r--r--addons/mrp/tests/test_cancel_mo.py103
-rw-r--r--addons/mrp/tests/test_multicompany.py187
-rw-r--r--addons/mrp/tests/test_oee.py71
-rw-r--r--addons/mrp/tests/test_order.py2055
-rw-r--r--addons/mrp/tests/test_procurement.py598
-rw-r--r--addons/mrp/tests/test_stock.py350
-rw-r--r--addons/mrp/tests/test_stock_report.py153
-rw-r--r--addons/mrp/tests/test_traceability.py308
-rw-r--r--addons/mrp/tests/test_unbuild.py599
-rw-r--r--addons/mrp/tests/test_warehouse_multistep_manufacturing.py507
15 files changed, 6412 insertions, 0 deletions
diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py
new file mode 100644
index 00000000..73cf6398
--- /dev/null
+++ b/addons/mrp/tests/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+
+from . import test_bom
+from . import test_byproduct
+from . import test_cancel_mo
+from . import test_order
+from . import test_stock
+from . import test_stock_report
+from . import test_warehouse_multistep_manufacturing
+from . import test_procurement
+from . import test_unbuild
+from . import test_oee
+from . import test_traceability
+from . import test_multicompany
+from . import test_backorder
diff --git a/addons/mrp/tests/common.py b/addons/mrp/tests/common.py
new file mode 100644
index 00000000..502b4a94
--- /dev/null
+++ b/addons/mrp/tests/common.py
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+
+from odoo.tests import Form
+from odoo.addons.mail.tests.common import mail_new_test_user
+from odoo.addons.stock.tests import common2
+
+
+class TestMrpCommon(common2.TestStockCommon):
+
+ @classmethod
+ def generate_mo(self, tracking_final='none', tracking_base_1='none', tracking_base_2='none', qty_final=5, qty_base_1=4, qty_base_2=1, picking_type_id=False, consumption=False):
+ """ This function generate a manufacturing order with one final
+ product and two consumed product. Arguments allows to choose
+ the tracking/qty for each different products. It returns the
+ MO, used bom and the tree products.
+ """
+ product_to_build = self.env['product.product'].create({
+ 'name': 'Young Tom',
+ 'type': 'product',
+ 'tracking': tracking_final,
+ })
+ product_to_use_1 = self.env['product.product'].create({
+ 'name': 'Botox',
+ 'type': 'product',
+ 'tracking': tracking_base_1,
+ })
+ product_to_use_2 = self.env['product.product'].create({
+ 'name': 'Old Tom',
+ 'type': 'product',
+ 'tracking': tracking_base_2,
+ })
+ bom_1 = self.env['mrp.bom'].create({
+ 'product_id': product_to_build.id,
+ 'product_tmpl_id': product_to_build.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'consumption': consumption if consumption else 'flexible',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_to_use_2.id, 'product_qty': qty_base_2}),
+ (0, 0, {'product_id': product_to_use_1.id, 'product_qty': qty_base_1})
+ ]})
+ mo_form = Form(self.env['mrp.production'])
+ if picking_type_id:
+ mo_form.picking_type_id = picking_type_id
+ mo_form.product_id = product_to_build
+ mo_form.bom_id = bom_1
+ mo_form.product_qty = qty_final
+ mo = mo_form.save()
+ mo.action_confirm()
+ return mo, bom_1, product_to_build, product_to_use_1, product_to_use_2
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMrpCommon, cls).setUpClass()
+
+ # Update demo products
+ (cls.product_2 | cls.product_3 | cls.product_4 | cls.product_5 | cls.product_6 | cls.product_7_3 | cls.product_8).write({
+ 'type': 'product',
+ })
+
+ # User Data: mrp user and mrp manager
+ cls.user_mrp_user = mail_new_test_user(
+ cls.env,
+ name='Hilda Ferachwal',
+ login='hilda',
+ email='h.h@example.com',
+ notification_type='inbox',
+ groups='mrp.group_mrp_user, stock.group_stock_user, mrp.group_mrp_byproducts',
+ )
+ cls.user_mrp_manager = mail_new_test_user(
+ cls.env,
+ name='Gary Youngwomen',
+ login='gary',
+ email='g.g@example.com',
+ notification_type='inbox',
+ groups='mrp.group_mrp_manager, stock.group_stock_user, mrp.group_mrp_byproducts',
+ )
+
+ cls.workcenter_1 = cls.env['mrp.workcenter'].create({
+ 'name': 'Nuclear Workcenter',
+ 'capacity': 2,
+ 'time_start': 10,
+ 'time_stop': 5,
+ 'time_efficiency': 80,
+ })
+
+ cls.bom_1 = cls.env['mrp.bom'].create({
+ 'product_id': cls.product_4.id,
+ 'product_tmpl_id': cls.product_4.product_tmpl_id.id,
+ 'product_uom_id': cls.uom_unit.id,
+ 'product_qty': 4.0,
+ 'consumption': 'flexible',
+ 'operation_ids': [
+ ],
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
+ (0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
+ ]})
+ cls.bom_2 = cls.env['mrp.bom'].create({
+ 'product_id': cls.product_5.id,
+ 'product_tmpl_id': cls.product_5.product_tmpl_id.id,
+ 'product_uom_id': cls.product_5.uom_id.id,
+ 'consumption': 'flexible',
+ 'product_qty': 1.0,
+ 'operation_ids': [
+ (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
+ ],
+ 'type': 'phantom',
+ 'sequence': 2,
+ 'bom_line_ids': [
+ (0, 0, {'product_id': cls.product_4.id, 'product_qty': 2}),
+ (0, 0, {'product_id': cls.product_3.id, 'product_qty': 3})
+ ]})
+ cls.bom_3 = cls.env['mrp.bom'].create({
+ 'product_id': cls.product_6.id,
+ 'product_tmpl_id': cls.product_6.product_tmpl_id.id,
+ 'product_uom_id': cls.uom_dozen.id,
+ 'consumption': 'flexible',
+ 'product_qty': 2.0,
+ 'operation_ids': [
+ (0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
+ (0, 0, {'name': 'Weld Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
+ ],
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': cls.product_5.id, 'product_qty': 2}),
+ (0, 0, {'product_id': cls.product_4.id, 'product_qty': 8}),
+ (0, 0, {'product_id': cls.product_2.id, 'product_qty': 12})
+ ]})
+
+ cls.stock_location_14 = cls.env['stock.location'].create({
+ 'name': 'Shelf 2',
+ 'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
+ })
+ cls.stock_location_components = cls.env['stock.location'].create({
+ 'name': 'Shelf 1',
+ 'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
+ })
+ cls.laptop = cls.env['product.product'].create({
+ 'name': 'Acoustic Bloc Screens',
+ 'uom_id': cls.env.ref("uom.product_uom_unit").id,
+ 'uom_po_id': cls.env.ref("uom.product_uom_unit").id,
+ 'type': 'product',
+ 'tracking': 'none',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.graphics_card = cls.env['product.product'].create({
+ 'name': 'Individual Workplace',
+ 'uom_id': cls.env.ref("uom.product_uom_unit").id,
+ 'uom_po_id': cls.env.ref("uom.product_uom_unit").id,
+ 'type': 'product',
+ 'tracking': 'none',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
diff --git a/addons/mrp/tests/test_backorder.py b/addons/mrp/tests/test_backorder.py
new file mode 100644
index 00000000..eadd7847
--- /dev/null
+++ b/addons/mrp/tests/test_backorder.py
@@ -0,0 +1,360 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.mrp.tests.common import TestMrpCommon
+from odoo.tests import Form
+from odoo.tests.common import SavepointCase
+
+
+class TestMrpProductionBackorder(TestMrpCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+
+ def setUp(self):
+ super().setUp()
+ warehouse_form = Form(self.env['stock.warehouse'])
+ warehouse_form.name = 'Test Warehouse'
+ warehouse_form.code = 'TWH'
+ self.warehouse = warehouse_form.save()
+
+ def test_no_tracking_1(self):
+ """Create a MO for 4 product. Produce 4. The backorder button should
+ not appear and hitting mark as done should not open the backorder wizard.
+ The name of the MO should be MO/001.
+ """
+ mo = self.generate_mo(qty_final=4)[0]
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 4
+ mo = mo_form.save()
+
+ # No backorder is proposed
+ self.assertTrue(mo.button_mark_done())
+ self.assertEqual(mo._get_quantity_to_backorder(), 0)
+ self.assertTrue("-001" not in mo.name)
+
+ def test_no_tracking_2(self):
+ """Create a MO for 4 product. Produce 1. The backorder button should
+ appear and hitting mark as done should open the backorder wizard. In the backorder
+ wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
+ created.
+ The sequence of the first MO should be MO/001-01, the sequence of the second MO
+ should be MO/001-02.
+ Check that all MO are reachable through the procurement group.
+ """
+ production, _, _, product_to_use_1, _ = self.generate_mo(qty_final=4, qty_base_1=3)
+ self.assertEqual(production.state, 'confirmed')
+ self.assertEqual(production.reserve_visible, True)
+
+ # Make some stock and reserve
+ for product in production.move_raw_ids.product_id:
+ self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': product.id,
+ 'inventory_quantity': 100,
+ 'location_id': production.location_src_id.id,
+ })
+ production.action_assign()
+ self.assertEqual(production.state, 'confirmed')
+ self.assertEqual(production.reserve_visible, False)
+
+ mo_form = Form(production)
+ mo_form.qty_producing = 1
+ production = mo_form.save()
+
+ action = production.button_mark_done()
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_backorder()
+
+ # Two related MO to the procurement group
+ self.assertEqual(len(production.procurement_group_id.mrp_production_ids), 2)
+
+ # Check MO backorder
+ mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
+ self.assertEqual(mo_backorder.product_id.id, production.product_id.id)
+ self.assertEqual(mo_backorder.product_qty, 3)
+ self.assertEqual(sum(mo_backorder.move_raw_ids.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_uom_qty")), 9)
+ self.assertEqual(mo_backorder.reserve_visible, False) # the reservation of the first MO should've been moved here
+
+ def test_no_tracking_pbm_1(self):
+ """Create a MO for 4 product. Produce 1. The backorder button should
+ appear and hitting mark as done should open the backorder wizard. In the backorder
+ wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
+ created.
+ The sequence of the first MO should be MO/001-01, the sequence of the second MO
+ should be MO/001-02.
+ Check that all MO are reachable through the procurement group.
+ """
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm'
+
+ production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id)
+
+ move_raw_ids = production.move_raw_ids
+ self.assertEqual(len(move_raw_ids), 2)
+ self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2})
+
+ pbm_move = move_raw_ids.move_orig_ids
+ self.assertEqual(len(pbm_move), 2)
+ self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2})
+ self.assertFalse(pbm_move.move_orig_ids)
+
+ mo_form = Form(production)
+ mo_form.qty_producing = 1
+ production = mo_form.save()
+ self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
+ self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
+
+ action = production.button_mark_done()
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_backorder()
+
+ mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
+ self.assertEqual(mo_backorder.delivery_count, 1)
+
+ pbm_move |= mo_backorder.move_raw_ids.move_orig_ids
+ # Check that quantity is correct
+ self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
+ self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
+
+ self.assertFalse(pbm_move.move_orig_ids)
+
+ def test_no_tracking_pbm_sam_1(self):
+ """Create a MO for 4 product. Produce 1. The backorder button should
+ appear and hitting mark as done should open the backorder wizard. In the backorder
+ wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
+ created.
+ The sequence of the first MO should be MO/001-01, the sequence of the second MO
+ should be MO/001-02.
+ Check that all MO are reachable through the procurement group.
+ """
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm_sam'
+ production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id)
+
+ move_raw_ids = production.move_raw_ids
+ self.assertEqual(len(move_raw_ids), 2)
+ self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2})
+
+ pbm_move = move_raw_ids.move_orig_ids
+ self.assertEqual(len(pbm_move), 2)
+ self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2})
+ self.assertFalse(pbm_move.move_orig_ids)
+ self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
+ self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
+
+ sam_move = production.move_finished_ids.move_dest_ids
+ self.assertEqual(len(sam_move), 1)
+ self.assertEqual(sam_move.product_id.id, product_to_build.id)
+ self.assertEqual(sum(sam_move.mapped("product_qty")), 4)
+
+ mo_form = Form(production)
+ mo_form.qty_producing = 1
+ production = mo_form.save()
+
+ action = production.button_mark_done()
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_backorder()
+
+ mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
+ self.assertEqual(mo_backorder.delivery_count, 2)
+
+ pbm_move |= mo_backorder.move_raw_ids.move_orig_ids
+ self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
+ self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
+
+ sam_move |= mo_backorder.move_finished_ids.move_orig_ids
+ self.assertEqual(sum(sam_move.mapped("product_qty")), 4)
+
+ def test_tracking_backorder_series_lot_1(self):
+ """ Create a MO of 4 tracked products. all component is tracked by lots
+ Produce one by one with one bakorder for each until end.
+ """
+ nb_product_todo = 4
+ production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='lot', tracking_base_1='lot', tracking_base_2='lot')
+ lot_final = self.env['stock.production.lot'].create({
+ 'name': 'lot_final',
+ 'product_id': p_final.id,
+ 'company_id': self.env.company.id,
+ })
+ lot_1 = self.env['stock.production.lot'].create({
+ 'name': 'lot_consumed_1',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ })
+ lot_2 = self.env['stock.production.lot'].create({
+ 'name': 'lot_consumed_2',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, nb_product_todo*4, lot_id=lot_1)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, nb_product_todo, lot_id=lot_2)
+
+ production.action_assign()
+ active_production = production
+ for i in range(nb_product_todo):
+
+ details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 4
+ ml.lot_id = lot_1
+ details_operation_form.save()
+ details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 1
+ ml.lot_id = lot_2
+ details_operation_form.save()
+
+ production_form = Form(active_production)
+ production_form.qty_producing = 1
+ production_form.lot_producing_id = lot_final
+ active_production = production_form.save()
+
+ active_production.button_mark_done()
+ if i + 1 != nb_product_todo: # If last MO, don't make a backorder
+ action = active_production.button_mark_done()
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_backorder()
+ active_production = active_production.procurement_group_id.mrp_production_ids[-1]
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), nb_product_todo, f'You should have the {nb_product_todo} final product in stock')
+ self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo)
+
+ def test_tracking_backorder_series_serial_1(self):
+ """ Create a MO of 4 tracked products (serial) with pbm_sam.
+ all component is tracked by serial
+ Produce one by one with one bakorder for each until end.
+ """
+ nb_product_todo = 4
+ production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='serial', tracking_base_1='serial', tracking_base_2='serial', qty_base_1=1)
+ serials_final, serials_p1, serials_p2 = [], [], []
+ for i in range(nb_product_todo):
+ serials_final.append(self.env['stock.production.lot'].create({
+ 'name': f'lot_final_{i}',
+ 'product_id': p_final.id,
+ 'company_id': self.env.company.id,
+ }))
+ serials_p1.append(self.env['stock.production.lot'].create({
+ 'name': f'lot_consumed_1_{i}',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ }))
+ serials_p2.append(self.env['stock.production.lot'].create({
+ 'name': f'lot_consumed_2_{i}',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ }))
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1, lot_id=serials_p1[-1])
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=serials_p2[-1])
+
+ production.action_assign()
+ active_production = production
+ for i in range(nb_product_todo):
+
+ details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 1
+ ml.lot_id = serials_p1[i]
+ details_operation_form.save()
+ details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 1
+ ml.lot_id = serials_p2[i]
+ details_operation_form.save()
+
+ production_form = Form(active_production)
+ production_form.qty_producing = 1
+ production_form.lot_producing_id = serials_final[i]
+ active_production = production_form.save()
+
+ active_production.button_mark_done()
+ if i + 1 != nb_product_todo: # If last MO, don't make a backorder
+ action = active_production.button_mark_done()
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_backorder()
+ active_production = active_production.procurement_group_id.mrp_production_ids[-1]
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), nb_product_todo, f'You should have the {nb_product_todo} final product in stock')
+ self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo)
+
+ def test_backorder_name(self):
+ def produce_one(mo):
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ mo = mo_form.save()
+ action = mo.button_mark_done()
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_backorder()
+ return mo.procurement_group_id.mrp_production_ids[-1]
+
+ default_picking_type_id = self.env['mrp.production']._get_default_picking_type()
+ default_picking_type = self.env['stock.picking.type'].browse(default_picking_type_id)
+ mo_sequence = default_picking_type.sequence_id
+
+ mo_sequence.prefix = "WH-MO-"
+ initial_mo_name = mo_sequence.prefix + str(mo_sequence.number_next_actual).zfill(mo_sequence.padding)
+
+ production = self.generate_mo(qty_final=5)[0]
+ self.assertEqual(production.name, initial_mo_name)
+
+ backorder = produce_one(production)
+ self.assertEqual(production.name, initial_mo_name + "-001")
+ self.assertEqual(backorder.name, initial_mo_name + "-002")
+
+ backorder.backorder_sequence = 998
+
+ for seq in [998, 999, 1000]:
+ new_backorder = produce_one(backorder)
+ self.assertEqual(backorder.name, initial_mo_name + "-" + str(seq))
+ self.assertEqual(new_backorder.name, initial_mo_name + "-" + str(seq + 1))
+ backorder = new_backorder
+
+
+class TestMrpWorkorderBackorder(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestMrpWorkorderBackorder, cls).setUpClass()
+ cls.uom_unit = cls.env['uom.uom'].search([
+ ('category_id', '=', cls.env.ref('uom.product_uom_categ_unit').id),
+ ('uom_type', '=', 'reference')
+ ], limit=1)
+ cls.finished1 = cls.env['product.product'].create({
+ 'name': 'finished1',
+ 'type': 'product',
+ })
+ cls.compfinished1 = cls.env['product.product'].create({
+ 'name': 'compfinished1',
+ 'type': 'product',
+ })
+ cls.compfinished2 = cls.env['product.product'].create({
+ 'name': 'compfinished2',
+ 'type': 'product',
+ })
+ cls.workcenter1 = cls.env['mrp.workcenter'].create({
+ 'name': 'workcenter1',
+ })
+ cls.workcenter2 = cls.env['mrp.workcenter'].create({
+ 'name': 'workcenter2',
+ })
+
+ cls.bom_finished1 = cls.env['mrp.bom'].create({
+ 'product_id': cls.finished1.id,
+ 'product_tmpl_id': cls.finished1.product_tmpl_id.id,
+ 'product_uom_id': cls.uom_unit.id,
+ 'product_qty': 1,
+ 'consumption': 'flexible',
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': cls.compfinished1.id, 'product_qty': 1}),
+ (0, 0, {'product_id': cls.compfinished2.id, 'product_qty': 1}),
+ ],
+ 'operation_ids': [
+ (0, 0, {'sequence': 1, 'name': 'finished operation 1', 'workcenter_id': cls.workcenter1.id}),
+ (0, 0, {'sequence': 2, 'name': 'finished operation 2', 'workcenter_id': cls.workcenter2.id}),
+ ],
+ })
+ cls.bom_finished1.bom_line_ids[0].operation_id = cls.bom_finished1.operation_ids[0].id
+ cls.bom_finished1.bom_line_ids[1].operation_id = cls.bom_finished1.operation_ids[1].id
diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py
new file mode 100644
index 00000000..bfa47231
--- /dev/null
+++ b/addons/mrp/tests/test_bom.py
@@ -0,0 +1,832 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import exceptions
+from odoo.tests import Form
+from odoo.addons.mrp.tests.common import TestMrpCommon
+from odoo.tools import float_compare, float_round
+
+
+class TestBoM(TestMrpCommon):
+
+ def test_01_explode(self):
+ boms, lines = self.bom_1.explode(self.product_4, 3)
+ self.assertEqual(set([bom[0].id for bom in boms]), set(self.bom_1.ids))
+ self.assertEqual(set([line[0].id for line in lines]), set(self.bom_1.bom_line_ids.ids))
+
+ boms, lines = self.bom_3.explode(self.product_6, 3)
+ self.assertEqual(set([bom[0].id for bom in boms]), set((self.bom_2 | self.bom_3).ids))
+ self.assertEqual(
+ set([line[0].id for line in lines]),
+ set((self.bom_2 | self.bom_3).mapped('bom_line_ids').filtered(lambda line: not line.child_bom_id or line.child_bom_id.type != 'phantom').ids))
+
+ def test_10_variants(self):
+ test_bom = self.env['mrp.bom'].create({
+ 'product_id': self.product_7_3.id,
+ 'product_tmpl_id': self.product_7_template.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 4.0,
+ 'type': 'normal',
+ })
+ test_bom.write({
+ 'operation_ids': [
+ (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
+ (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
+ ],
+ })
+ test_bom_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom.id,
+ 'product_id': self.product_2.id,
+ 'product_qty': 2,
+ })
+ test_bom_l2 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom.id,
+ 'product_id': self.product_3.id,
+ 'product_qty': 2,
+ 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v1.id)],
+ })
+ test_bom_l3 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom.id,
+ 'product_id': self.product_4.id,
+ 'product_qty': 2,
+ 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v2.id)],
+ })
+ boms, lines = test_bom.explode(self.product_7_3, 4)
+ self.assertIn(test_bom, [b[0]for b in boms])
+ self.assertIn(test_bom_l1, [l[0] for l in lines])
+ self.assertNotIn(test_bom_l2, [l[0] for l in lines])
+ self.assertNotIn(test_bom_l3, [l[0] for l in lines])
+
+ boms, lines = test_bom.explode(self.product_7_1, 4)
+ self.assertIn(test_bom, [b[0]for b in boms])
+ self.assertIn(test_bom_l1, [l[0] for l in lines])
+ self.assertIn(test_bom_l2, [l[0] for l in lines])
+ self.assertNotIn(test_bom_l3, [l[0] for l in lines])
+
+ boms, lines = test_bom.explode(self.product_7_2, 4)
+ self.assertIn(test_bom, [b[0]for b in boms])
+ self.assertIn(test_bom_l1, [l[0] for l in lines])
+ self.assertNotIn(test_bom_l2, [l[0] for l in lines])
+ self.assertIn(test_bom_l3, [l[0] for l in lines])
+
+ def test_11_multi_level_variants(self):
+ tmp_picking_type = self.env['stock.picking.type'].create({
+ 'name': 'Manufacturing',
+ 'code': 'mrp_operation',
+ 'sequence_code': 'TMP',
+ 'sequence_id': self.env['ir.sequence'].create({
+ 'code': 'mrp.production',
+ 'name': 'tmp_production_sequence',
+ }).id,
+ })
+ test_bom_1 = self.env['mrp.bom'].create({
+ 'product_tmpl_id': self.product_5.product_tmpl_id.id,
+ 'product_uom_id': self.product_5.uom_id.id,
+ 'product_qty': 1.0,
+ 'type': 'phantom'
+ })
+ test_bom_1.write({
+ 'operation_ids': [
+ (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
+ ],
+ })
+ test_bom_1_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_1.id,
+ 'product_id': self.product_3.id,
+ 'product_qty': 3,
+ })
+
+ test_bom_2 = self.env['mrp.bom'].create({
+ 'product_id': self.product_7_3.id,
+ 'product_tmpl_id': self.product_7_template.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 4.0,
+ 'type': 'normal',
+ })
+ test_bom_2.write({
+ 'operation_ids': [
+ (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
+ (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
+ ]
+ })
+ test_bom_2_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_2.id,
+ 'product_id': self.product_2.id,
+ 'product_qty': 2,
+ })
+ test_bom_2_l2 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_2.id,
+ 'product_id': self.product_5.id,
+ 'product_qty': 2,
+ 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v1.id)],
+ })
+ test_bom_2_l3 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_2.id,
+ 'product_id': self.product_5.id,
+ 'product_qty': 2,
+ 'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v2.id)],
+ })
+ test_bom_2_l4 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_2.id,
+ 'product_id': self.product_4.id,
+ 'product_qty': 2,
+ })
+
+ # check product > product_tmpl
+ boms, lines = test_bom_2.explode(self.product_7_1, 4)
+ self.assertEqual(set((test_bom_2 | self.bom_2).ids), set([b[0].id for b in boms]))
+ self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | self.bom_2.bom_line_ids).ids), set([l[0].id for l in lines]))
+
+ # check sequence priority
+ test_bom_1.write({'sequence': 1})
+ boms, lines = test_bom_2.explode(self.product_7_1, 4)
+ self.assertEqual(set((test_bom_2 | test_bom_1).ids), set([b[0].id for b in boms]))
+ self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | test_bom_1.bom_line_ids).ids), set([l[0].id for l in lines]))
+
+ # check with another picking_type
+ test_bom_1.write({'picking_type_id': self.warehouse_1.manu_type_id.id})
+ self.bom_2.write({'picking_type_id': tmp_picking_type.id})
+ test_bom_2.write({'picking_type_id': tmp_picking_type.id})
+ boms, lines = test_bom_2.explode(self.product_7_1, 4)
+ self.assertEqual(set((test_bom_2 | self.bom_2).ids), set([b[0].id for b in boms]))
+ self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | self.bom_2.bom_line_ids).ids), set([l[0].id for l in lines]))
+
+ #check recursion
+ test_bom_3 = self.env['mrp.bom'].create({
+ 'product_id': self.product_9.id,
+ 'product_tmpl_id': self.product_9.product_tmpl_id.id,
+ 'product_uom_id': self.product_9.uom_id.id,
+ 'product_qty': 1.0,
+ 'consumption': 'flexible',
+ 'type': 'normal'
+ })
+ test_bom_4 = self.env['mrp.bom'].create({
+ 'product_id': self.product_10.id,
+ 'product_tmpl_id': self.product_10.product_tmpl_id.id,
+ 'product_uom_id': self.product_10.uom_id.id,
+ 'product_qty': 1.0,
+ 'consumption': 'flexible',
+ 'type': 'phantom'
+ })
+ test_bom_3_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_3.id,
+ 'product_id': self.product_10.id,
+ 'product_qty': 1.0,
+ })
+ test_bom_4_l1 = self.env['mrp.bom.line'].create({
+ 'bom_id': test_bom_4.id,
+ 'product_id': self.product_9.id,
+ 'product_qty': 1.0,
+ })
+ with self.assertRaises(exceptions.UserError):
+ test_bom_3.explode(self.product_9, 1)
+
+ def test_12_multi_level_variants2(self):
+ """Test skip bom line with same attribute values in bom lines."""
+
+ Product = self.env['product.product']
+ ProductAttribute = self.env['product.attribute']
+ ProductAttributeValue = self.env['product.attribute.value']
+
+ # Product Attribute
+ att_color = ProductAttribute.create({'name': 'Color', 'sequence': 1})
+ att_size = ProductAttribute.create({'name': 'size', 'sequence': 2})
+
+ # Product Attribute color Value
+ att_color_red = ProductAttributeValue.create({'name': 'red', 'attribute_id': att_color.id, 'sequence': 1})
+ att_color_blue = ProductAttributeValue.create({'name': 'blue', 'attribute_id': att_color.id, 'sequence': 2})
+ # Product Attribute size Value
+ att_size_big = ProductAttributeValue.create({'name': 'big', 'attribute_id': att_size.id, 'sequence': 1})
+ att_size_medium = ProductAttributeValue.create({'name': 'medium', 'attribute_id': att_size.id, 'sequence': 2})
+
+ # Create Template Product
+ product_template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'attribute_line_ids': [
+ (0, 0, {
+ 'attribute_id': att_color.id,
+ 'value_ids': [(6, 0, [att_color_red.id, att_color_blue.id])]
+ }),
+ (0, 0, {
+ 'attribute_id': att_size.id,
+ 'value_ids': [(6, 0, [att_size_big.id, att_size_medium.id])]
+ })
+ ]
+ })
+
+ sofa_red = product_template.attribute_line_ids[0].product_template_value_ids[0]
+ sofa_blue = product_template.attribute_line_ids[0].product_template_value_ids[1]
+
+ sofa_big = product_template.attribute_line_ids[1].product_template_value_ids[0]
+ sofa_medium = product_template.attribute_line_ids[1].product_template_value_ids[1]
+
+ # Create components Of BOM
+ product_A = Product.create({
+ 'name': 'Wood'})
+ product_B = Product.create({
+ 'name': 'Clothes'})
+
+ # Create BOM
+ self.env['mrp.bom'].create({
+ 'product_tmpl_id': product_template.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {
+ 'product_id': product_A.id,
+ 'product_qty': 1,
+ 'bom_product_template_attribute_value_ids': [(4, sofa_red.id), (4, sofa_blue.id), (4, sofa_big.id)],
+ }),
+ (0, 0, {
+ 'product_id': product_B.id,
+ 'product_qty': 1,
+ 'bom_product_template_attribute_value_ids': [(4, sofa_red.id), (4, sofa_blue.id)]
+ })
+ ]
+ })
+
+ dict_consumed_products = {
+ sofa_red + sofa_big: product_A + product_B,
+ sofa_red + sofa_medium: product_B,
+ sofa_blue + sofa_big: product_A + product_B,
+ sofa_blue + sofa_medium: product_B,
+ }
+
+ # Create production order for all variants.
+ for combination, consumed_products in dict_consumed_products.items():
+ product = product_template.product_variant_ids.filtered(lambda p: p.product_template_attribute_value_ids == combination)
+ mrp_order_form = Form(self.env['mrp.production'])
+ mrp_order_form.product_id = product
+ mrp_order = mrp_order_form.save()
+
+ # Check consumed materials in production order.
+ self.assertEqual(mrp_order.move_raw_ids.product_id, consumed_products)
+
+ def test_13_bom_kit_qty(self):
+ self.env['mrp.bom'].create({
+ 'product_id': self.product_7_3.id,
+ 'product_tmpl_id': self.product_7_template.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 4.0,
+ 'type': 'phantom',
+ 'bom_line_ids': [
+ (0, 0, {
+ 'product_id': self.product_2.id,
+ 'product_qty': 2,
+ }),
+ (0, 0, {
+ 'product_id': self.product_3.id,
+ 'product_qty': 2,
+ })
+ ]
+ })
+ location = self.env.ref('stock.stock_location_stock')
+ self.env['stock.quant']._update_available_quantity(self.product_2, location, 4.0)
+ self.env['stock.quant']._update_available_quantity(self.product_3, location, 8.0)
+ # Force the kit product available qty to be computed at the same time than its component quantities
+ # Because `qty_available` of a bom kit "recurse" on `qty_available` of its component,
+ # and this is a tricky thing for the ORM:
+ # `qty_available` gets called for `product_7_3`, `product_2` and `product_3`
+ # which then recurse on calling `qty_available` for `product_2` and `product_3` to compute the quantity of
+ # the kit `product_7_3`. `product_2` and `product_3` gets protected at the first call of the compute method,
+ # ending the recurse call to not call the compute method and just left the Falsy value `0.0`
+ # for the components available qty.
+ kit_product_qty, _, _ = (self.product_7_3 + self.product_2 + self.product_3).mapped("qty_available")
+ self.assertEqual(kit_product_qty, 2)
+
+ def test_20_bom_report(self):
+ """ Simulate a crumble receipt with mrp and open the bom structure
+ report and check that data insde are correct.
+ """
+ uom_kg = self.env.ref('uom.product_uom_kgm')
+ uom_litre = self.env.ref('uom.product_uom_litre')
+ crumble = self.env['product.product'].create({
+ 'name': 'Crumble',
+ 'type': 'product',
+ 'uom_id': uom_kg.id,
+ 'uom_po_id': uom_kg.id,
+ })
+ butter = self.env['product.product'].create({
+ 'name': 'Butter',
+ 'type': 'product',
+ 'uom_id': uom_kg.id,
+ 'uom_po_id': uom_kg.id,
+ 'standard_price': 7.01
+ })
+ biscuit = self.env['product.product'].create({
+ 'name': 'Biscuit',
+ 'type': 'product',
+ 'uom_id': uom_kg.id,
+ 'uom_po_id': uom_kg.id,
+ 'standard_price': 1.5
+ })
+ bom_form_crumble = Form(self.env['mrp.bom'])
+ bom_form_crumble.product_tmpl_id = crumble.product_tmpl_id
+ bom_form_crumble.product_qty = 11
+ bom_form_crumble.product_uom_id = uom_kg
+ bom_crumble = bom_form_crumble.save()
+
+ workcenter = self.env['mrp.workcenter'].create({
+ 'costs_hour': 10,
+ 'name': 'Deserts Table'
+ })
+
+ with Form(bom_crumble) as bom:
+ with bom.bom_line_ids.new() as line:
+ line.product_id = butter
+ line.product_uom_id = uom_kg
+ line.product_qty = 5
+ with bom.bom_line_ids.new() as line:
+ line.product_id = biscuit
+ line.product_uom_id = uom_kg
+ line.product_qty = 6
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter
+ operation.name = 'Prepare biscuits'
+ operation.time_cycle_manual = 5
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter
+ operation.name = 'Prepare butter'
+ operation.time_cycle_manual = 3
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter
+ operation.name = 'Mix manually'
+ operation.time_cycle_manual = 5
+
+ # TEST BOM STRUCTURE VALUE WITH BOM QUANTITY
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=11, searchVariant=False)
+ # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
+ self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch')
+ # Operation cost is the sum of operation line.
+ operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2)
+ self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16')
+
+ for component_line in report_values['lines']['components']:
+ # standard price * bom line quantity * current quantity / bom finished product quantity
+ if component_line['prod_id'] == butter.id:
+ # 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$
+ self.assertEqual(float_compare(component_line['total'], (7.01 * 5), precision_digits=2), 0)
+ if component_line['prod_id'] == biscuit.id:
+ # 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$
+ self.assertEqual(float_compare(component_line['total'], (1.5 * 6), precision_digits=2), 0)
+ # total price = 35.05 + 9 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 46,21
+ self.assertEqual(float_compare(report_values['lines']['total'], 46.21, precision_digits=2), 0, 'Product Bom Price is not correct')
+ self.assertEqual(float_compare(report_values['lines']['total'] / 11.0, 4.20, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
+
+ # TEST BOM STRUCTURE VALUE BY UNIT
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=1, searchVariant=False)
+ # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
+ self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch')
+ # Operation cost is the sum of operation line.
+ operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2)
+ self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16')
+
+ for component_line in report_values['lines']['components']:
+ # standard price * bom line quantity * current quantity / bom finished product quantity
+ if component_line['prod_id'] == butter.id:
+ # 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19)
+ self.assertEqual(float_compare(component_line['total'], (7.01 * 5) * (1 / 11), precision_digits=2), 0)
+ if component_line['prod_id'] == biscuit.id:
+ # 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82)
+ self.assertEqual(float_compare(component_line['total'], (1.5 * 6) * (1 / 11), precision_digits=2), 0)
+ # total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17
+ self.assertEqual(float_compare(report_values['lines']['total'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
+
+ # TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY
+ report_values_12 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=12, searchVariant=False)
+ report_values_22 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=22, searchVariant=False)
+ operation_cost = float_round(10 / 60 * 10, precision_digits=2) * 2 + float_round(6 / 60 * 10, precision_digits=2)
+ # Both needs 2 operation cycle
+ self.assertEqual(report_values_12['lines']['operations_cost'], report_values_22['lines']['operations_cost'])
+ self.assertEqual(report_values_22['lines']['operations_cost'], operation_cost)
+ report_values_23 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=23, searchVariant=False)
+ operation_cost = float_round(15 / 60 * 10, precision_digits=2) * 2 + float_round(9 / 60 * 10, precision_digits=2)
+ self.assertEqual(report_values_23['lines']['operations_cost'], operation_cost)
+
+ # Create a more complex BoM with a sub product
+ cheese_cake = self.env['product.product'].create({
+ 'name': 'Cheese Cake 300g',
+ 'type': 'product',
+ })
+ cream = self.env['product.product'].create({
+ 'name': 'cream',
+ 'type': 'product',
+ 'uom_id': uom_litre.id,
+ 'uom_po_id': uom_litre.id,
+ 'standard_price': 5.17,
+ })
+ bom_form_cheese_cake = Form(self.env['mrp.bom'])
+ bom_form_cheese_cake.product_tmpl_id = cheese_cake.product_tmpl_id
+ bom_form_cheese_cake.product_qty = 60
+ bom_form_cheese_cake.product_uom_id = self.uom_unit
+ bom_cheese_cake = bom_form_cheese_cake.save()
+
+ workcenter_2 = self.env['mrp.workcenter'].create({
+ 'name': 'cake mounting',
+ 'costs_hour': 20,
+ 'time_start': 10,
+ 'time_stop': 15
+ })
+
+ with Form(bom_cheese_cake) as bom:
+ with bom.bom_line_ids.new() as line:
+ line.product_id = cream
+ line.product_uom_id = uom_litre
+ line.product_qty = 3
+ with bom.bom_line_ids.new() as line:
+ line.product_id = crumble
+ line.product_uom_id = uom_kg
+ line.product_qty = 5.4
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter
+ operation.name = 'Mix cheese and crumble'
+ operation.time_cycle_manual = 10
+ with bom.operation_ids.new() as operation:
+ operation.workcenter_id = workcenter_2
+ operation.name = 'Cake mounting'
+ operation.time_cycle_manual = 5
+
+
+ # TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False)
+ self.assertEqual(report_values['lines']['operations_time'], 40.0, 'Operation time should be the same for 1 unit or for the batch')
+ # Operation cost is the sum of operation line.
+ operation_cost = float_round(10 / 60 * 10, precision_digits=2) + float_round(30 / 60 * 20, precision_digits=2)
+ self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0)
+
+ for component_line in report_values['lines']['components']:
+ # standard price * bom line quantity * current quantity / bom finished product quantity
+ if component_line['prod_id'] == cream.id:
+ # 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$
+ self.assertEqual(float_compare(component_line['total'], (3 * 5.17), precision_digits=2), 0)
+ if component_line['prod_id'] == crumble.id:
+ # 5.4 kg of crumble at the cost of a batch.
+ crumble_cost = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['total']
+ self.assertEqual(float_compare(component_line['total'], crumble_cost, precision_digits=2), 0)
+ # total price = 15.51 + crumble_cost + operation_cost(10 + 1.67 = 11.67) = 27.18 + crumble_cost
+ self.assertEqual(float_compare(report_values['lines']['total'], 27.18 + crumble_cost, precision_digits=2), 0, 'Product Bom Price is not correct')
+
+ def test_21_bom_report_variant(self):
+ """ Test a sub BoM process with multiple variants.
+ BOM 1:
+ product template = car
+ quantity = 5 units
+ - red paint 50l -> red car (product.product)
+ - blue paint 50l -> blue car
+ - red dashboard with gps -> red car with GPS
+ - red dashboard w/h gps -> red w/h GPS
+ - blue dashboard with gps -> blue car with GPS
+ - blue dashboard w/h gps -> blue w/h GPS
+
+ BOM 2:
+ product_tmpl = dashboard
+ quantity = 2
+ - red paint 1l -> red dashboard (product.product)
+ - blue paint 1l -> blue dashboard
+ - gps -> dashboard with gps
+
+ Check the Price for a Blue Car with GPS -> 910$:
+ 10l of blue paint -> 200$
+ 1 blue dashboard GPS -> 710$:
+ - 0.5l of blue paint -> 10$
+ - GPS -> 700$
+
+ Check the price for a red car -> 10.5l of red paint -> 210$
+ """
+ # Create a product template car with attributes gps(yes, no), color(red, blue)
+ self.car = self.env['product.template'].create({
+ 'name': 'Car',
+ })
+ self.gps_attribute = self.env['product.attribute'].create({'name': 'GPS', 'sequence': 1})
+ self.gps_yes = self.env['product.attribute.value'].create({
+ 'name': 'Yes',
+ 'attribute_id': self.gps_attribute.id,
+ 'sequence': 1,
+ })
+ self.gps_no = self.env['product.attribute.value'].create({
+ 'name': 'No',
+ 'attribute_id': self.gps_attribute.id,
+ 'sequence': 2,
+ })
+
+ self.car_gps_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.car.id,
+ 'attribute_id': self.gps_attribute.id,
+ 'value_ids': [(6, 0, [self.gps_yes.id, self.gps_no.id])],
+ })
+ self.car_gps_yes = self.car_gps_attribute_line.product_template_value_ids[0]
+ self.car_gps_no = self.car_gps_attribute_line.product_template_value_ids[1]
+
+ self.color_attribute = self.env['product.attribute'].create({'name': 'Color', 'sequence': 1})
+ self.color_red = self.env['product.attribute.value'].create({
+ 'name': 'Red',
+ 'attribute_id': self.color_attribute.id,
+ 'sequence': 1,
+ })
+ self.color_blue = self.env['product.attribute.value'].create({
+ 'name': 'Blue',
+ 'attribute_id': self.color_attribute.id,
+ 'sequence': 2,
+ })
+
+ self.car_color_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.car.id,
+ 'attribute_id': self.color_attribute.id,
+ 'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
+ })
+ self.car_color_red = self.car_color_attribute_line.product_template_value_ids[0]
+ self.car_color_blue = self.car_color_attribute_line.product_template_value_ids[1]
+
+ # Blue and red paint
+ uom_litre = self.env.ref('uom.product_uom_litre')
+ self.paint = self.env['product.template'].create({
+ 'name': 'Paint',
+ 'uom_id': uom_litre.id,
+ 'uom_po_id': uom_litre.id
+ })
+ self.paint_color_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.paint.id,
+ 'attribute_id': self.color_attribute.id,
+ 'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
+ })
+ self.paint_color_red = self.paint_color_attribute_line.product_template_value_ids[0]
+ self.paint_color_blue = self.paint_color_attribute_line.product_template_value_ids[1]
+
+ self.paint.product_variant_ids.write({'standard_price': 20})
+
+ self.dashboard = self.env['product.template'].create({
+ 'name': 'Dashboard',
+ 'standard_price': 1000,
+ })
+
+ self.dashboard_gps_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.dashboard.id,
+ 'attribute_id': self.gps_attribute.id,
+ 'value_ids': [(6, 0, [self.gps_yes.id, self.gps_no.id])],
+ })
+ self.dashboard_gps_yes = self.dashboard_gps_attribute_line.product_template_value_ids[0]
+ self.dashboard_gps_no = self.dashboard_gps_attribute_line.product_template_value_ids[1]
+
+ self.dashboard_color_attribute_line = self.env['product.template.attribute.line'].create({
+ 'product_tmpl_id': self.dashboard.id,
+ 'attribute_id': self.color_attribute.id,
+ 'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
+ })
+ self.dashboard_color_red = self.dashboard_color_attribute_line.product_template_value_ids[0]
+ self.dashboard_color_blue = self.dashboard_color_attribute_line.product_template_value_ids[1]
+
+ self.gps = self.env['product.product'].create({
+ 'name': 'GPS',
+ 'standard_price': 700,
+ })
+
+ bom_form_car = Form(self.env['mrp.bom'])
+ bom_form_car.product_tmpl_id = self.car
+ bom_form_car.product_qty = 5
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.paint._get_variant_for_combination(self.paint_color_red)
+ line.product_uom_id = uom_litre
+ line.product_qty = 50
+ line.bom_product_template_attribute_value_ids.add(self.car_color_red)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.paint._get_variant_for_combination(self.paint_color_blue)
+ line.product_uom_id = uom_litre
+ line.product_qty = 50
+ line.bom_product_template_attribute_value_ids.add(self.car_color_blue)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_yes + self.dashboard_color_red)
+ line.product_qty = 5
+ line.bom_product_template_attribute_value_ids.add(self.car_gps_yes)
+ line.bom_product_template_attribute_value_ids.add(self.car_color_red)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_yes + self.dashboard_color_blue)
+ line.product_qty = 5
+ line.bom_product_template_attribute_value_ids.add(self.car_gps_yes)
+ line.bom_product_template_attribute_value_ids.add(self.car_color_blue)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_no + self.dashboard_color_red)
+ line.product_qty = 5
+ line.bom_product_template_attribute_value_ids.add(self.car_gps_no)
+ line.bom_product_template_attribute_value_ids.add(self.car_color_red)
+ with bom_form_car.bom_line_ids.new() as line:
+ line.product_id = self.dashboard._get_variant_for_combination(self.dashboard_gps_no + self.dashboard_color_blue)
+ line.product_qty = 5
+ line.bom_product_template_attribute_value_ids.add(self.car_gps_no)
+ line.bom_product_template_attribute_value_ids.add(self.car_color_blue)
+ bom_car = bom_form_car.save()
+
+ bom_dashboard = Form(self.env['mrp.bom'])
+ bom_dashboard.product_tmpl_id = self.dashboard
+ bom_dashboard.product_qty = 2
+ with bom_dashboard.bom_line_ids.new() as line:
+ line.product_id = self.paint._get_variant_for_combination(self.paint_color_red)
+ line.product_uom_id = uom_litre
+ line.product_qty = 1
+ line.bom_product_template_attribute_value_ids.add(self.dashboard_color_red)
+ with bom_dashboard.bom_line_ids.new() as line:
+ line.product_id = self.paint._get_variant_for_combination(self.paint_color_blue)
+ line.product_uom_id = uom_litre
+ line.product_qty = 1
+ line.bom_product_template_attribute_value_ids.add(self.dashboard_color_blue)
+ with bom_dashboard.bom_line_ids.new() as line:
+ line.product_id = self.gps
+ line.product_qty = 2
+ line.bom_product_template_attribute_value_ids.add(self.dashboard_gps_yes)
+ bom_dashboard = bom_dashboard.save()
+
+ blue_car_with_gps = self.car._get_variant_for_combination(self.car_color_blue + self.car_gps_yes)
+
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=blue_car_with_gps.id)
+ # Two lines. blue dashboard with gps and blue paint.
+ self.assertEqual(len(report_values['lines']['components']), 2)
+
+ # 10l of blue paint
+ blue_paint = self.paint._get_variant_for_combination(self.paint_color_blue)
+ self.assertEqual(blue_paint.id, report_values['lines']['components'][0]['prod_id'])
+ self.assertEqual(report_values['lines']['components'][0]['prod_qty'], 10)
+ # 1 blue dashboard with GPS
+ blue_dashboard_gps = self.dashboard._get_variant_for_combination(self.dashboard_color_blue + self.dashboard_gps_yes)
+ self.assertEqual(blue_dashboard_gps.id, report_values['lines']['components'][1]['prod_id'])
+ self.assertEqual(report_values['lines']['components'][1]['prod_qty'], 1)
+ component = report_values['lines']['components'][1]
+ report_values_dashboad = self.env['report.mrp.report_bom_structure']._get_bom(
+ component['child_bom'], component['prod_id'], component['prod_qty'],
+ component['line_id'], component['level'] + 1)
+
+ self.assertEqual(len(report_values_dashboad['components']), 2)
+ self.assertEqual(blue_paint.id, report_values_dashboad['components'][0]['prod_id'])
+ self.assertEqual(self.gps.id, report_values_dashboad['components'][1]['prod_id'])
+
+ # 0.5l of paint at price of 20$/litre -> 10$
+ self.assertEqual(report_values_dashboad['components'][0]['total'], 10)
+ # GPS 700$
+ self.assertEqual(report_values_dashboad['components'][1]['total'], 700)
+
+ # Dashboard blue with GPS should have a BoM cost of 710$
+ self.assertEqual(report_values['lines']['components'][1]['total'], 710)
+ # 10l of paint at price of 20$/litre -> 200$
+ self.assertEqual(report_values['lines']['components'][0]['total'], 200)
+
+ # Total cost of blue car with GPS: 10 + 700 + 200 = 910
+ self.assertEqual(report_values['lines']['total'], 910)
+
+ red_car_without_gps = self.car._get_variant_for_combination(self.car_color_red + self.car_gps_no)
+
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=red_car_without_gps.id)
+ # Same math than before but without GPS
+ self.assertEqual(report_values['lines']['total'], 210)
+
+ def test_22_bom_report_recursive_bom(self):
+ """ Test report with recursive BoM and different quantities.
+ BoM 1:
+ product = Finished (units)
+ quantity = 100 units
+ - Semi-Finished 5 kg
+
+ BoM 2:
+ product = Semi-Finished (kg)
+ quantity = 11 kg
+ - Assembly 2 dozens
+
+ BoM 3:
+ product = Assembly (dozens)
+ quantity = 5 dozens
+ - Raw Material 4 litres (product.product 5$/litre)
+
+ Check the Price for 80 units of Finished -> 2.92$:
+ """
+ # Create a products templates
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ uom_kg = self.env.ref('uom.product_uom_kgm')
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ uom_litre = self.env.ref('uom.product_uom_litre')
+
+ finished = self.env['product.product'].create({
+ 'name': 'Finished',
+ 'type': 'product',
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ })
+
+ semi_finished = self.env['product.product'].create({
+ 'name': 'Semi-Finished',
+ 'type': 'product',
+ 'uom_id': uom_kg.id,
+ 'uom_po_id': uom_kg.id,
+ })
+
+ assembly = self.env['product.product'].create({
+ 'name': 'Assembly',
+ 'type': 'product',
+ 'uom_id': uom_dozen.id,
+ 'uom_po_id': uom_dozen.id,
+ })
+
+ raw_material = self.env['product.product'].create({
+ 'name': 'Raw Material',
+ 'type': 'product',
+ 'uom_id': uom_litre.id,
+ 'uom_po_id': uom_litre.id,
+ 'standard_price': 5,
+ })
+
+ #Create bom
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = finished.product_tmpl_id
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = semi_finished
+ line.product_uom_id = uom_kg
+ line.product_qty = 5
+ bom_finished = bom_finished.save()
+
+ bom_semi_finished = Form(self.env['mrp.bom'])
+ bom_semi_finished.product_tmpl_id = semi_finished.product_tmpl_id
+ bom_semi_finished.product_qty = 11
+ with bom_semi_finished.bom_line_ids.new() as line:
+ line.product_id = assembly
+ line.product_uom_id = uom_dozen
+ line.product_qty = 2
+ bom_semi_finished = bom_semi_finished.save()
+
+ bom_assembly = Form(self.env['mrp.bom'])
+ bom_assembly.product_tmpl_id = assembly.product_tmpl_id
+ bom_assembly.product_qty = 5
+ with bom_assembly.bom_line_ids.new() as line:
+ line.product_id = raw_material
+ line.product_uom_id = uom_litre
+ line.product_qty = 4
+ bom_assembly = bom_assembly.save()
+
+ report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_finished.id, searchQty=80)
+
+ self.assertAlmostEqual(report_values['lines']['total'], 2.92)
+
+ def test_validate_no_bom_line_with_same_product(self):
+ """
+ Cannot set a BOM line on a BOM with the same product as the BOM itself
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ finished = self.env['product.product'].create({
+ 'name': 'Finished',
+ 'type': 'product',
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ })
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = finished.product_tmpl_id
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = finished
+ line.product_uom_id = uom_unit
+ line.product_qty = 5
+ with self.assertRaises(exceptions.ValidationError), self.cr.savepoint():
+ bom_finished = bom_finished.save()
+
+ def test_validate_no_bom_line_with_same_product_variant(self):
+ """
+ Cannot set a BOM line on a BOM with the same product variant as the BOM itself
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = self.product_7_template
+ bom_finished.product_id = self.product_7_3
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = self.product_7_3
+ line.product_uom_id = uom_unit
+ line.product_qty = 5
+ with self.assertRaises(exceptions.ValidationError), self.cr.savepoint():
+ bom_finished = bom_finished.save()
+
+ def test_validate_bom_line_with_different_product_variant(self):
+ """
+ Can set a BOM line on a BOM with a different product variant as the BOM itself (same product)
+ Usecase for example A black T-shirt made from a white T-shirt and
+ black color.
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = self.product_7_template
+ bom_finished.product_id = self.product_7_3
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = self.product_7_2
+ line.product_uom_id = uom_unit
+ line.product_qty = 5
+ bom_finished = bom_finished.save()
+
+ def test_validate_bom_line_with_variant_of_bom_product(self):
+ """
+ Can set a BOM line on a BOM with a product variant when the BOM has no variant selected
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ bom_finished = Form(self.env['mrp.bom'])
+ bom_finished.product_tmpl_id = self.product_6.product_tmpl_id
+ # no product_id
+ bom_finished.product_qty = 100
+ with bom_finished.bom_line_ids.new() as line:
+ line.product_id = self.product_7_2
+ line.product_uom_id = uom_unit
+ line.product_qty = 5
+ bom_finished = bom_finished.save()
diff --git a/addons/mrp/tests/test_byproduct.py b/addons/mrp/tests/test_byproduct.py
new file mode 100644
index 00000000..0249e2cc
--- /dev/null
+++ b/addons/mrp/tests/test_byproduct.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import Form
+from odoo.tests import common
+
+
+class TestMrpByProduct(common.TransactionCase):
+
+ def setUp(self):
+ super(TestMrpByProduct, self).setUp()
+ self.MrpBom = self.env['mrp.bom']
+ self.warehouse = self.env.ref('stock.warehouse0')
+ route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
+ route_mto = self.warehouse.mto_pull_id.route_id.id
+ self.uom_unit_id = self.ref('uom.product_uom_unit')
+ def create_product(name, route_ids=[]):
+ return self.env['product.product'].create({
+ 'name': name,
+ 'type': 'product',
+ 'route_ids': route_ids})
+
+ # Create product A, B, C.
+ # --------------------------
+ self.product_a = create_product('Product A', route_ids=[(6, 0, [route_manufacture, route_mto])])
+ self.product_b = create_product('Product B', route_ids=[(6, 0, [route_manufacture, route_mto])])
+ self.product_c_id = create_product('Product C', route_ids=[]).id
+
+ def test_00_mrp_byproduct(self):
+ """ Test by product with production order."""
+ # Create BOM for product B
+ # ------------------------
+ bom_product_b = self.MrpBom.create({
+ 'product_tmpl_id': self.product_b.product_tmpl_id.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'product_uom_id': self.uom_unit_id,
+ 'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})]
+ })
+
+ # Create BOM for product A and set byproduct product B
+ bom_product_a = self.MrpBom.create({
+ 'product_tmpl_id': self.product_a.product_tmpl_id.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'product_uom_id': self.uom_unit_id,
+ 'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})],
+ 'byproduct_ids': [(0, 0, {'product_id': self.product_b.id, 'product_uom_id': self.uom_unit_id, 'product_qty': 1})]
+ })
+
+ # Create production order for product A
+ # -------------------------------------
+
+ mnf_product_a_form = Form(self.env['mrp.production'])
+ mnf_product_a_form.product_id = self.product_a
+ mnf_product_a_form.bom_id = bom_product_a
+ mnf_product_a_form.product_qty = 2.0
+ mnf_product_a = mnf_product_a_form.save()
+ mnf_product_a.action_confirm()
+
+ # I confirm the production order.
+ self.assertEqual(mnf_product_a.state, 'confirmed', 'Production order should be in state confirmed')
+
+ # Now I check the stock moves for the byproduct I created in the bill of material.
+ # This move is created automatically when I confirmed the production order.
+ moves = mnf_product_a.move_raw_ids | mnf_product_a.move_finished_ids
+ self.assertTrue(moves, 'No moves are created !')
+
+ # I consume and produce the production of products.
+ # I create record for selecting mode and quantity of products to produce.
+ mo_form = Form(mnf_product_a)
+ mo_form.qty_producing = 2.00
+ mnf_product_a = mo_form.save()
+ # I finish the production order.
+ self.assertEqual(len(mnf_product_a.move_raw_ids), 1, "Wrong consume move on production order.")
+ consume_move_c = mnf_product_a.move_raw_ids
+ by_product_move = mnf_product_a.move_finished_ids.filtered(lambda x: x.product_id.id == self.product_b.id)
+ # Check sub production produced quantity...
+ self.assertEqual(consume_move_c.product_uom_qty, 4, "Wrong consumed quantity of product c.")
+ self.assertEqual(by_product_move.product_uom_qty, 2, "Wrong produced quantity of sub product.")
+
+ mnf_product_a._post_inventory()
+
+ # I see that stock moves of External Hard Disk including Headset USB are done now.
+ self.assertFalse(any(move.state != 'done' for move in moves), 'Moves are not done!')
+
+ def test_change_product(self):
+ """ Create a production order for a specific product with a BoM. Then change the BoM and the finished product for
+ other ones and check the finished product of the first mo did not became a byproduct of the second one."""
+ # Create BOM for product A with product B as component
+ bom_product_a = self.MrpBom.create({
+ 'product_tmpl_id': self.product_a.product_tmpl_id.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'product_uom_id': self.uom_unit_id,
+ 'bom_line_ids': [(0, 0, {'product_id': self.product_b.id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})],
+ })
+
+ bom_product_a_2 = self.MrpBom.create({
+ 'product_tmpl_id': self.product_b.product_tmpl_id.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'product_uom_id': self.uom_unit_id,
+ 'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})],
+ })
+ # Create production order for product A
+ # -------------------------------------
+
+ mnf_product_a_form = Form(self.env['mrp.production'])
+ mnf_product_a_form.product_id = self.product_a
+ mnf_product_a_form.bom_id = bom_product_a
+ mnf_product_a_form.product_qty = 1.0
+ mnf_product_a = mnf_product_a_form.save()
+ mnf_product_a_form = Form(mnf_product_a)
+ mnf_product_a_form.bom_id = bom_product_a_2
+ mnf_product_a = mnf_product_a_form.save()
+ self.assertEqual(mnf_product_a.move_raw_ids.product_id.id, self.product_c_id)
+ self.assertFalse(mnf_product_a.move_byproduct_ids)
diff --git a/addons/mrp/tests/test_cancel_mo.py b/addons/mrp/tests/test_cancel_mo.py
new file mode 100644
index 00000000..d305571c
--- /dev/null
+++ b/addons/mrp/tests/test_cancel_mo.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import Form
+from datetime import datetime, timedelta
+
+from odoo.fields import Datetime as Dt
+from odoo.exceptions import UserError
+from odoo.addons.mrp.tests.common import TestMrpCommon
+
+
+class TestMrpCancelMO(TestMrpCommon):
+
+ def test_cancel_mo_without_routing_1(self):
+ """ Cancel a Manufacturing Order with no routing, no production.
+ """
+ # Create MO
+ manufacturing_order = self.generate_mo()[0]
+ # Do nothing, cancel it
+ manufacturing_order.action_cancel()
+ # Check the MO and its moves are cancelled
+ self.assertEqual(manufacturing_order.state, 'cancel', "MO should be in cancel state.")
+ self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'cancel',
+ "Cancelled MO raw moves must be cancelled as well.")
+ self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'cancel',
+ "Cancelled MO raw moves must be cancelled as well.")
+ self.assertEqual(manufacturing_order.move_finished_ids.state, 'cancel',
+ "Cancelled MO finished move must be cancelled as well.")
+
+ def test_cancel_mo_without_routing_2(self):
+ """ Cancel a Manufacturing Order with no routing but some productions.
+ """
+ # Create MO
+ manufacturing_order = self.generate_mo()[0]
+ # Produce some quantity
+ mo_form = Form(manufacturing_order)
+ mo_form.qty_producing = 2
+ manufacturing_order = mo_form.save()
+ # Cancel it
+ manufacturing_order.action_cancel()
+ # Check it's cancelled
+ self.assertEqual(manufacturing_order.state, 'cancel', "MO should be in cancel state.")
+ self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'cancel',
+ "Cancelled MO raw moves must be cancelled as well.")
+ self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'cancel',
+ "Cancelled MO raw moves must be cancelled as well.")
+ self.assertEqual(manufacturing_order.move_finished_ids.state, 'cancel',
+ "Cancelled MO finished move must be cancelled as well.")
+
+ def test_cancel_mo_without_routing_3(self):
+ """ Cancel a Manufacturing Order with no routing but some productions
+ after post inventory.
+ """
+ # Create MO
+ manufacturing_order = self.generate_mo(consumption='strict')[0]
+ # Produce some quantity (not all to avoid to done the MO when post inventory)
+ mo_form = Form(manufacturing_order)
+ mo_form.qty_producing = 2
+ manufacturing_order = mo_form.save()
+ # Post Inventory
+ manufacturing_order._post_inventory()
+ # Cancel the MO
+ manufacturing_order.action_cancel()
+ # Check MO is marked as done and its SML are done or cancelled
+ self.assertEqual(manufacturing_order.state, 'done', "MO should be in done state.")
+ self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'done',
+ "Due to 'post_inventory', some move raw must stay in done state")
+ self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'done',
+ "Due to 'post_inventory', some move raw must stay in done state")
+ self.assertEqual(manufacturing_order.move_raw_ids[2].state, 'cancel',
+ "The other move raw are cancelled like their MO.")
+ self.assertEqual(manufacturing_order.move_raw_ids[3].state, 'cancel',
+ "The other move raw are cancelled like their MO.")
+ self.assertEqual(manufacturing_order.move_finished_ids[0].state, 'done',
+ "Due to 'post_inventory', a move finished must stay in done state")
+ self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel',
+ "The other move finished is cancelled like its MO.")
+
+ def test_unlink_mo(self):
+ """ Try to unlink a Manufacturing Order, and check it's possible or not
+ depending of the MO state (must be in cancel state to be unlinked, but
+ the unlink method will try to cancel MO before unlink them).
+ """
+ # Case #1: Create MO, do nothing and try to unlink it (can be deleted)
+ manufacturing_order = self.generate_mo()[0]
+ self.assertEqual(manufacturing_order.exists().state, 'confirmed')
+ manufacturing_order.unlink()
+ # Check the MO is deleted.
+ self.assertEqual(manufacturing_order.exists().state, False)
+
+ # Case #2: Create MO, make and post some production, then try to unlink
+ # it (cannot be deleted)
+ manufacturing_order = self.generate_mo()[0]
+ # Produce some quantity (not all to avoid to done the MO when post inventory)
+ mo_form = Form(manufacturing_order)
+ mo_form.qty_producing = 2
+ manufacturing_order = mo_form.save()
+ # Post Inventory
+ manufacturing_order._post_inventory()
+ # Unlink the MO must raises an UserError since it cannot be really cancelled
+ self.assertEqual(manufacturing_order.exists().state, 'progress')
+ with self.assertRaises(UserError):
+ manufacturing_order.unlink()
diff --git a/addons/mrp/tests/test_multicompany.py b/addons/mrp/tests/test_multicompany.py
new file mode 100644
index 00000000..6b0c3fd3
--- /dev/null
+++ b/addons/mrp/tests/test_multicompany.py
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import common, Form
+from odoo.exceptions import UserError
+
+
+class TestMrpMulticompany(common.TransactionCase):
+
+ def setUp(self):
+ super(TestMrpMulticompany, self).setUp()
+
+ group_user = self.env.ref('base.group_user')
+ group_mrp_manager = self.env.ref('mrp.group_mrp_manager')
+ self.company_a = self.env['res.company'].create({'name': 'Company A'})
+ self.company_b = self.env['res.company'].create({'name': 'Company B'})
+ self.warehouse_a = self.env['stock.warehouse'].search([('company_id', '=', self.company_a.id)], limit=1)
+ self.warehouse_b = self.env['stock.warehouse'].search([('company_id', '=', self.company_b.id)], limit=1)
+ self.stock_location_a = self.warehouse_a.lot_stock_id
+ self.stock_location_b = self.warehouse_b.lot_stock_id
+
+ self.user_a = self.env['res.users'].create({
+ 'name': 'user company a with access to company b',
+ 'login': 'user a',
+ 'groups_id': [(6, 0, [group_user.id, group_mrp_manager.id])],
+ 'company_id': self.company_a.id,
+ 'company_ids': [(6, 0, [self.company_a.id, self.company_b.id])]
+ })
+ self.user_b = self.env['res.users'].create({
+ 'name': 'user company a with access to company b',
+ 'login': 'user b',
+ 'groups_id': [(6, 0, [group_user.id, group_mrp_manager.id])],
+ 'company_id': self.company_b.id,
+ 'company_ids': [(6, 0, [self.company_a.id, self.company_b.id])]
+ })
+
+ def test_bom_1(self):
+ """Check it is not possible to use a product of Company B in a
+ bom of Company A. """
+
+ product_b = self.env['product.product'].create({
+ 'name': 'p1',
+ 'company_id': self.company_b.id,
+ })
+ with self.assertRaises(UserError):
+ self.env['mrp.bom'].create({
+ 'product_id': product_b.id,
+ 'product_tmpl_id': product_b.product_tmpl_id.id,
+ 'company_id': self.company_a.id,
+ })
+
+ def test_bom_2(self):
+ """Check it is not possible to use a product of Company B as a component
+ in a bom of Company A. """
+
+ product_a = self.env['product.product'].create({
+ 'name': 'p1',
+ 'company_id': self.company_a.id,
+ })
+ product_b = self.env['product.product'].create({
+ 'name': 'p2',
+ 'company_id': self.company_b.id,
+ })
+ with self.assertRaises(UserError):
+ self.env['mrp.bom'].create({
+ 'product_id': product_a.id,
+ 'product_tmpl_id': product_b.product_tmpl_id.id,
+ 'company_id': self.company_a.id,
+ 'bom_line_ids': [(0, 0, {'product_id': product_b.id})]
+ })
+
+ def test_production_1(self):
+ """Check it is not possible to confirm a production of Company B with
+ product of Company A. """
+
+ product_a = self.env['product.product'].create({
+ 'name': 'p1',
+ 'company_id': self.company_a.id,
+ })
+ mo = self.env['mrp.production'].create({
+ 'product_id': product_a.id,
+ 'product_uom_id': product_a.uom_id.id,
+ 'company_id': self.company_b.id,
+ })
+ with self.assertRaises(UserError):
+ mo.action_confirm()
+
+ def test_production_2(self):
+ """Check that confirming a production in company b with user_a will create
+ stock moves on company b. """
+
+ product_a = self.env['product.product'].create({
+ 'name': 'p1',
+ 'company_id': self.company_a.id,
+ })
+ component_a = self.env['product.product'].create({
+ 'name': 'p2',
+ 'company_id': self.company_a.id,
+ })
+ self.env['mrp.bom'].create({
+ 'product_id': product_a.id,
+ 'product_tmpl_id': product_a.product_tmpl_id.id,
+ 'company_id': self.company_a.id,
+ 'bom_line_ids': [(0, 0, {'product_id': component_a.id})]
+ })
+ mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
+ mo_form.product_id = product_a
+ mo = mo_form.save()
+ mo.with_user(self.user_b).action_confirm()
+ self.assertEqual(mo.move_raw_ids.company_id, self.company_a)
+ self.assertEqual(mo.move_finished_ids.company_id, self.company_a)
+
+ def test_product_produce_1(self):
+ """Check that using a finished lot of company b in the produce wizard of a production
+ of company a is not allowed """
+
+ product = self.env['product.product'].create({
+ 'name': 'p1',
+ 'tracking': 'lot',
+ })
+ component = self.env['product.product'].create({
+ 'name': 'p2',
+ })
+ lot_b = self.env['stock.production.lot'].create({
+ 'product_id': product.id,
+ 'company_id': self.company_b.id,
+ })
+ self.env['mrp.bom'].create({
+ 'product_id': product.id,
+ 'product_tmpl_id': product.product_tmpl_id.id,
+ 'company_id': self.company_a.id,
+ 'bom_line_ids': [(0, 0, {'product_id': component.id})]
+ })
+ mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
+ mo_form.product_id = product
+ mo_form.lot_producing_id = lot_b
+ mo = mo_form.save()
+ with self.assertRaises(UserError):
+ mo.with_user(self.user_b).action_confirm()
+
+ def test_product_produce_2(self):
+ """Check that using a component lot of company b in the produce wizard of a production
+ of company a is not allowed """
+
+ product = self.env['product.product'].create({
+ 'name': 'p1',
+ })
+ component = self.env['product.product'].create({
+ 'name': 'p2',
+ 'tracking': 'lot',
+ })
+ lot_b = self.env['stock.production.lot'].create({
+ 'product_id': component.id,
+ 'company_id': self.company_b.id,
+ })
+ self.env['mrp.bom'].create({
+ 'product_id': product.id,
+ 'product_tmpl_id': product.product_tmpl_id.id,
+ 'company_id': self.company_a.id,
+ 'bom_line_ids': [(0, 0, {'product_id': component.id})]
+ })
+ mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
+ mo_form.product_id = product
+ mo = mo_form.save()
+ mo.with_user(self.user_b).action_confirm()
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.lot_id = lot_b
+ ml.qty_done = 1
+ details_operation_form.save()
+ with self.assertRaises(UserError):
+ mo.button_mark_done()
+
+
+ def test_partner_1(self):
+ """ On a product without company, as a user of Company B, check it is not possible to use a
+ location limited to Company A as `property_stock_production` """
+
+ shared_product = self.env['product.product'].create({
+ 'name': 'Shared Product',
+ 'company_id': False,
+ })
+ with self.assertRaises(UserError):
+ shared_product.with_user(self.user_b).property_stock_production = self.stock_location_a
diff --git a/addons/mrp/tests/test_oee.py b/addons/mrp/tests/test_oee.py
new file mode 100644
index 00000000..4cf8a31a
--- /dev/null
+++ b/addons/mrp/tests/test_oee.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime, timedelta, time
+from pytz import timezone, utc
+
+from odoo import fields
+from odoo.addons.mrp.tests.common import TestMrpCommon
+
+
+class TestOee(TestMrpCommon):
+ def create_productivity_line(self, loss_reason, date_start=False, date_end=False):
+ return self.env['mrp.workcenter.productivity'].create({
+ 'workcenter_id': self.workcenter_1.id,
+ 'date_start': date_start,
+ 'date_end': date_end,
+ 'loss_id': loss_reason.id,
+ 'description': loss_reason.name
+ })
+
+ def test_wrokcenter_oee(self):
+ """ Test case workcenter oee. """
+ day = datetime.date(datetime.today())
+ # Make the test work the weekend. It will fails due to workcenter working hours.
+ if day.weekday() in (5, 6):
+ day -= timedelta(days=2)
+
+ tz = timezone(self.workcenter_1.resource_calendar_id.tz)
+
+ def time_to_string_utc_datetime(time):
+ return fields.Datetime.to_string(
+ tz.localize(datetime.combine(day, time)).astimezone(utc)
+ )
+
+ start_time = time_to_string_utc_datetime(time(10, 43, 22))
+ end_time = time_to_string_utc_datetime(time(10, 56, 22))
+ # Productive time duration (13 min)
+ self.create_productivity_line(self.env.ref('mrp.block_reason7'), start_time, end_time)
+
+ # Material Availability time duration (1.52 min)
+ # Check working state is blocked or not.
+ start_time = time_to_string_utc_datetime(time(10, 47, 8))
+ workcenter_productivity_1 = self.create_productivity_line(self.env.ref('mrp.block_reason0'), start_time)
+ self.assertEqual(self.workcenter_1.working_state, 'blocked', "Wrong working state of workcenter.")
+
+ # Check working state is normal or not.
+ end_time = time_to_string_utc_datetime(time(10, 48, 39))
+ workcenter_productivity_1.write({'date_end': end_time})
+ self.assertEqual(self.workcenter_1.working_state, 'normal', "Wrong working state of workcenter.")
+
+ # Process Defect time duration (1.33 min)
+ start_time = time_to_string_utc_datetime(time(10, 48, 38))
+ end_time = time_to_string_utc_datetime(time(10, 49, 58))
+ self.create_productivity_line(self.env.ref('mrp.block_reason5'), start_time, end_time)
+ # Reduced Speed time duration (3.0 min)
+ start_time = time_to_string_utc_datetime(time(10, 50, 22))
+ end_time = time_to_string_utc_datetime(time(10, 53, 22))
+ self.create_productivity_line(self.env.ref('mrp.block_reason4'), start_time, end_time)
+
+ # Block time : ( Process Defact (1.33 min) + Reduced Speed (3.0 min) + Material Availability (1.52 min)) = 5.85 min
+ blocked_time_in_hour = round(((1.33 + 3.0 + 1.52) / 60.0), 2)
+ # Productive time : Productive time duration (13 min)
+ productive_time_in_hour = round((13.0 / 60.0), 2)
+
+ # Check blocked time and productive time
+ self.assertEqual(self.workcenter_1.blocked_time, blocked_time_in_hour, "Wrong block time on workcenter.")
+ self.assertEqual(self.workcenter_1.productive_time, productive_time_in_hour, "Wrong productive time on workcenter.")
+
+ # Check overall equipment effectiveness
+ computed_oee = round(((productive_time_in_hour * 100.0)/(productive_time_in_hour + blocked_time_in_hour)), 2)
+ self.assertEqual(self.workcenter_1.oee, computed_oee, "Wrong oee on workcenter.")
diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py
new file mode 100644
index 00000000..f0bae429
--- /dev/null
+++ b/addons/mrp/tests/test_order.py
@@ -0,0 +1,2055 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import Form
+from datetime import datetime, timedelta
+
+from odoo.fields import Datetime as Dt
+from odoo.exceptions import UserError
+from odoo.addons.mrp.tests.common import TestMrpCommon
+
+class TestMrpOrder(TestMrpCommon):
+
+ def test_access_rights_manager(self):
+ """ Checks an MRP manager can create, confirm and cancel a manufacturing order. """
+ man_order_form = Form(self.env['mrp.production'].with_user(self.user_mrp_manager))
+ man_order_form.product_id = self.product_4
+ man_order_form.product_qty = 5.0
+ man_order_form.bom_id = self.bom_1
+ man_order_form.location_src_id = self.location_1
+ man_order_form.location_dest_id = self.warehouse_1.wh_output_stock_loc_id
+ man_order = man_order_form.save()
+ man_order.action_confirm()
+ man_order.action_cancel()
+ self.assertEqual(man_order.state, 'cancel', "Production order should be in cancel state.")
+ man_order.unlink()
+
+ def test_access_rights_user(self):
+ """ Checks an MRP user can create, confirm and cancel a manufacturing order. """
+ man_order_form = Form(self.env['mrp.production'].with_user(self.user_mrp_user))
+ man_order_form.product_id = self.product_4
+ man_order_form.product_qty = 5.0
+ man_order_form.bom_id = self.bom_1
+ man_order_form.location_src_id = self.location_1
+ man_order_form.location_dest_id = self.warehouse_1.wh_output_stock_loc_id
+ man_order = man_order_form.save()
+ man_order.action_confirm()
+ man_order.action_cancel()
+ self.assertEqual(man_order.state, 'cancel', "Production order should be in cancel state.")
+ man_order.unlink()
+
+ def test_basic(self):
+ """ Checks a basic manufacturing order: no routing (thus no workorders), no lot and
+ consume strictly what's needed. """
+ self.product_1.type = 'product'
+ self.product_2.type = 'product'
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'Initial inventory',
+ 'line_ids': [(0, 0, {
+ 'product_id': self.product_1.id,
+ 'product_uom_id': self.product_1.uom_id.id,
+ 'product_qty': 500,
+ 'location_id': self.warehouse_1.lot_stock_id.id
+ }), (0, 0, {
+ 'product_id': self.product_2.id,
+ 'product_uom_id': self.product_2.uom_id.id,
+ 'product_qty': 500,
+ 'location_id': self.warehouse_1.lot_stock_id.id
+ })]
+ })
+ inventory.action_start()
+ inventory.action_validate()
+
+ test_date_planned = Dt.now() - timedelta(days=1)
+ test_quantity = 3.0
+ man_order_form = Form(self.env['mrp.production'].with_user(self.user_mrp_user))
+ man_order_form.product_id = self.product_4
+ man_order_form.bom_id = self.bom_1
+ man_order_form.product_uom_id = self.product_4.uom_id
+ man_order_form.product_qty = test_quantity
+ man_order_form.date_planned_start = test_date_planned
+ man_order_form.location_src_id = self.location_1
+ man_order_form.location_dest_id = self.warehouse_1.wh_output_stock_loc_id
+ man_order = man_order_form.save()
+
+ self.assertEqual(man_order.state, 'draft', "Production order should be in draft state.")
+ man_order.action_confirm()
+ self.assertEqual(man_order.state, 'confirmed', "Production order should be in confirmed state.")
+
+ # check production move
+ production_move = man_order.move_finished_ids
+ self.assertAlmostEqual(production_move.date, test_date_planned + timedelta(hours=1), delta=timedelta(seconds=10))
+ self.assertEqual(production_move.product_id, self.product_4)
+ self.assertEqual(production_move.product_uom, man_order.product_uom_id)
+ self.assertEqual(production_move.product_qty, man_order.product_qty)
+ self.assertEqual(production_move.location_id, self.product_4.property_stock_production)
+ self.assertEqual(production_move.location_dest_id, man_order.location_dest_id)
+
+ # check consumption moves
+ for move in man_order.move_raw_ids:
+ self.assertEqual(move.date, test_date_planned)
+ first_move = man_order.move_raw_ids.filtered(lambda move: move.product_id == self.product_2)
+ self.assertEqual(first_move.product_qty, test_quantity / self.bom_1.product_qty * self.product_4.uom_id.factor_inv * 2)
+ first_move = man_order.move_raw_ids.filtered(lambda move: move.product_id == self.product_1)
+ self.assertEqual(first_move.product_qty, test_quantity / self.bom_1.product_qty * self.product_4.uom_id.factor_inv * 4)
+
+ # produce product
+ mo_form = Form(man_order)
+ mo_form.qty_producing = 2.0
+ man_order = mo_form.save()
+
+ action = man_order.button_mark_done()
+ self.assertEqual(man_order.state, 'progress', "Production order should be open a backorder wizard, then not done yet.")
+
+ quantity_issues = man_order._get_consumption_issues()
+ action = man_order._action_generate_consumption_wizard(quantity_issues)
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_close_mo()
+ self.assertEqual(man_order.state, 'done', "Production order should be done.")
+
+ # check that copy handles moves correctly
+ mo_copy = man_order.copy()
+ self.assertEqual(mo_copy.state, 'draft', "Copied production order should be draft.")
+ self.assertEqual(len(mo_copy.move_raw_ids), 2, "Incorrect number of component moves [i.e. no 0 qty moves should be copied].")
+ self.assertEqual(len(mo_copy.move_finished_ids), 1, "Incorrect number of moves for products to produce [i.e. cancelled moves should not be copied")
+ self.assertEqual(mo_copy.move_finished_ids.product_uom_qty, 2, "Incorrect qty of products to produce")
+
+ def test_production_avialability(self):
+ """ Checks the availability of a production order through mutliple calls to `action_assign`.
+ """
+ self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink()
+ self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_4).unlink()
+ self.bom_3.ready_to_produce = 'all_available'
+
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.product_6
+ production_form.bom_id = self.bom_3
+ production_form.product_qty = 5.0
+ production_form.product_uom_id = self.product_6.uom_id
+ production_2 = production_form.save()
+
+ production_2.action_confirm()
+ production_2.action_assign()
+
+ # check sub product availability state is waiting
+ self.assertEqual(production_2.reservation_state, 'confirmed', 'Production order should be availability for waiting state')
+
+ # Update Inventory
+ self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': self.product_2.id,
+ 'inventory_quantity': 2.0,
+ 'location_id': self.stock_location_14.id
+ })
+
+ production_2.action_assign()
+ # check sub product availability state is partially available
+ self.assertEqual(production_2.reservation_state, 'confirmed', 'Production order should be availability for partially available state')
+
+ # Update Inventory
+ self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': self.product_2.id,
+ 'inventory_quantity': 5.0,
+ 'location_id': self.stock_location_14.id
+ })
+
+ production_2.action_assign()
+ # check sub product availability state is assigned
+ self.assertEqual(production_2.reservation_state, 'assigned', 'Production order should be availability for assigned state')
+
+ def test_split_move_line(self):
+ """ Consume more component quantity than the initial demand.
+ It should create extra move and share the quantity between the two stock
+ moves """
+ mo, bom, p_final, p1, p2 = self.generate_mo(qty_base_1=10, qty_final=1, qty_base_2=1)
+ mo.action_assign()
+ # check is_quantity_done_editable
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 2
+ details_operation_form.save()
+ details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 11
+ details_operation_form.save()
+
+ self.assertEqual(len(mo.move_raw_ids), 2)
+ self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 2)
+ self.assertEqual(mo.move_raw_ids[0].move_line_ids.mapped('qty_done'), [2])
+ self.assertEqual(mo.move_raw_ids[1].move_line_ids.mapped('qty_done'), [11])
+ self.assertEqual(mo.move_raw_ids[0].quantity_done, 2)
+ self.assertEqual(mo.move_raw_ids[1].quantity_done, 11)
+ mo.button_mark_done()
+ self.assertEqual(len(mo.move_raw_ids), 4)
+ self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4)
+ self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1, 10, 1, 1])
+ self.assertEqual(mo.move_raw_ids.mapped('move_line_ids.qty_done'), [1, 10, 1, 1])
+
+ def test_update_quantity_1(self):
+ """ Build 5 final products with different consumed lots,
+ then edit the finished quantity and update the Manufacturing
+ order quantity. Then check if the produced quantity do not
+ change and it is possible to close the MO.
+ """
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot')
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ lot_1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ })
+ lot_2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 10, lot_id=lot_1)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 10, lot_id=lot_2)
+
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+ mo.action_assign()
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ mo = mo_form.save()
+
+ details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = lot_1
+ ml.qty_done = 20
+ details_operation_form.save()
+ update_quantity_wizard = self.env['change.production.qty'].create({
+ 'mo_id': mo.id,
+ 'product_qty': 4,
+ })
+ update_quantity_wizard.change_prod_qty()
+
+ self.assertEqual(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).quantity_done, 20, 'Update the produce quantity should not impact already produced quantity.')
+ self.assertEqual(mo.move_finished_ids.product_uom_qty, 4)
+ mo.button_mark_done()
+
+ def test_update_quantity_2(self):
+ """ Build 5 final products with different consumed lots,
+ then edit the finished quantity and update the Manufacturing
+ order quantity. Then check if the produced quantity do not
+ change and it is possible to close the MO.
+ """
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=3)
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 20)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+ mo.action_assign()
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 2
+ mo = mo_form.save()
+
+ mo._post_inventory()
+
+ update_quantity_wizard = self.env['change.production.qty'].create({
+ 'mo_id': mo.id,
+ 'product_qty': 5,
+ })
+ update_quantity_wizard.change_prod_qty()
+ mo_form = Form(mo)
+ mo_form.qty_producing = 5
+ mo = mo_form.save()
+ mo.button_mark_done()
+
+ self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).mapped('quantity_done')), 20)
+ self.assertEqual(sum(mo.move_finished_ids.mapped('quantity_done')), 5)
+
+ def test_update_quantity_3(self):
+ bom = self.env['mrp.bom'].create({
+ 'product_id': self.product_6.id,
+ 'product_tmpl_id': self.product_6.product_tmpl_id.id,
+ 'product_qty': 1,
+ 'product_uom_id': self.product_6.uom_id.id,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': self.product_2.id, 'product_qty': 2.03}),
+ (0, 0, {'product_id': self.product_8.id, 'product_qty': 4.16})
+ ],
+ 'operation_ids': [
+ (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
+ ]
+ })
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.product_6
+ production_form.bom_id = bom
+ production_form.product_qty = 1
+ production_form.product_uom_id = self.product_6.uom_id
+ production = production_form.save()
+ self.assertEqual(production.workorder_ids.duration_expected, 90)
+ mo_form = Form(production)
+ mo_form.product_qty = 3
+ production = mo_form.save()
+ self.assertEqual(production.workorder_ids.duration_expected, 165)
+
+ def test_update_quantity_4(self):
+ bom = self.env['mrp.bom'].create({
+ 'product_id': self.product_6.id,
+ 'product_tmpl_id': self.product_6.product_tmpl_id.id,
+ 'product_qty': 1,
+ 'product_uom_id': self.product_6.uom_id.id,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': self.product_2.id, 'product_qty': 2.03}),
+ (0, 0, {'product_id': self.product_8.id, 'product_qty': 4.16})
+ ],
+ })
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.product_6
+ production_form.bom_id = bom
+ production_form.product_qty = 1
+ production_form.product_uom_id = self.product_6.uom_id
+ production = production_form.save()
+ production_form = Form(production)
+ with production_form.workorder_ids.new() as wo:
+ wo.name = 'OP1'
+ wo.workcenter_id = self.workcenter_1
+ wo.duration_expected = 40
+ production = production_form.save()
+ self.assertEqual(production.workorder_ids.duration_expected, 40)
+ mo_form = Form(production)
+ mo_form.product_qty = 3
+ production = mo_form.save()
+ self.assertEqual(production.workorder_ids.duration_expected, 90)
+
+ def test_qty_producing(self):
+ """Qty producing should be the qty remain to produce, instead of 0"""
+ bom = self.env['mrp.bom'].create({
+ 'product_id': self.product_6.id,
+ 'product_tmpl_id': self.product_6.product_tmpl_id.id,
+ 'product_qty': 1,
+ 'product_uom_id': self.product_6.uom_id.id,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': self.product_2.id, 'product_qty': 2.00}),
+ ],
+ })
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.product_6
+ production_form.bom_id = bom
+ production_form.product_qty = 5
+ production_form.product_uom_id = self.product_6.uom_id
+ production = production_form.save()
+ production_form = Form(production)
+ with production_form.workorder_ids.new() as wo:
+ wo.name = 'OP1'
+ wo.workcenter_id = self.workcenter_1
+ wo.duration_expected = 40
+ production = production_form.save()
+ production.action_confirm()
+ production.button_plan()
+ production.workorder_ids[0].button_start()
+ self.assertEqual(production.workorder_ids.qty_producing, 5, "Wrong quantity is suggested to produce.")
+
+ def test_update_quantity_5(self):
+ bom = self.env['mrp.bom'].create({
+ 'product_id': self.product_6.id,
+ 'product_tmpl_id': self.product_6.product_tmpl_id.id,
+ 'product_qty': 1,
+ 'product_uom_id': self.product_6.uom_id.id,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': self.product_2.id, 'product_qty': 3}),
+ ],
+ })
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.product_6
+ production_form.bom_id = bom
+ production_form.product_qty = 1
+ production_form.product_uom_id = self.product_6.uom_id
+ production = production_form.save()
+ production.action_confirm()
+ production.action_assign()
+ production_form = Form(production)
+ # change the quantity producing and the initial demand
+ # in the same transaction
+ production_form.qty_producing = 10
+ with production_form.move_raw_ids.edit(0) as move:
+ move.product_uom_qty = 2
+ production = production_form.save()
+ production.button_mark_done()
+
+ def test_update_plan_date(self):
+ """Editing the scheduled date after planning the MO should unplan the MO, and adjust the date on the stock moves"""
+ planned_date = datetime(2023, 5, 15, 9, 0)
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = self.product_4
+ mo_form.bom_id = self.bom_1
+ mo_form.product_qty = 1
+ mo_form.date_planned_start = planned_date
+ mo = mo_form.save()
+ self.assertEqual(mo.move_finished_ids[0].date, datetime(2023, 5, 15, 10, 0))
+ mo.action_confirm()
+ mo.button_plan()
+ with Form(mo) as frm:
+ frm.date_planned_start = datetime(2024, 5, 15, 9, 0)
+ self.assertEqual(mo.move_finished_ids[0].date, datetime(2024, 5, 15, 10, 0))
+
+ def test_rounding(self):
+ """ Checks we round up when bringing goods to produce and round half-up when producing.
+ This implementation allows to implement an efficiency notion (see rev 347f140fe63612ee05e).
+ """
+ self.product_6.uom_id.rounding = 1.0
+ bom_eff = self.env['mrp.bom'].create({
+ 'product_id': self.product_6.id,
+ 'product_tmpl_id': self.product_6.product_tmpl_id.id,
+ 'product_qty': 1,
+ 'product_uom_id': self.product_6.uom_id.id,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': self.product_2.id, 'product_qty': 2.03}),
+ (0, 0, {'product_id': self.product_8.id, 'product_qty': 4.16})
+ ]
+ })
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.product_6
+ production_form.bom_id = bom_eff
+ production_form.product_qty = 20
+ production_form.product_uom_id = self.product_6.uom_id
+ production = production_form.save()
+ production.action_confirm()
+ #Check the production order has the right quantities
+ self.assertEqual(production.move_raw_ids[0].product_qty, 41, 'The quantity should be rounded up')
+ self.assertEqual(production.move_raw_ids[1].product_qty, 84, 'The quantity should be rounded up')
+
+ # produce product
+ mo_form = Form(production)
+ mo_form.qty_producing = 8
+ production = mo_form.save()
+ self.assertEqual(production.move_raw_ids[0].quantity_done, 16, 'Should use half-up rounding when producing')
+ self.assertEqual(production.move_raw_ids[1].quantity_done, 34, 'Should use half-up rounding when producing')
+
+ def test_product_produce_1(self):
+ """ Checks the production wizard contains lines even for untracked products. """
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo()
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+
+ mo.action_assign()
+
+ # change the quantity done in one line
+ details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 1
+ details_operation_form.save()
+
+ # change the quantity producing
+ mo_form = Form(mo)
+ mo_form.qty_producing = 3
+
+ # check than all quantities are update correctly
+ self.assertEqual(mo_form.move_raw_ids._records[0]['product_uom_qty'], 5, "Wrong quantity to consume")
+ self.assertEqual(mo_form.move_raw_ids._records[0]['quantity_done'], 3, "Wrong quantity done")
+ self.assertEqual(mo_form.move_raw_ids._records[1]['product_uom_qty'], 20, "Wrong quantity to consume")
+ self.assertEqual(mo_form.move_raw_ids._records[1]['quantity_done'], 12, "Wrong quantity done")
+
+ def test_product_produce_2(self):
+ """ Checks that, for a BOM where one of the components is tracked by serial number and the
+ other is not tracked, when creating a manufacturing order for two finished products and
+ reserving, the produce wizards proposes the corrects lines when producing one at a time.
+ """
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='serial', qty_base_1=1, qty_final=2)
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ lot_p1_1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ })
+ lot_p1_2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1, lot_id=lot_p1_1)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1, lot_id=lot_p1_2)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+
+ mo.action_assign()
+
+ self.assertEqual(len(mo.move_raw_ids.move_line_ids), 3, 'You should have 3 stock move lines. One for each serial to consume and for the untracked product.')
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ mo = mo_form.save()
+
+ # get the proposed lot
+ details_operation_form = Form(mo.move_raw_ids.filtered(lambda move: move.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
+ self.assertEqual(len(details_operation_form.move_line_ids), 2)
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ consumed_lots = ml.lot_id
+ ml.qty_done = 1
+ details_operation_form.save()
+
+ remaining_lot = (lot_p1_1 | lot_p1_2) - consumed_lots
+ remaining_lot.ensure_one()
+ action = mo.button_mark_done()
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_backorder()
+
+ # Check MO backorder
+ mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
+
+ mo_form = Form(mo_backorder)
+ mo_form.qty_producing = 1
+ mo_backorder = mo_form.save()
+ details_operation_form = Form(mo_backorder.move_raw_ids.filtered(lambda move: move.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
+ self.assertEqual(len(details_operation_form.move_line_ids), 1)
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ self.assertEqual(ml.lot_id, remaining_lot)
+
+ def test_product_produce_3(self):
+ """ Checks that, for a BOM where one of the components is tracked by lot and the other is
+ not tracked, when creating a manufacturing order for 1 finished product and reserving, the
+ reserved lines are displayed. Then, over-consume by creating new line.
+ """
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.stock_shelf_1 = self.stock_location_components
+
+ self.stock_shelf_2 = self.stock_location_14
+ mo, _, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot', qty_base_1=10, qty_final=1)
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ first_lot_for_p1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ })
+ second_lot_for_p1 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ })
+
+ final_product_lot = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': p_final.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_1, 3, lot_id=first_lot_for_p1)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_2, 3, lot_id=first_lot_for_p1)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 8, lot_id=second_lot_for_p1)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+
+ mo.action_assign()
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1.0
+ mo_form.lot_producing_id = final_product_lot
+ mo = mo_form.save()
+ # p2
+ details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as line:
+ line.qty_done = line.product_uom_qty
+ with details_operation_form.move_line_ids.new() as line:
+ line.qty_done = 1
+ details_operation_form.save()
+
+ # p1
+ details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
+ for i in range(len(details_operation_form.move_line_ids)):
+ # reservation in shelf1: 3 lot1, shelf2: 3 lot1, stock: 4 lot2
+ with details_operation_form.move_line_ids.edit(i) as line:
+ line.qty_done = line.product_uom_qty
+ with details_operation_form.move_line_ids.new() as line:
+ line.qty_done = 2
+ line.lot_id = first_lot_for_p1
+ with details_operation_form.move_line_ids.new() as line:
+ line.qty_done = 1
+ line.lot_id = second_lot_for_p1
+ details_operation_form.save()
+
+ move_1 = mo.move_raw_ids.filtered(lambda m: m.product_id == p1)
+ # qty_done/product_uom_qty lot
+ # 3/3 lot 1 shelf 1
+ # 1/1 lot 1 shelf 2
+ # 2/2 lot 1 shelf 2
+ # 2/0 lot 1 other
+ # 5/4 lot 2
+ ml_to_shelf_1 = move_1.move_line_ids.filtered(lambda ml: ml.lot_id == first_lot_for_p1 and ml.location_id == self.stock_shelf_1)
+ ml_to_shelf_2 = move_1.move_line_ids.filtered(lambda ml: ml.lot_id == first_lot_for_p1 and ml.location_id == self.stock_shelf_2)
+
+ self.assertEqual(sum(ml_to_shelf_1.mapped('qty_done')), 3.0, '3 units should be took from shelf1 as reserved.')
+ self.assertEqual(sum(ml_to_shelf_2.mapped('qty_done')), 3.0, '3 units should be took from shelf2 as reserved.')
+ self.assertEqual(move_1.quantity_done, 13, 'You should have used the tem units.')
+
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done', "Production order should be in done state.")
+
+ def test_product_produce_4(self):
+ """ Possibility to produce with a given raw material in multiple locations. """
+ # FIXME sle: how is it possible to consume before producing in the interface?
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.stock_shelf_1 = self.stock_location_components
+ self.stock_shelf_2 = self.stock_location_14
+ mo, _, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=5)
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_1, 2)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_2, 3)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1)
+
+ mo.action_assign()
+ ml_p1 = mo.move_raw_ids.filtered(lambda x: x.product_id == p1).mapped('move_line_ids')
+ ml_p2 = mo.move_raw_ids.filtered(lambda x: x.product_id == p2).mapped('move_line_ids')
+ self.assertEqual(len(ml_p1), 2)
+ self.assertEqual(len(ml_p2), 1)
+
+ # Add some quantity already done to force an extra move line to be created
+ ml_p1[0].qty_done = 1.0
+
+ # Produce baby!
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ mo = mo_form.save()
+
+ m_p1 = mo.move_raw_ids.filtered(lambda x: x.product_id == p1)
+ ml_p1 = m_p1.mapped('move_line_ids')
+ self.assertEqual(len(ml_p1), 2)
+ self.assertEqual(sorted(ml_p1.mapped('qty_done')), [2.0, 3.0], 'Quantity done should be 1.0, 2.0 or 3.0')
+ self.assertEqual(m_p1.quantity_done, 5.0, 'Total qty done should be 6.0')
+ self.assertEqual(sum(ml_p1.mapped('product_uom_qty')), 5.0, 'Total qty reserved should be 5.0')
+
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done', "Production order should be in done state.")
+
+ def test_product_produce_6(self):
+ """ Plan 5 finished products, reserve and produce 3. Post the current production.
+ Simulate an unlock and edit and, on the opened moves, set the consumed quantity
+ to 3. Now, try to update the quantity to mo2 to 3. It should fail since there
+ are consumed quantities. Unlock and edit, remove the consumed quantities and
+ update the quantity to produce to 3."""
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo()
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 20)
+
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+ mo.action_assign()
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 3
+ mo = mo_form.save()
+
+ mo._post_inventory()
+ self.assertEqual(len(mo.move_raw_ids), 4)
+
+ mo.move_raw_ids.filtered(lambda m: m.state != 'done')[0].quantity_done = 3
+
+ update_quantity_wizard = self.env['change.production.qty'].create({
+ 'mo_id': mo.id,
+ 'product_qty': 3,
+ })
+
+ mo.move_raw_ids.filtered(lambda m: m.state != 'done')[0].quantity_done = 0
+ update_quantity_wizard.change_prod_qty()
+
+ self.assertEqual(len(mo.move_raw_ids), 2)
+
+ mo.button_mark_done()
+ self.assertTrue(all(s == 'done' for s in mo.move_raw_ids.mapped('state')))
+ self.assertEqual(sum(mo.move_raw_ids.mapped('move_line_ids.product_uom_qty')), 0)
+
+ def test_consumption_strict_1(self):
+ """ Checks the constraints of a strict BOM without tracking when playing around
+ quantities to consume."""
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo(consumption='strict', qty_final=1)
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+
+ mo.action_assign()
+
+ mo_form = Form(mo)
+
+ # try adding another line for a bom product to increase the quantity
+ mo_form.qty_producing = 1
+ with mo_form.move_raw_ids.new() as line:
+ line.product_id = p1
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.qty_done = 1
+ details_operation_form.save()
+ # Won't accept to be done, instead return a wizard
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'to_close')
+ consumption_issues = mo._get_consumption_issues()
+ action = mo._action_generate_consumption_wizard(consumption_issues)
+ warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context']))
+ warning = warning.save()
+
+ self.assertEqual(len(warning.mrp_consumption_warning_line_ids), 1)
+ self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_consumed_qty_uom, 5)
+ self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_expected_qty_uom, 4)
+ # Force the warning (as a manager)
+ warning.action_confirm()
+ self.assertEqual(mo.state, 'done')
+
+ def test_consumption_warning_1(self):
+ """ Checks the constraints of a strict BOM without tracking when playing around
+ quantities to consume."""
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo(consumption='warning', qty_final=1)
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+
+ mo.action_assign()
+
+ mo_form = Form(mo)
+
+ # try adding another line for a bom product to increase the quantity
+ mo_form.qty_producing = 1
+ with mo_form.move_raw_ids.new() as line:
+ line.product_id = p1
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.qty_done = 1
+ details_operation_form.save()
+
+ # Won't accept to be done, instead return a wizard
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'to_close')
+
+ consumption_issues = mo._get_consumption_issues()
+ action = mo._action_generate_consumption_wizard(consumption_issues)
+ warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context']))
+ warning = warning.save()
+
+ self.assertEqual(len(warning.mrp_consumption_warning_line_ids), 1)
+ self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_consumed_qty_uom, 5)
+ self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_expected_qty_uom, 4)
+ # Force the warning (as a manager or employee)
+ warning.action_confirm()
+ self.assertEqual(mo.state, 'done')
+
+ def test_consumption_flexible_1(self):
+ """ Checks the constraints of a strict BOM without tracking when playing around
+ quantities to consume."""
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo(consumption='flexible', qty_final=1)
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+
+ mo.action_assign()
+
+ mo_form = Form(mo)
+
+ # try adding another line for a bom product to increase the quantity
+ mo_form.qty_producing = 1
+ with mo_form.move_raw_ids.new() as line:
+ line.product_id = p1
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.qty_done = 1
+ details_operation_form.save()
+
+ # Won't accept to be done, instead return a wizard
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done')
+
+ def test_consumption_flexible_2(self):
+ """ Checks the constraints of a strict BOM only apply to the product of the BoM. """
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo(consumption='flexible', qty_final=1)
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+ add_product = self.env['product.product'].create({
+ 'name': 'additional',
+ 'type': 'product',
+ })
+ mo.action_assign()
+
+ mo_form = Form(mo)
+
+ # try adding another line for a bom product to increase the quantity
+ mo_form.qty_producing = 1
+ with mo_form.move_raw_ids.new() as line:
+ line.product_id = p1
+ with mo_form.move_raw_ids.new() as line:
+ line.product_id = add_product
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.qty_done = 1
+ details_operation_form.save()
+
+ # Won't accept to be done, instead return a wizard
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done')
+
+ def test_product_produce_9(self):
+ """ Checks the production wizard contains lines even for untracked products. """
+ serial = self.env['product.product'].create({
+ 'name': 'S1',
+ 'tracking': 'serial',
+ })
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo()
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+
+ mo.action_assign()
+ mo_form = Form(mo)
+
+ # change the quantity done in one line
+ with self.assertRaises(AssertionError):
+ with mo_form.move_raw_ids.new() as move:
+ move.product_id = serial
+ move.quantity_done = 2
+ mo_form.save()
+
+ def test_product_produce_10(self):
+ """ Produce byproduct with serial, lot and not tracked.
+ byproduct1 serial 1.0
+ byproduct2 lot 2.0
+ byproduct3 none 1.0 dozen
+ Check qty producing update and moves finished values.
+ """
+ dozen = self.env.ref('uom.product_uom_dozen')
+ self.byproduct1 = self.env['product.product'].create({
+ 'name': 'Byproduct 1',
+ 'type': 'product',
+ 'tracking': 'serial'
+ })
+ self.serial_1 = self.env['stock.production.lot'].create({
+ 'product_id': self.byproduct1.id,
+ 'name': 'serial 1',
+ 'company_id': self.env.company.id,
+ })
+ self.serial_2 = self.env['stock.production.lot'].create({
+ 'product_id': self.byproduct1.id,
+ 'name': 'serial 2',
+ 'company_id': self.env.company.id,
+ })
+
+ self.byproduct2 = self.env['product.product'].create({
+ 'name': 'Byproduct 2',
+ 'type': 'product',
+ 'tracking': 'lot',
+ })
+ self.lot_1 = self.env['stock.production.lot'].create({
+ 'product_id': self.byproduct2.id,
+ 'name': 'Lot 1',
+ 'company_id': self.env.company.id,
+ })
+ self.lot_2 = self.env['stock.production.lot'].create({
+ 'product_id': self.byproduct2.id,
+ 'name': 'Lot 2',
+ 'company_id': self.env.company.id,
+ })
+
+ self.byproduct3 = self.env['product.product'].create({
+ 'name': 'Byproduct 3',
+ 'type': 'product',
+ 'tracking': 'none',
+ })
+
+ with Form(self.bom_1) as bom:
+ bom.product_qty = 1.0
+ with bom.byproduct_ids.new() as bp:
+ bp.product_id = self.byproduct1
+ bp.product_qty = 1.0
+ with bom.byproduct_ids.new() as bp:
+ bp.product_id = self.byproduct2
+ bp.product_qty = 2.0
+ with bom.byproduct_ids.new() as bp:
+ bp.product_id = self.byproduct3
+ bp.product_qty = 2.0
+ bp.product_uom_id = dozen
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = self.product_4
+ mo_form.bom_id = self.bom_1
+ mo_form.product_qty = 2
+ mo = mo_form.save()
+
+ mo.action_confirm()
+ move_byproduct_1 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct1)
+ self.assertEqual(len(move_byproduct_1), 1)
+ self.assertEqual(move_byproduct_1.product_uom_qty, 2.0)
+ self.assertEqual(move_byproduct_1.quantity_done, 0)
+ self.assertEqual(len(move_byproduct_1.move_line_ids), 0)
+
+ move_byproduct_2 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2)
+ self.assertEqual(len(move_byproduct_2), 1)
+ self.assertEqual(move_byproduct_2.product_uom_qty, 4.0)
+ self.assertEqual(move_byproduct_2.quantity_done, 0)
+ self.assertEqual(len(move_byproduct_2.move_line_ids), 0)
+
+ move_byproduct_3 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3)
+ self.assertEqual(move_byproduct_3.product_uom_qty, 4.0)
+ self.assertEqual(move_byproduct_3.quantity_done, 0)
+ self.assertEqual(move_byproduct_3.product_uom, dozen)
+ self.assertEqual(len(move_byproduct_3.move_line_ids), 0)
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1.0
+ mo = mo_form.save()
+ move_byproduct_1 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct1)
+ self.assertEqual(len(move_byproduct_1), 1)
+ self.assertEqual(move_byproduct_1.product_uom_qty, 2.0)
+ self.assertEqual(move_byproduct_1.quantity_done, 0)
+
+ move_byproduct_2 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2)
+ self.assertEqual(len(move_byproduct_2), 1)
+ self.assertEqual(move_byproduct_2.product_uom_qty, 4.0)
+ self.assertEqual(move_byproduct_2.quantity_done, 0)
+
+ move_byproduct_3 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3)
+ self.assertEqual(move_byproduct_3.product_uom_qty, 4.0)
+ self.assertEqual(move_byproduct_3.quantity_done, 2.0)
+ self.assertEqual(move_byproduct_3.product_uom, dozen)
+
+ details_operation_form = Form(move_byproduct_1, view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.serial_1
+ ml.qty_done = 1
+ details_operation_form.save()
+ details_operation_form = Form(move_byproduct_2, view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.lot_1
+ ml.qty_done = 2
+ details_operation_form.save()
+ action = mo.button_mark_done()
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_backorder()
+ mo2 = mo.procurement_group_id.mrp_production_ids[-1]
+
+ mo_form = Form(mo2)
+ mo_form.qty_producing = 1
+ mo2 = mo_form.save()
+
+ move_byproduct_1 = mo2.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct1)
+ self.assertEqual(len(move_byproduct_1), 1)
+ self.assertEqual(move_byproduct_1.product_uom_qty, 1.0)
+ self.assertEqual(move_byproduct_1.quantity_done, 0)
+
+ move_byproduct_2 = mo2.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2)
+ self.assertEqual(len(move_byproduct_2), 1)
+ self.assertEqual(move_byproduct_2.product_uom_qty, 2.0)
+ self.assertEqual(move_byproduct_2.quantity_done, 0)
+
+ move_byproduct_3 = mo2.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3)
+ self.assertEqual(move_byproduct_3.product_uom_qty, 2.0)
+ self.assertEqual(move_byproduct_3.quantity_done, 2.0)
+ self.assertEqual(move_byproduct_3.product_uom, dozen)
+
+ details_operation_form = Form(move_byproduct_1, view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.serial_2
+ ml.qty_done = 1
+ details_operation_form.save()
+ details_operation_form = Form(move_byproduct_2, view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.lot_2
+ ml.qty_done = 2
+ details_operation_form.save()
+ details_operation_form = Form(move_byproduct_3, view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 3
+ details_operation_form.save()
+
+ mo2.button_mark_done()
+ move_lines_byproduct_1 = (mo | mo2).move_finished_ids.filtered(lambda l: l.product_id == self.byproduct1).mapped('move_line_ids')
+ move_lines_byproduct_2 = (mo | mo2).move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2).mapped('move_line_ids')
+ move_lines_byproduct_3 = (mo | mo2).move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3).mapped('move_line_ids')
+ self.assertEqual(move_lines_byproduct_1.filtered(lambda ml: ml.lot_id == self.serial_1).qty_done, 1.0)
+ self.assertEqual(move_lines_byproduct_1.filtered(lambda ml: ml.lot_id == self.serial_2).qty_done, 1.0)
+ self.assertEqual(move_lines_byproduct_2.filtered(lambda ml: ml.lot_id == self.lot_1).qty_done, 2.0)
+ self.assertEqual(move_lines_byproduct_2.filtered(lambda ml: ml.lot_id == self.lot_2).qty_done, 2.0)
+ self.assertEqual(sum(move_lines_byproduct_3.mapped('qty_done')), 5.0)
+ self.assertEqual(move_lines_byproduct_3.mapped('product_uom_id'), dozen)
+
+ def test_product_produce_11(self):
+ """ Checks that, for a BOM with two components, when creating a manufacturing order for one
+ finished products and without reserving, the produce wizards proposes the corrects lines
+ even if we change the quantity to produce multiple times.
+ """
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1)
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 4)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1)
+
+ mo.bom_id.consumption = 'flexible' # Because we'll over-consume with a product not defined in the BOM
+ mo.action_assign()
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 3
+ self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 15, 'Update the produce quantity should change the components quantity.')
+ mo = mo_form.save()
+ self.assertEqual(sum(mo.move_raw_ids.mapped('reserved_availability')), 5, 'Update the produce quantity should not change the components reserved quantity.')
+ mo_form = Form(mo)
+ mo_form.qty_producing = 4
+ self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 20, 'Update the produce quantity should change the components quantity.')
+ mo = mo_form.save()
+ self.assertEqual(sum(mo.move_raw_ids.mapped('reserved_availability')), 5, 'Update the produce quantity should not change the components reserved quantity.')
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should change the components quantity.')
+ mo = mo_form.save()
+ self.assertEqual(sum(mo.move_raw_ids.mapped('reserved_availability')), 5, 'Update the produce quantity should not change the components reserved quantity.')
+ # try adding another product that doesn't belong to the BoM
+ with mo_form.move_raw_ids.new() as move:
+ move.product_id = self.product_4
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.qty_done = 10
+ details_operation_form.save()
+ # Check that this new product is not updated by qty_producing
+ mo_form = Form(mo)
+ mo_form.qty_producing = 2
+ for move in mo_form.move_raw_ids._records:
+ if move['product_id'] == self.product_4.id:
+ self.assertEqual(move['quantity_done'], 10)
+ break
+ mo = mo_form.save()
+ mo.button_mark_done()
+
+ def test_product_produce_duplicate_1(self):
+ """ produce a finished product tracked by serial number 2 times with the
+ same SN. Check that an error is raised the second time"""
+ mo1, bom, p_final, p1, p2 = self.generate_mo(tracking_final='serial', qty_final=1, qty_base_1=1,)
+
+ mo_form = Form(mo1)
+ mo_form.qty_producing = 1
+ mo1 = mo_form.save()
+ mo1.action_generate_serial()
+ sn = mo1.lot_producing_id
+ mo1.button_mark_done()
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = p_final
+ mo_form.bom_id = bom
+ mo_form.product_qty = 1
+ mo2 = mo_form.save()
+ mo2.action_confirm()
+
+ mo_form = Form(mo2)
+ with self.assertLogs(level="WARNING"):
+ mo_form.lot_producing_id = sn
+ mo2 = mo_form.save()
+ with self.assertRaises(UserError):
+ mo2.button_mark_done()
+
+ def test_product_produce_duplicate_2(self):
+ """ produce a finished product with component tracked by serial number 2
+ times with the same SN. Check that an error is raised the second time"""
+ mo1, bom, p_final, p1, p2 = self.generate_mo(tracking_base_2='serial', qty_final=1, qty_base_1=1,)
+ sn = self.env['stock.production.lot'].create({
+ 'name': 'sn used twice',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ })
+ mo_form = Form(mo1)
+ mo_form.qty_producing = 1
+ mo1 = mo_form.save()
+ details_operation_form = Form(mo1.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = sn
+ details_operation_form.save()
+ mo1.button_mark_done()
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = p_final
+ mo_form.bom_id = bom
+ mo_form.product_qty = 1
+ mo2 = mo_form.save()
+ mo2.action_confirm()
+
+ mo_form = Form(mo2)
+ mo_form.qty_producing = 1
+ mo2 = mo_form.save()
+ details_operation_form = Form(mo2.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = sn
+ details_operation_form.save()
+ with self.assertRaises(UserError):
+ mo2.button_mark_done()
+
+ def test_product_produce_duplicate_3(self):
+ """ produce a finished product with by-product tracked by serial number 2
+ times with the same SN. Check that an error is raised the second time"""
+ finished_product = self.env['product.product'].create({'name': 'finished product'})
+ byproduct = self.env['product.product'].create({'name': 'byproduct', 'tracking': 'serial'})
+ component = self.env['product.product'].create({'name': 'component'})
+ bom = self.env['mrp.bom'].create({
+ 'product_id': finished_product.id,
+ 'product_tmpl_id': finished_product.product_tmpl_id.id,
+ 'product_uom_id': finished_product.uom_id.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': component.id, 'product_qty': 1}),
+ ],
+ 'byproduct_ids': [
+ (0, 0, {'product_id': byproduct.id, 'product_qty': 1, 'product_uom_id': byproduct.uom_id.id})
+ ]})
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = finished_product
+ mo_form.bom_id = bom
+ mo_form.product_qty = 1
+ mo = mo_form.save()
+ mo.action_confirm()
+
+ sn = self.env['stock.production.lot'].create({
+ 'name': 'sn used twice',
+ 'product_id': byproduct.id,
+ 'company_id': self.env.company.id,
+ })
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ mo = mo_form.save()
+ move_byproduct = mo.move_finished_ids.filtered(lambda m: m.product_id != mo.product_id)
+ details_operation_form = Form(move_byproduct, view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = sn
+ details_operation_form.save()
+ mo.button_mark_done()
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = finished_product
+ mo_form.bom_id = bom
+ mo_form.product_qty = 1
+ mo2 = mo_form.save()
+ mo2.action_confirm()
+
+ mo_form = Form(mo2)
+ mo_form.qty_producing = 1
+ mo2 = mo_form.save()
+ move_byproduct = mo2.move_finished_ids.filtered(lambda m: m.product_id != mo.product_id)
+ details_operation_form = Form(move_byproduct, view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = sn
+ details_operation_form.save()
+ with self.assertRaises(UserError):
+ mo2.button_mark_done()
+
+ def test_product_produce_duplicate_4(self):
+ """ Consuming the same serial number two times should not give an error if
+ a repair order of the first production has been made before the second one"""
+ mo1, bom, p_final, p1, p2 = self.generate_mo(tracking_base_2='serial', qty_final=1, qty_base_1=1,)
+ sn = self.env['stock.production.lot'].create({
+ 'name': 'sn used twice',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ })
+ mo_form = Form(mo1)
+ mo_form.qty_producing = 1
+ mo1 = mo_form.save()
+ details_operation_form = Form(mo1.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = sn
+ details_operation_form.save()
+ mo1.button_mark_done()
+
+ unbuild_form = Form(self.env['mrp.unbuild'])
+ unbuild_form.product_id = p_final
+ unbuild_form.bom_id = bom
+ unbuild_form.product_qty = 1
+ unbuild_form.mo_id = mo1
+ unbuild_order = unbuild_form.save()
+ unbuild_order.action_unbuild()
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = p_final
+ mo_form.bom_id = bom
+ mo_form.product_qty = 1
+ mo2 = mo_form.save()
+ mo2.action_confirm()
+
+ mo_form = Form(mo2)
+ mo_form.qty_producing = 1
+ mo2 = mo_form.save()
+ details_operation_form = Form(mo2.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = sn
+ details_operation_form.save()
+ mo2.button_mark_done()
+
+ def test_product_produce_12(self):
+ """ Checks that, the production is robust against deletion of finished move."""
+
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1)
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ mo = mo_form.save()
+ # remove the finished move from the available to be updated
+ mo.move_finished_ids._action_done()
+ mo.button_mark_done()
+
+ def test_product_produce_13(self):
+ """ Check that the production cannot be completed without any consumption."""
+ product = self.env['product.product'].create({
+ 'name': 'Product no BoM',
+ 'type': 'product',
+ })
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product
+ mo = mo_form.save()
+ move = self.env['stock.move'].create({
+ 'name': 'mrp_move',
+ 'product_id': self.product_2.id,
+ 'product_uom': self.ref('uom.product_uom_unit'),
+ 'production_id': mo.id,
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_output'),
+ 'product_uom_qty': 0,
+ 'quantity_done': 0,
+ })
+ mo.move_raw_ids |= move
+ mo.action_confirm()
+
+ mo.qty_producing = 1
+ # can't produce without any consumption (i.e. components w/ 0 consumed)
+ with self.assertRaises(UserError):
+ mo.button_mark_done()
+
+ mo.move_raw_ids.quantity_done = 1
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done')
+
+ def test_product_produce_uom(self):
+ """ Produce a finished product tracked by serial number. Set another
+ UoM on the bom. The produce wizard should keep the UoM of the product (unit)
+ and quantity = 1."""
+ dozen = self.env.ref('uom.product_uom_dozen')
+ unit = self.env.ref('uom.product_uom_unit')
+ plastic_laminate = self.env['product.product'].create({
+ 'name': 'Plastic Laminate',
+ 'type': 'product',
+ 'uom_id': unit.id,
+ 'uom_po_id': unit.id,
+ 'tracking': 'serial',
+ })
+ ply_veneer = self.env['product.product'].create({
+ 'name': 'Ply Veneer',
+ 'type': 'product',
+ 'uom_id': unit.id,
+ 'uom_po_id': unit.id,
+ })
+ bom = self.env['mrp.bom'].create({
+ 'product_tmpl_id': plastic_laminate.product_tmpl_id.id,
+ 'product_uom_id': unit.id,
+ 'sequence': 1,
+ 'bom_line_ids': [(0, 0, {
+ 'product_id': ply_veneer.id,
+ 'product_qty': 1,
+ 'product_uom_id': unit.id,
+ 'sequence': 1,
+ })]
+ })
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = plastic_laminate
+ mo_form.bom_id = bom
+ mo_form.product_uom_id = dozen
+ mo_form.product_qty = 1
+ mo = mo_form.save()
+
+ final_product_lot = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': plastic_laminate.id,
+ 'company_id': self.env.company.id,
+ })
+
+ mo.action_confirm()
+ mo.action_assign()
+ self.assertEqual(mo.move_raw_ids.product_qty, 12, '12 units should be reserved.')
+
+ # produce product
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1/12.0
+ mo_form.lot_producing_id = final_product_lot
+ mo = mo_form.save()
+
+ move_line_raw = mo.move_raw_ids.mapped('move_line_ids').filtered(lambda m: m.qty_done)
+ self.assertEqual(move_line_raw.qty_done, 1)
+ self.assertEqual(move_line_raw.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.')
+
+ mo._post_inventory()
+ move_line_finished = mo.move_finished_ids.mapped('move_line_ids').filtered(lambda m: m.qty_done)
+ self.assertEqual(move_line_finished.qty_done, 1)
+ self.assertEqual(move_line_finished.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.')
+
+ def test_product_type_service_1(self):
+ # Create finished product
+ finished_product = self.env['product.product'].create({
+ 'name': 'Geyser',
+ 'type': 'product',
+ })
+
+ # Create service type product
+ product_raw = self.env['product.product'].create({
+ 'name': 'raw Geyser',
+ 'type': 'service',
+ })
+
+ # Create bom for finish product
+ bom = self.env['mrp.bom'].create({
+ 'product_id': finished_product.id,
+ 'product_tmpl_id': finished_product.product_tmpl_id.id,
+ 'product_uom_id': self.env.ref('uom.product_uom_unit').id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})]
+ })
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = finished_product
+ mo_form.bom_id = bom
+ mo_form.product_uom_id = self.env.ref('uom.product_uom_unit')
+ mo_form.product_qty = 1
+ mo = mo_form.save()
+
+ # Check Mo is created or not
+ self.assertTrue(mo, "Mo is created")
+
+ def test_immediate_validate_1(self):
+ """ In a production with a single available move raw, clicking on mark as done without filling any
+ quantities should open a wizard asking to process all the reservation (so, the whole move).
+ """
+ mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
+ mo.action_assign()
+ res_dict = mo.button_mark_done()
+ self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(mo.move_raw_ids.mapped('state'), ['done', 'done'])
+ self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1, 1])
+ self.assertEqual(mo.move_finished_ids.state, 'done')
+ self.assertEqual(mo.move_finished_ids.quantity_done, 1)
+
+ def test_immediate_validate_2(self):
+ """ In a production with a single available move raw, clicking on mark as done after filling quantity
+ for a stock move only will trigger an error as qty_producing is left to 0."""
+ mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
+ mo.action_assign()
+ details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.qty_done = 1
+ details_operation_form.save()
+ with self.assertRaises(UserError):
+ res_dict = mo.button_mark_done()
+
+ def test_immediate_validate_3(self):
+ """ In a production with a serial number tracked product. Check that the immediate production only creates
+ one unit of finished product. Test with reservation."""
+ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='serial', qty_final=2, qty_base_1=1, qty_base_2=1)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
+ mo.action_assign()
+ action = mo.button_mark_done()
+ self.assertEqual(action.get('res_model'), 'mrp.immediate.production')
+ wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
+ action = wizard.process()
+ self.assertEqual(action.get('res_model'), 'mrp.production.backorder')
+ wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
+ action = wizard.action_backorder()
+ self.assertEqual(mo.qty_producing, 1)
+ self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1, 1])
+ self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 2)
+ mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
+ self.assertEqual(mo_backorder.product_qty, 1)
+ self.assertEqual(mo_backorder.move_raw_ids.mapped('product_uom_qty'), [1, 1])
+
+ def test_immediate_validate_4(self):
+ """ In a production with a serial number tracked product. Check that the immediate production only creates
+ one unit of finished product. Test without reservation."""
+ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='serial', qty_final=2, qty_base_1=1, qty_base_2=1)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
+ action = mo.button_mark_done()
+ self.assertEqual(action.get('res_model'), 'mrp.immediate.production')
+ wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
+ action = wizard.process()
+ self.assertEqual(action.get('res_model'), 'mrp.production.backorder')
+ wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
+ action = wizard.action_backorder()
+ self.assertEqual(mo.qty_producing, 1)
+ self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1, 1])
+ self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 2)
+ mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
+ self.assertEqual(mo_backorder.product_qty, 1)
+ self.assertEqual(mo_backorder.move_raw_ids.mapped('product_uom_qty'), [1, 1])
+
+ def test_immediate_validate_5(self):
+ """Validate three productions at once."""
+ mo1, bom, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
+ mo1.action_assign()
+ mo2_form = Form(self.env['mrp.production'])
+ mo2_form.product_id = p_final
+ mo2_form.bom_id = bom
+ mo2_form.product_qty = 1
+ mo2 = mo2_form.save()
+ mo2.action_confirm()
+ mo2.action_assign()
+ mo3_form = Form(self.env['mrp.production'])
+ mo3_form.product_id = p_final
+ mo3_form.bom_id = bom
+ mo3_form.product_qty = 1
+ mo3 = mo3_form.save()
+ mo3.action_confirm()
+ mo3.action_assign()
+ mos = mo1 | mo2 | mo3
+ res_dict = mos.button_mark_done()
+ self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(mos.move_raw_ids.mapped('state'), ['done'] * 6)
+ self.assertEqual(mos.move_raw_ids.mapped('quantity_done'), [1] * 6)
+ self.assertEqual(mos.move_finished_ids.mapped('state'), ['done'] * 3)
+ self.assertEqual(mos.move_finished_ids.mapped('quantity_done'), [1] * 3)
+
+ def test_immediate_validate_6(self):
+ """In a production for a tracked product, clicking on mark as done without filling any quantities should
+ pop up the immediate transfer wizard. Processing should choose a new lot for the finished product. """
+ mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1, tracking_final='lot')
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
+ mo.action_assign()
+ res_dict = mo.button_mark_done()
+ self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(mo.move_raw_ids.mapped('state'), ['done'] * 2)
+ self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1] * 2)
+ self.assertEqual(mo.move_finished_ids.state, 'done')
+ self.assertEqual(mo.move_finished_ids.quantity_done, 1)
+ self.assertTrue(mo.move_finished_ids.move_line_ids.lot_id != False)
+
+ def test_immediate_validate_uom(self):
+ """In a production with a different uom than the finished product one, the
+ immediate production wizard should fill the correct quantities. """
+ p_final = self.env['product.product'].create({
+ 'name': 'final',
+ 'type': 'product',
+ })
+ component = self.env['product.product'].create({
+ 'name': 'component',
+ 'type': 'product',
+ })
+ bom = self.env['mrp.bom'].create({
+ 'product_id': p_final.id,
+ 'product_tmpl_id': p_final.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'consumption': 'flexible',
+ 'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})]
+ })
+ self.env['stock.quant']._update_available_quantity(component, self.stock_location_components, 25.0)
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.bom_id = bom
+ mo_form.product_uom_id = self.uom_dozen
+ mo_form.product_qty = 1
+ mo = mo_form.save()
+ mo.action_confirm()
+ mo.action_assign()
+ res_dict = mo.button_mark_done()
+ self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(mo.move_raw_ids.state, 'done')
+ self.assertEqual(mo.move_raw_ids.quantity_done, 12)
+ self.assertEqual(mo.move_finished_ids.state, 'done')
+ self.assertEqual(mo.move_finished_ids.quantity_done, 1)
+ self.assertEqual(component.qty_available, 13)
+
+ def test_immediate_validate_uom_2(self):
+ """The rounding precision of a component should be based on the UoM used in the MO for this component,
+ not on the produced product's UoM nor the default UoM of the component"""
+ uom_units = self.env['ir.model.data'].xmlid_to_object('uom.product_uom_unit')
+ uom_L = self.env['ir.model.data'].xmlid_to_object('uom.product_uom_litre')
+ uom_cL = self.env['uom.uom'].create({
+ 'name': 'cL',
+ 'category_id': uom_L.category_id.id,
+ 'uom_type': 'smaller',
+ 'factor': 100,
+ 'rounding': 1,
+ })
+ uom_units.rounding = 1
+ uom_L.rounding = 0.01
+
+ product = self.env['product.product'].create({
+ 'name': 'SuperProduct',
+ 'uom_id': uom_units.id,
+ })
+ consumable_component = self.env['product.product'].create({
+ 'name': 'Consumable Component',
+ 'type': 'consu',
+ 'uom_id': uom_cL.id,
+ 'uom_po_id': uom_cL.id,
+ })
+ storable_component = self.env['product.product'].create({
+ 'name': 'Storable Component',
+ 'type': 'product',
+ 'uom_id': uom_cL.id,
+ 'uom_po_id': uom_cL.id,
+ })
+ self.env['stock.quant']._update_available_quantity(storable_component, self.env.ref('stock.stock_location_stock'), 100)
+
+ for component in [consumable_component, storable_component]:
+ bom = self.env['mrp.bom'].create({
+ 'product_tmpl_id': product.product_tmpl_id.id,
+ 'bom_line_ids': [(0, 0, {
+ 'product_id': component.id,
+ 'product_qty': 0.2,
+ 'product_uom_id': uom_L.id,
+ })],
+ })
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.bom_id = bom
+ mo = mo_form.save()
+ mo.action_confirm()
+ action = mo.button_mark_done()
+ self.assertEqual(action.get('res_model'), 'mrp.immediate.production')
+ wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
+ action = wizard.process()
+
+ self.assertEqual(mo.move_raw_ids.product_uom_qty, 0.2)
+ self.assertEqual(mo.move_raw_ids.quantity_done, 0.2)
+
+ def test_copy(self):
+ """ Check that copying a done production, create all the stock moves"""
+ mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
+ mo.action_confirm()
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ mo = mo_form.save()
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done')
+ mo_copy = mo.copy()
+ self.assertTrue(mo_copy.move_raw_ids)
+ self.assertTrue(mo_copy.move_finished_ids)
+ mo_copy.action_confirm()
+ mo_form = Form(mo_copy)
+ mo_form.qty_producing = 1
+ mo_copy = mo_form.save()
+ mo_copy.button_mark_done()
+ self.assertEqual(mo_copy.state, 'done')
+
+ def test_product_produce_different_uom(self):
+ """ Check that for products tracked by lots,
+ with component product UOM different from UOM used in the BOM,
+ we do not create a new move line due to extra reserved quantity
+ caused by decimal rounding conversions.
+ """
+
+ # the overall decimal accuracy is set to 3 digits
+ precision = self.env.ref('product.decimal_product_uom')
+ precision.digits = 3
+
+ # define L and ml, L has rounding .001 but ml has rounding .01
+ # when producing e.g. 187.5ml, it will be rounded to .188L
+ categ_test = self.env['uom.category'].create({'name': 'Volume Test'})
+
+ uom_L = self.env['uom.uom'].create({
+ 'name': 'Test Liters',
+ 'category_id': categ_test.id,
+ 'uom_type': 'reference',
+ 'rounding': 0.001
+ })
+
+ uom_ml = self.env['uom.uom'].create({
+ 'name': 'Test ml',
+ 'category_id': categ_test.id,
+ 'uom_type': 'smaller',
+ 'rounding': 0.01,
+ 'factor_inv': 0.001,
+ })
+
+ # create a product component and the final product using the component
+ product_comp = self.env['product.product'].create({
+ 'name': 'Product Component',
+ 'type': 'product',
+ 'tracking': 'lot',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'uom_id': uom_L.id,
+ 'uom_po_id': uom_L.id,
+ })
+
+ product_final = self.env['product.product'].create({
+ 'name': 'Product Final',
+ 'type': 'product',
+ 'tracking': 'lot',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'uom_id': uom_L.id,
+ 'uom_po_id': uom_L.id,
+ })
+
+ # the products are tracked by lot, so we go through _generate_consumed_move_line
+ lot_final = self.env['stock.production.lot'].create({
+ 'name': 'Lot Final',
+ 'product_id': product_final.id,
+ 'company_id': self.env.company.id,
+ })
+
+ lot_comp = self.env['stock.production.lot'].create({
+ 'name': 'Lot Component',
+ 'product_id': product_comp.id,
+ 'company_id': self.env.company.id,
+ })
+
+ # update the quantity on hand for Component, in a lot
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.env['stock.quant']._update_available_quantity(product_comp, self.stock_location, 1, lot_id=lot_comp)
+
+ # create a BOM for Final, using Component
+ test_bom = self.env['mrp.bom'].create({
+ 'product_id': product_final.id,
+ 'product_tmpl_id': product_final.product_tmpl_id.id,
+ 'product_uom_id': uom_L.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [(0, 0, {
+ 'product_id': product_comp.id,
+ 'product_qty': 375.00,
+ 'product_uom_id': uom_ml.id
+ })],
+ })
+
+ # create a MO for this BOM
+ mo_product_final_form = Form(self.env['mrp.production'])
+ mo_product_final_form.product_id = product_final
+ mo_product_final_form.product_uom_id = uom_L
+ mo_product_final_form.bom_id = test_bom
+ mo_product_final_form.product_qty = 0.5
+ mo_product_final_form = mo_product_final_form.save()
+
+ mo_product_final_form.action_confirm()
+ mo_product_final_form.action_assign()
+ self.assertEqual(mo_product_final_form.reservation_state, 'assigned')
+
+ # produce
+ res_dict = mo_product_final_form.button_mark_done()
+ self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+
+ # check that in _generate_consumed_move_line,
+ # we do not create an extra move line because
+ # of a conversion 187.5ml = 0.188L
+ # thus creating an extra line with 'product_uom_qty': 0.5
+ self.assertEqual(len(mo_product_final_form.move_raw_ids.move_line_ids), 1, 'One move line should exist for the MO.')
+
+ def test_multi_button_plan(self):
+ """ Test batch methods (confirm/validate) of the MO with the same bom """
+ self.bom_2.type = "normal" # avoid to get the operation of the kit bom
+
+ mo_3 = Form(self.env['mrp.production'])
+ mo_3.bom_id = self.bom_3
+ mo_3 = mo_3.save()
+
+ self.assertEqual(len(mo_3.workorder_ids), 2)
+
+ mo_3.button_plan()
+ self.assertEqual(mo_3.state, 'confirmed')
+ self.assertEqual(mo_3.workorder_ids[0].state, 'ready')
+
+ mo_1 = Form(self.env['mrp.production'])
+ mo_1.bom_id = self.bom_3
+ mo_1 = mo_1.save()
+
+ mo_2 = Form(self.env['mrp.production'])
+ mo_2.bom_id = self.bom_3
+ mo_2 = mo_2.save()
+
+ self.assertEqual(mo_1.product_id, self.product_6)
+ self.assertEqual(mo_2.product_id, self.product_6)
+ self.assertEqual(len(self.bom_3.operation_ids), 2)
+ self.assertEqual(len(mo_1.workorder_ids), 2)
+ self.assertEqual(len(mo_2.workorder_ids), 2)
+
+ (mo_1 | mo_2).button_plan() # Confirm and plan in the same "request"
+ self.assertEqual(mo_1.state, 'confirmed')
+ self.assertEqual(mo_2.state, 'confirmed')
+ self.assertEqual(mo_1.workorder_ids[0].state, 'ready')
+ self.assertEqual(mo_2.workorder_ids[0].state, 'ready')
+
+ # produce
+ res_dict = (mo_1 | mo_2).button_mark_done()
+ self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(mo_1.state, 'done')
+ self.assertEqual(mo_2.state, 'done')
+
+ def test_workcenter_timezone(self):
+ # Workcenter is based in Bangkok
+ # Possible working hours are Monday to Friday, from 8:00 to 12:00 and from 13:00 to 17:00 (UTC+7)
+ workcenter = self.workcenter_1
+ workcenter.resource_calendar_id.tz = 'Asia/Bangkok'
+
+ bom = self.env['mrp.bom'].create({
+ 'product_tmpl_id': self.product_1.product_tmpl_id.id,
+ 'bom_line_ids': [(0, 0, {
+ 'product_id': self.product_2.id,
+ })],
+ 'operation_ids': [(0, 0, {
+ 'name': 'SuperOperation01',
+ 'workcenter_id': workcenter.id,
+ }), (0, 0, {
+ 'name': 'SuperOperation01',
+ 'workcenter_id': workcenter.id,
+ })],
+ })
+
+ # Next Monday at 6:00 am UTC
+ date_planned = (Dt.now() + timedelta(days=7 - Dt.now().weekday())).replace(hour=6, minute=0, second=0)
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.bom_id = bom
+ mo_form.date_planned_start = date_planned
+ mo = mo_form.save()
+
+ mo.workorder_ids[0].duration_expected = 240
+ mo.workorder_ids[1].duration_expected = 60
+
+ mo.action_confirm()
+ mo.button_plan()
+
+ # Asia/Bangkok is UTC+7 and the start date is on Monday at 06:00 UTC (i.e., 13:00 UTC+7).
+ # So, in Bangkok, the first workorder uses the entire Monday afternoon slot 13:00 - 17:00 UTC+7 (i.e., 06:00 - 10:00 UTC)
+ # The second job uses the beginning of the Tuesday morning slot: 08:00 - 09:00 UTC+7 (i.e., 01:00 - 02:00 UTC)
+ self.assertEqual(mo.workorder_ids[0].date_planned_start, date_planned)
+ self.assertEqual(mo.workorder_ids[0].date_planned_finished, date_planned + timedelta(hours=4))
+ tuesday = date_planned + timedelta(days=1)
+ self.assertEqual(mo.workorder_ids[1].date_planned_start, tuesday.replace(hour=1))
+ self.assertEqual(mo.workorder_ids[1].date_planned_finished, tuesday.replace(hour=2))
+
+ def test_products_with_variants(self):
+ """Check for product with different variants with same bom"""
+ product = self.env['product.template'].create({
+ "attribute_line_ids": [
+ [0, 0, {"attribute_id": 2, "value_ids": [[6, 0, [3, 4]]]}]
+ ],
+ "name": "Product with variants",
+ })
+
+ variant_1 = product.product_variant_ids[0]
+ variant_2 = product.product_variant_ids[1]
+
+ component = self.env['product.template'].create({
+ "name": "Component",
+ })
+
+ self.env['mrp.bom'].create({
+ 'product_id': False,
+ 'product_tmpl_id': product.id,
+ 'bom_line_ids': [
+ (0, 0, {'product_id': component.product_variant_id.id, 'product_qty': 1})
+ ]
+ })
+
+ # First behavior to check, is changing the product (same product but another variant) after saving the MO a first time.
+ mo_form_1 = Form(self.env['mrp.production'])
+ mo_form_1.product_id = variant_1
+ mo_1 = mo_form_1.save()
+ mo_form_1 = Form(self.env['mrp.production'].browse(mo_1.id))
+ mo_form_1.product_id = variant_2
+ mo_1 = mo_form_1.save()
+ mo_1.action_confirm()
+ mo_1.action_assign()
+ mo_form_1 = Form(self.env['mrp.production'].browse(mo_1.id))
+ mo_form_1.qty_producing = 1
+ mo_1 = mo_form_1.save()
+ mo_1.button_mark_done()
+
+ move_lines_1 = self.env['stock.move.line'].search([("reference", "=", mo_1.name)])
+ move_finished_ids_1 = self.env['stock.move'].search([("production_id", "=", mo_1.id)])
+ self.assertEqual(len(move_lines_1), 2, "There should only be 2 move lines: the component line and produced product line")
+ self.assertEqual(len(move_finished_ids_1), 1, "There should only be 1 produced product for this MO")
+ self.assertEqual(move_finished_ids_1.product_id, variant_2, "Incorrect variant produced")
+
+ # Second behavior is changing the product before saving the MO
+ mo_form_2 = Form(self.env['mrp.production'])
+ mo_form_2.product_id = variant_1
+ mo_form_2.product_id = variant_2
+ mo_2 = mo_form_2.save()
+ mo_2.action_confirm()
+ mo_2.action_assign()
+ mo_form_2 = Form(self.env['mrp.production'].browse(mo_2.id))
+ mo_form_2.qty_producing = 1
+ mo_2 = mo_form_2.save()
+ mo_2.button_mark_done()
+
+ move_lines_2 = self.env['stock.move.line'].search([("reference", "=", mo_2.name)])
+ move_finished_ids_2 = self.env['stock.move'].search([("production_id", "=", mo_2.id)])
+ self.assertEqual(len(move_lines_2), 2, "There should only be 2 move lines: the component line and produced product line")
+ self.assertEqual(len(move_finished_ids_2), 1, "There should only be 1 produced product for this MO")
+ self.assertEqual(move_finished_ids_2.product_id, variant_2, "Incorrect variant produced")
+
+ # Third behavior is changing the product before saving the MO, then another time after
+ mo_form_3 = Form(self.env['mrp.production'])
+ mo_form_3.product_id = variant_1
+ mo_form_3.product_id = variant_2
+ mo_3 = mo_form_3.save()
+ mo_form_3 = Form(self.env['mrp.production'].browse(mo_3.id))
+ mo_form_3.product_id = variant_1
+ mo_3 = mo_form_3.save()
+ mo_3.action_confirm()
+ mo_3.action_assign()
+ mo_form_3 = Form(self.env['mrp.production'].browse(mo_3.id))
+ mo_form_3.qty_producing = 1
+ mo_3 = mo_form_3.save()
+ mo_3.button_mark_done()
+
+ move_lines_3 = self.env['stock.move.line'].search([("reference", "=", mo_3.name)])
+ move_finished_ids_3 = self.env['stock.move'].search([("production_id", "=", mo_3.id)])
+ self.assertEqual(len(move_lines_3), 2, "There should only be 2 move lines: the component line and produced product line")
+ self.assertEqual(len(move_finished_ids_3), 1, "There should only be 1 produced product for this MO")
+ self.assertEqual(move_finished_ids_3.product_id, variant_1, "Incorrect variant produced")
+
+ def test_manufacturing_order_with_work_orders(self):
+ """Test the behavior of a manufacturing order when opening the workorder related to it,
+ as well as the behavior when a backorder is created
+ """
+ # create a few work centers
+ work_center_1 = self.env['mrp.workcenter'].create({"name": "WC1"})
+ work_center_2 = self.env['mrp.workcenter'].create({"name": "WC2"})
+ work_center_3 = self.env['mrp.workcenter'].create({"name": "WC3"})
+
+ # create a product, a bom related to it with 3 components and 3 operations
+ product = self.env['product.template'].create({"name": "Product"})
+ component_1 = self.env['product.template'].create({"name": "Component 1", "type": "product"})
+ component_2 = self.env['product.template'].create({"name": "Component 2", "type": "product"})
+ component_3 = self.env['product.template'].create({"name": "Component 3", "type": "product"})
+
+ self.env['stock.quant'].create({
+ "product_id": component_1.product_variant_id.id,
+ "location_id": 8,
+ "inventory_quantity": 100
+ })
+ self.env['stock.quant'].create({
+ "product_id": component_2.product_variant_id.id,
+ "location_id": 8,
+ "inventory_quantity": 100
+ })
+ self.env['stock.quant'].create({
+ "product_id": component_3.product_variant_id.id,
+ "location_id": 8,
+ "inventory_quantity": 100
+ })
+
+ self.env['mrp.bom'].create({
+ "product_tmpl_id": product.id,
+ "product_id": False,
+ "product_qty": 1,
+ "bom_line_ids": [
+ [0, 0, {"product_id": component_1.product_variant_id.id, "product_qty": 1}],
+ [0, 0, {"product_id": component_2.product_variant_id.id, "product_qty": 1}],
+ [0, 0, {"product_id": component_3.product_variant_id.id, "product_qty": 1}]
+ ],
+ "operation_ids": [
+ [0, 0, {"name": "Operation 1", "workcenter_id": work_center_1.id}],
+ [0, 0, {"name": "Operation 2", "workcenter_id": work_center_2.id}],
+ [0, 0, {"name": "Operation 3", "workcenter_id": work_center_3.id}]
+ ]
+ })
+
+ # create a manufacturing order with 10 product to produce
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product.product_variant_id
+ mo_form.product_qty = 10
+ mo = mo_form.save()
+
+ self.assertEqual(mo.state, 'draft')
+ mo.action_confirm()
+
+ wo_1 = mo.workorder_ids[0]
+ wo_2 = mo.workorder_ids[1]
+ wo_3 = mo.workorder_ids[2]
+ self.assertEqual(mo.state, 'confirmed')
+
+ wo_1.button_start()
+ self.assertEqual(mo.state, 'progress')
+ wo_1.button_finish()
+
+ wo_2.button_start()
+ wo_2.qty_producing = 8
+ wo_2.button_finish()
+
+ wo_3.button_start()
+ wo_3.qty_producing = 8
+ wo_3.button_finish()
+
+ self.assertEqual(mo.state, 'to_close')
+ mo.button_mark_done()
+
+ bo = self.env['mrp.production.backorder'].create({
+ "mrp_production_backorder_line_ids": [
+ [0, 0, {"mrp_production_id": mo.id, "to_backorder": True}]
+ ]
+ })
+ bo.action_backorder()
+
+ self.assertEqual(mo.state, 'done')
+
+ mo_2 = self.env['mrp.production'].browse(mo.id + 1)
+ self.assertEqual(mo_2.state, 'progress')
+ wo_4, wo_5, wo_6 = mo_2.workorder_ids
+ self.assertEqual(wo_4.state, 'cancel')
+
+ wo_5.button_start()
+ self.assertEqual(mo_2.state, 'progress')
+ wo_5.button_finish()
+
+ wo_6.button_start()
+ wo_6.button_finish()
+ self.assertEqual(mo_2.state, 'to_close')
+ mo_2.button_mark_done()
+ self.assertEqual(mo_2.state, 'done')
+
+ def test_move_finished_onchanges(self):
+ """ Test that move_finished_ids (i.e. produced products) are still correct even after
+ multiple onchanges have changed the the moves
+ """
+
+ product1 = self.env['product.product'].create({
+ 'name': 'Oatmeal Cookie',
+ })
+ product2 = self.env['product.product'].create({
+ 'name': 'Chocolate Chip Cookie',
+ })
+
+ # ===== product_id onchange checks ===== #
+ # check product_id onchange without saving
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product1
+ mo_form.product_id = product2
+ mo = mo_form.save()
+ self.assertEqual(len(mo.move_finished_ids), 1, 'Wrong number of finished product moves created')
+ self.assertEqual(mo.move_finished_ids.product_id, product2, 'Wrong product to produce in finished product move')
+ # check product_id onchange after saving
+ mo_form = Form(self.env['mrp.production'].browse(mo.id))
+ mo_form.product_id = product1
+ mo = mo_form.save()
+ self.assertEqual(len(mo.move_finished_ids), 1, 'Wrong number of finish product moves created')
+ self.assertEqual(mo.move_finished_ids.product_id, product1, 'Wrong product to produce in finished product move')
+ # check product_id onchange when mo._origin.product_id is unchanged
+ mo_form = Form(self.env['mrp.production'].browse(mo.id))
+ mo_form.product_id = product2
+ mo_form.product_id = product1
+ mo = mo_form.save()
+ self.assertEqual(len(mo.move_finished_ids), 1, 'Wrong number of finish product moves created')
+ self.assertEqual(mo.move_finished_ids.product_id, product1, 'Wrong product to produce in finished product move')
+
+ # ===== product_qty onchange checks ===== #
+ # check product_qty onchange without saving
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product1
+ mo_form.product_qty = 5
+ mo_form.product_qty = 10
+ mo2 = mo_form.save()
+ self.assertEqual(len(mo2.move_finished_ids), 1, 'Wrong number of finished product moves created')
+ self.assertEqual(mo2.move_finished_ids.product_qty, 10, 'Wrong qty to produce for the finished product move')
+
+ # check product_qty onchange after saving
+ mo_form = Form(self.env['mrp.production'].browse(mo2.id))
+ mo_form.product_qty = 5
+ mo2 = mo_form.save()
+ self.assertEqual(len(mo2.move_finished_ids), 1, 'Wrong number of finish product moves created')
+ self.assertEqual(mo2.move_finished_ids.product_qty, 5, 'Wrong qty to produce for the finished product move')
+
+ # check product_qty onchange when mo._origin.product_id is unchanged
+ mo_form = Form(self.env['mrp.production'].browse(mo2.id))
+ mo_form.product_qty = 10
+ mo_form.product_qty = 5
+ mo2 = mo_form.save()
+ self.assertEqual(len(mo2.move_finished_ids), 1, 'Wrong number of finish product moves created')
+ self.assertEqual(mo2.move_finished_ids.product_qty, 5, 'Wrong qty to produce for the finished product move')
+
+ # ===== product_uom_id onchange checks ===== #
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product1
+ mo_form.product_qty = 1
+ mo_form.product_uom_id = self.env['uom.uom'].browse(self.ref('uom.product_uom_dozen'))
+ mo3 = mo_form.save()
+ self.assertEqual(len(mo3.move_finished_ids), 1, 'Wrong number of finish product moves created')
+ self.assertEqual(mo3.move_finished_ids.product_qty, 12, 'Wrong qty to produce for the finished product move')
+
+ # ===== bom_id onchange checks ===== #
+ component = self.env['product.product'].create({
+ "name": "Sugar",
+ })
+
+ bom1 = self.env['mrp.bom'].create({
+ 'product_id': False,
+ 'product_tmpl_id': product1.product_tmpl_id.id,
+ 'bom_line_ids': [
+ (0, 0, {'product_id': component.id, 'product_qty': 1})
+ ]
+ })
+
+ bom2 = self.env['mrp.bom'].create({
+ 'product_id': False,
+ 'product_tmpl_id': product1.product_tmpl_id.id,
+ 'bom_line_ids': [
+ (0, 0, {'product_id': component.id, 'product_qty': 10})
+ ]
+ })
+ # check bom_id onchange before product change
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.bom_id = bom1
+ mo_form.bom_id = bom2
+ mo_form.product_id = product2
+ mo4 = mo_form.save()
+ self.assertFalse(mo4.bom_id, 'BoM should have been removed')
+ self.assertEqual(len(mo4.move_finished_ids), 1, 'Wrong number of finished product moves created')
+ self.assertEqual(mo4.move_finished_ids.product_id, product2, 'Wrong product to produce in finished product move')
+ # check bom_id onchange after product change
+ mo_form = Form(self.env['mrp.production'].browse(mo4.id))
+ mo_form.product_id = product1
+ mo_form.bom_id = bom1
+ mo_form.bom_id = bom2
+ mo4 = mo_form.save()
+ self.assertEqual(len(mo4.move_finished_ids), 1, 'Wrong number of finish product moves created')
+ self.assertEqual(mo4.move_finished_ids.product_id, product1, 'Wrong product to produce in finished product move')
+ # check product_id onchange when mo._origin.product_id is unchanged
+ mo_form = Form(self.env['mrp.production'].browse(mo4.id))
+ mo_form.bom_id = bom2
+ mo_form.bom_id = bom1
+ mo4 = mo_form.save()
+ self.assertEqual(len(mo4.move_finished_ids), 1, 'Wrong number of finish product moves created')
+ self.assertEqual(mo4.move_finished_ids.product_id, product1, 'Wrong product to produce in finished product move')
diff --git a/addons/mrp/tests/test_procurement.py b/addons/mrp/tests/test_procurement.py
new file mode 100644
index 00000000..b7da5255
--- /dev/null
+++ b/addons/mrp/tests/test_procurement.py
@@ -0,0 +1,598 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime, timedelta
+
+from odoo import fields
+from odoo.tests import Form
+from odoo.addons.mrp.tests.common import TestMrpCommon
+from odoo.exceptions import UserError
+from odoo.tools import mute_logger
+
+
+class TestProcurement(TestMrpCommon):
+
+ def test_procurement(self):
+ """This test case when create production order check procurement is create"""
+ # Update BOM
+ self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink()
+ self.bom_1.bom_line_ids.filtered(lambda x: x.product_id == self.product_1).unlink()
+ # Update route
+ self.warehouse = self.env.ref('stock.warehouse0')
+ self.warehouse.mto_pull_id.route_id.active = True
+ route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
+ route_mto = self.warehouse.mto_pull_id.route_id.id
+ self.product_4.write({'route_ids': [(6, 0, [route_manufacture, route_mto])]})
+
+ # Create production order
+ # -------------------------
+ # Product6 Unit 24
+ # Product4 8 Dozen
+ # Product2 12 Unit
+ # -----------------------
+
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.product_6
+ production_form.bom_id = self.bom_3
+ production_form.product_qty = 24
+ production_form.product_uom_id = self.product_6.uom_id
+ production_product_6 = production_form.save()
+ production_product_6.action_confirm()
+ production_product_6.action_assign()
+
+ # check production state is Confirmed
+ self.assertEqual(production_product_6.state, 'confirmed')
+
+ # Check procurement for product 4 created or not.
+ # Check it created a purchase order
+
+ move_raw_product4 = production_product_6.move_raw_ids.filtered(lambda x: x.product_id == self.product_4)
+ produce_product_4 = self.env['mrp.production'].search([('product_id', '=', self.product_4.id),
+ ('move_dest_ids', '=', move_raw_product4[0].id)])
+ # produce product
+ self.assertEqual(produce_product_4.reservation_state, 'confirmed', "Consume material not available")
+
+ # Create production order
+ # -------------------------
+ # Product 4 96 Unit
+ # Product2 48 Unit
+ # ---------------------
+ # Update Inventory
+ self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': self.product_2.id,
+ 'inventory_quantity': 48,
+ 'location_id': self.warehouse.lot_stock_id.id,
+ })
+ produce_product_4.action_assign()
+ self.assertEqual(produce_product_4.product_qty, 8, "Wrong quantity of finish product.")
+ self.assertEqual(produce_product_4.product_uom_id, self.uom_dozen, "Wrong quantity of finish product.")
+ self.assertEqual(produce_product_4.reservation_state, 'assigned', "Consume material not available")
+
+ # produce product4
+ # ---------------
+
+ mo_form = Form(produce_product_4)
+ mo_form.qty_producing = produce_product_4.product_qty
+ produce_product_4 = mo_form.save()
+ # Check procurement and Production state for product 4.
+ produce_product_4.button_mark_done()
+ self.assertEqual(produce_product_4.state, 'done', 'Production order should be in state done')
+
+ # Produce product 6
+ # ------------------
+
+ # Update Inventory
+ self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': self.product_2.id,
+ 'inventory_quantity': 12,
+ 'location_id': self.warehouse.lot_stock_id.id,
+ })
+ production_product_6.action_assign()
+
+ # ------------------------------------
+
+ self.assertEqual(production_product_6.reservation_state, 'assigned', "Consume material not available")
+ mo_form = Form(production_product_6)
+ mo_form.qty_producing = production_product_6.product_qty
+ production_product_6 = mo_form.save()
+ # Check procurement and Production state for product 6.
+ production_product_6.button_mark_done()
+ self.assertEqual(production_product_6.state, 'done', 'Production order should be in state done')
+ self.assertEqual(self.product_6.qty_available, 24, 'Wrong quantity available of finished product.')
+
+ def test_procurement_2(self):
+ """Check that a manufacturing order create the right procurements when the route are set on
+ a parent category of a product"""
+ # find a child category id
+ all_categ_id = self.env['product.category'].search([('parent_id', '=', None)], limit=1)
+ child_categ_id = self.env['product.category'].search([('parent_id', '=', all_categ_id.id)], limit=1)
+
+ # set the product of `self.bom_1` to this child category
+ for bom_line_id in self.bom_1.bom_line_ids:
+ # check that no routes are defined on the product
+ self.assertEqual(len(bom_line_id.product_id.route_ids), 0)
+ # set the category of the product to a child category
+ bom_line_id.product_id.categ_id = child_categ_id
+
+ # set the MTO route to the parent category (all)
+ self.warehouse = self.env.ref('stock.warehouse0')
+ mto_route = self.warehouse.mto_pull_id.route_id
+ mto_route.active = True
+ mto_route.product_categ_selectable = True
+ all_categ_id.write({'route_ids': [(6, 0, [mto_route.id])]})
+
+ # create MO, but check it raises error as components are in make to order and not everyone has
+ with self.assertRaises(UserError):
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.product_4
+ production_form.product_uom_id = self.product_4.uom_id
+ production_form.product_qty = 1
+ production_product_4 = production_form.save()
+ production_product_4.action_confirm()
+
+ def test_procurement_4(self):
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+ product_A = self.env['product.product'].create({
+ 'name': 'productA',
+ 'type': 'product',
+ 'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))]
+ })
+ product_B = self.env['product.product'].create({
+ 'name': 'productB',
+ 'type': 'product',
+ 'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))]
+ })
+ product_C = self.env['product.product'].create({
+ 'name': 'productC',
+ 'type': 'product',
+ })
+ product_route = self.env['stock.location.route'].create({
+ 'name': 'Stock -> output route',
+ 'product_selectable': True,
+ 'rule_ids': [(0, 0, {
+ 'name': 'Stock -> output rule',
+ 'action': 'pull',
+ 'picking_type_id': self.ref('stock.picking_type_internal'),
+ 'location_src_id': self.ref('stock.stock_location_stock'),
+ 'location_id': self.ref('stock.stock_location_output'),
+ })],
+ })
+
+ # Set this route on `product.product_product_3`
+ product_C.write({
+ 'route_ids': [(4, product_route.id)]
+ })
+
+ bom_A = self.env['mrp.bom'].create({
+ 'product_id': product_A.id,
+ 'product_tmpl_id': product_A.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_B.id, 'product_qty': 2.0})
+ ]})
+
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'A RR',
+ 'location_id': warehouse.lot_stock_id.id,
+ 'product_id': product_A.id,
+ 'product_min_qty': 10,
+ 'product_max_qty': 100,
+ })
+
+ bom_B = self.env['mrp.bom'].create({
+ 'product_id': product_B.id,
+ 'product_tmpl_id': product_B.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_C.id, 'product_qty': 1.0})
+ ]})
+
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'B RR',
+ 'location_id': warehouse.lot_stock_id.id,
+ 'product_id': product_B.id,
+ 'product_min_qty': 20,
+ 'product_max_qty': 200,
+ })
+
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'C RR',
+ 'location_id': warehouse.lot_stock_id.id,
+ 'product_id': product_C.id,
+ 'product_min_qty': 20,
+ 'product_max_qty': 200,
+ })
+
+ with mute_logger('odoo.addons.stock.models.procurement'):
+ self.env['procurement.group'].run_scheduler()
+
+ production_A = self.env['mrp.production'].search([
+ ('product_id', '=', product_A.id),
+ ('state', '=', 'confirmed')
+ ])
+ self.assertEqual(production_A.product_uom_qty, 100, "100 units of A should be scheduled for production")
+ production_B = self.env['mrp.production'].search([
+ ('product_id', '=', product_B.id),
+ ('state', '=', 'confirmed')
+ ])
+ self.assertEqual(sum(production_B.mapped('product_uom_qty')), 400, "400 units of B should be scheduled for production")
+
+ def test_procurement_3(self):
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+ warehouse.write({'reception_steps': 'three_steps'})
+ warehouse.mto_pull_id.route_id.active = True
+ self.env['stock.location']._parent_store_compute()
+ warehouse.reception_route_id.rule_ids.filtered(
+ lambda p: p.location_src_id == warehouse.wh_input_stock_loc_id and
+ p.location_id == warehouse.wh_qc_stock_loc_id).write({
+ 'procure_method': 'make_to_stock'
+ })
+
+ finished_product = self.env['product.product'].create({
+ 'name': 'Finished Product',
+ 'type': 'product',
+ })
+ component = self.env['product.product'].create({
+ 'name': 'Component',
+ 'type': 'product',
+ 'route_ids': [(4, warehouse.mto_pull_id.route_id.id)]
+ })
+ self.env['stock.quant']._update_available_quantity(component, warehouse.wh_input_stock_loc_id, 100)
+ bom = self.env['mrp.bom'].create({
+ 'product_id': finished_product.id,
+ 'product_tmpl_id': finished_product.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': component.id, 'product_qty': 1.0})
+ ]})
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = finished_product
+ mo_form.bom_id = bom
+ mo_form.product_qty = 5
+ mo_form.product_uom_id = finished_product.uom_id
+ mo_form.location_src_id = warehouse.lot_stock_id
+ mo = mo_form.save()
+ mo.action_confirm()
+ pickings = self.env['stock.picking'].search([('product_id', '=', component.id)])
+ self.assertEqual(len(pickings), 2.0)
+ picking_input_to_qc = pickings.filtered(lambda p: p.location_id == warehouse.wh_input_stock_loc_id)
+ picking_qc_to_stock = pickings - picking_input_to_qc
+ self.assertTrue(picking_input_to_qc)
+ self.assertTrue(picking_qc_to_stock)
+ picking_input_to_qc.action_assign()
+ self.assertEqual(picking_input_to_qc.state, 'assigned')
+ picking_input_to_qc.move_line_ids.write({'qty_done': 5.0})
+ picking_input_to_qc._action_done()
+ picking_qc_to_stock.action_assign()
+ self.assertEqual(picking_qc_to_stock.state, 'assigned')
+ picking_qc_to_stock.move_line_ids.write({'qty_done': 3.0})
+ picking_qc_to_stock.with_context(skip_backorder=True, picking_ids_not_to_backorder=picking_qc_to_stock.ids).button_validate()
+ self.assertEqual(picking_qc_to_stock.state, 'done')
+ mo.action_assign()
+ self.assertEqual(mo.move_raw_ids.reserved_availability, 3.0)
+ produce_form = Form(mo)
+ produce_form.qty_producing = 3.0
+ mo = produce_form.save()
+ self.assertEqual(mo.move_raw_ids.quantity_done, 3.0)
+ picking_qc_to_stock.move_line_ids.qty_done = 5.0
+ self.assertEqual(mo.move_raw_ids.reserved_availability, 5.0)
+ self.assertEqual(mo.move_raw_ids.quantity_done, 3.0)
+
+ def test_link_date_mo_moves(self):
+ """ Check link of shedule date for manufaturing with date stock move."""
+
+ # create a product with manufacture route
+ product_1 = self.env['product.product'].create({
+ 'name': 'AAA',
+ 'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))]
+ })
+
+ component_1 = self.env['product.product'].create({
+ 'name': 'component',
+ })
+
+ self.env['mrp.bom'].create({
+ 'product_id': product_1.id,
+ 'product_tmpl_id': product_1.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': component_1.id, 'product_qty': 1}),
+ ]})
+
+ # create a move for product_1 from stock to output and reserve to trigger the
+ # rule
+ move_dest = self.env['stock.move'].create({
+ 'name': 'move_orig',
+ 'product_id': product_1.id,
+ 'product_uom': self.ref('uom.product_uom_unit'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_output'),
+ 'product_uom_qty': 10,
+ 'procure_method': 'make_to_order'
+ })
+
+ move_dest._action_confirm()
+ mo = self.env['mrp.production'].search([
+ ('product_id', '=', product_1.id),
+ ('state', '=', 'confirmed')
+ ])
+
+ self.assertAlmostEqual(mo.move_finished_ids.date, mo.move_raw_ids.date + timedelta(hours=1), delta=timedelta(seconds=1))
+
+ self.assertEqual(len(mo), 1, 'the manufacture order is not created')
+
+ mo_form = Form(mo)
+ self.assertEqual(mo_form.product_qty, 10, 'the quantity to produce is not good relative to the move')
+
+ mo = mo_form.save()
+
+ # Confirming mo create finished move
+ move_orig = self.env['stock.move'].search([
+ ('move_dest_ids', 'in', move_dest.ids)
+ ], limit=1)
+
+ self.assertEqual(len(move_orig), 1, 'the move orig is not created')
+ self.assertEqual(move_orig.product_qty, 10, 'the quantity to produce is not good relative to the move')
+
+ new_sheduled_date = fields.Datetime.to_datetime(mo.date_planned_start) + timedelta(days=5)
+ mo_form = Form(mo)
+ mo_form.date_planned_start = new_sheduled_date
+ mo = mo_form.save()
+
+ self.assertAlmostEqual(mo.move_raw_ids.date, mo.date_planned_start, delta=timedelta(seconds=1))
+ self.assertAlmostEqual(mo.move_finished_ids.date, mo.date_planned_finished, delta=timedelta(seconds=1))
+
+ def test_finished_move_cancellation(self):
+ """Check state of finished move on cancellation of raw moves. """
+ product_bottle = self.env['product.product'].create({
+ 'name': 'Plastic Bottle',
+ 'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))]
+ })
+
+ component_mold = self.env['product.product'].create({
+ 'name': 'Plastic Mold',
+ })
+
+ self.env['mrp.bom'].create({
+ 'product_id': product_bottle.id,
+ 'product_tmpl_id': product_bottle.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': component_mold.id, 'product_qty': 1}),
+ ]})
+
+ move_dest = self.env['stock.move'].create({
+ 'name': 'move_bottle',
+ 'product_id': product_bottle.id,
+ 'product_uom': self.ref('uom.product_uom_unit'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_output'),
+ 'product_uom_qty': 10,
+ 'procure_method': 'make_to_order',
+ })
+
+ move_dest._action_confirm()
+ mo = self.env['mrp.production'].search([
+ ('product_id', '=', product_bottle.id),
+ ('state', '=', 'confirmed')
+ ])
+ mo.move_raw_ids[0]._action_cancel()
+ self.assertEqual(mo.state, 'cancel', 'Manufacturing order should be cancelled.')
+ self.assertEqual(mo.move_finished_ids[0].state, 'cancel', 'Finished move should be cancelled if mo is cancelled.')
+ self.assertEqual(mo.move_dest_ids[0].state, 'waiting', 'Destination move should not be cancelled if prapogation cancel is False on manufacturing rule.')
+
+ def test_procurement_with_empty_bom(self):
+ """Ensure that a procurement request using a product with an empty BoM
+ will create a MO in draft state that could be completed afterwards.
+ """
+ self.warehouse = self.env.ref('stock.warehouse0')
+ route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
+ route_mto = self.warehouse.mto_pull_id.route_id.id
+ product = self.env['product.product'].create({
+ 'name': 'Clafoutis',
+ 'route_ids': [(6, 0, [route_manufacture, route_mto])]
+ })
+ self.env['mrp.bom'].create({
+ 'product_id': product.id,
+ 'product_tmpl_id': product.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ })
+ move_dest = self.env['stock.move'].create({
+ 'name': 'Customer MTO Move',
+ 'product_id': product.id,
+ 'product_uom': self.ref('uom.product_uom_unit'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_output'),
+ 'product_uom_qty': 10,
+ 'procure_method': 'make_to_order',
+ })
+ move_dest._action_confirm()
+
+ production = self.env['mrp.production'].search([('product_id', '=', product.id)])
+ self.assertTrue(production)
+ self.assertFalse(production.move_raw_ids)
+ self.assertEqual(production.state, 'draft')
+
+ comp1 = self.env['product.product'].create({
+ 'name': 'egg',
+ })
+ move_values = production._get_move_raw_values(comp1, 40.0, self.env.ref('uom.product_uom_unit'))
+ self.env['stock.move'].create(move_values)
+
+ production.action_confirm()
+ produce_form = Form(production)
+ produce_form.qty_producing = production.product_qty
+ production = produce_form.save()
+ production.button_mark_done()
+
+ move_dest._action_assign()
+ self.assertEqual(move_dest.reserved_availability, 10.0)
+
+ def test_auto_assign(self):
+ """ When auto reordering rule exists, check for when:
+ 1. There is not enough of a manufactured product to assign (reserve for) a picking => auto-create 1st MO
+ 2. There is not enough of a manufactured component to assign the created MO => auto-create 2nd MO
+ 3. Add an extra manufactured component (not in stock) to 1st MO => auto-create 3rd MO
+ 4. When 2nd MO is completed => auto-assign to 1st MO
+ 5. When 1st MO is completed => auto-assign to picking """
+
+ self.warehouse = self.env.ref('stock.warehouse0')
+ route_manufacture = self.warehouse.manufacture_pull_id.route_id
+
+ product_1 = self.env['product.product'].create({
+ 'name': 'Cake',
+ 'type': 'product',
+ 'route_ids': [(6, 0, [route_manufacture.id])]
+ })
+ product_2 = self.env['product.product'].create({
+ 'name': 'Cake Mix',
+ 'type': 'product',
+ 'route_ids': [(6, 0, [route_manufacture.id])]
+ })
+ product_3 = self.env['product.product'].create({
+ 'name': 'Flour',
+ 'type': 'consu',
+ })
+
+ self.env['mrp.bom'].create({
+ 'product_id': product_1.id,
+ 'product_tmpl_id': product_1.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1,
+ 'consumption': 'flexible',
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_2.id, 'product_qty': 1}),
+ ]})
+
+ self.env['mrp.bom'].create({
+ 'product_id': product_2.id,
+ 'product_tmpl_id': product_2.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_3.id, 'product_qty': 1}),
+ ]})
+
+ # extra manufactured component added to 1st MO after it is already confirmed
+ product_4 = self.env['product.product'].create({
+ 'name': 'Flavor Enchancer',
+ 'type': 'product',
+ 'route_ids': [(6, 0, [route_manufacture.id])]
+ })
+ product_5 = self.env['product.product'].create({
+ 'name': 'MSG',
+ 'type': 'consu',
+ })
+
+ self.env['mrp.bom'].create({
+ 'product_id': product_4.id,
+ 'product_tmpl_id': product_4.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_5.id, 'product_qty': 1}),
+ ]})
+
+ # setup auto orderpoints (reordering rules)
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'Cake RR',
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'product_id': product_1.id,
+ 'product_min_qty': 0,
+ 'product_max_qty': 5,
+ })
+
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'Cake Mix RR',
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'product_id': product_2.id,
+ 'product_min_qty': 0,
+ 'product_max_qty': 5,
+ })
+
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'Flavor Enchancer RR',
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'product_id': product_4.id,
+ 'product_min_qty': 0,
+ 'product_max_qty': 5,
+ })
+
+ # create picking output to trigger creating MO for reordering product_1
+ pick_output = self.env['stock.picking'].create({
+ 'name': 'Cake Delivery Order',
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'move_lines': [(0, 0, {
+ 'name': '/',
+ 'product_id': product_1.id,
+ 'product_uom': product_1.uom_id.id,
+ 'product_uom_qty': 10.00,
+ 'procure_method': 'make_to_stock',
+ })],
+ })
+ pick_output.action_confirm() # should trigger orderpoint to create and confirm 1st MO
+ pick_output.action_assign()
+
+ mo = self.env['mrp.production'].search([
+ ('product_id', '=', product_1.id),
+ ('state', '=', 'confirmed')
+ ])
+
+ self.assertEqual(len(mo), 1, "Manufacture order was not automatically created")
+ mo.action_assign()
+ self.assertEqual(mo.move_raw_ids.reserved_availability, 0, "No components should be reserved yet")
+ self.assertEqual(mo.product_qty, 15, "Quantity to produce should be picking demand + reordering rule max qty")
+
+ # 2nd MO for product_2 should have been created and confirmed when 1st MO for product_1 was confirmed
+ mo2 = self.env['mrp.production'].search([
+ ('product_id', '=', product_2.id),
+ ('state', '=', 'confirmed')
+ ])
+
+ self.assertEqual(len(mo2), 1, 'Second manufacture order was not created')
+ self.assertEqual(mo2.product_qty, 20, "Quantity to produce should be MO's 'to consume' qty + reordering rule max qty")
+ mo2_form = Form(mo2)
+ mo2_form.qty_producing = 20
+ mo2 = mo2_form.save()
+ mo2.button_mark_done()
+
+ self.assertEqual(mo.move_raw_ids.reserved_availability, 15, "Components should have been auto-reserved")
+
+ # add new component to 1st MO
+ mo_form = Form(mo)
+ with mo_form.move_raw_ids.new() as line:
+ line.product_id = product_4
+ line.product_uom_qty = 1
+ mo_form.save() # should trigger orderpoint to create and confirm 3rd MO
+
+ mo3 = self.env['mrp.production'].search([
+ ('product_id', '=', product_4.id),
+ ('state', '=', 'confirmed')
+ ])
+
+ self.assertEqual(len(mo3), 1, 'Third manufacture order for added component was not created')
+ self.assertEqual(mo3.product_qty, 6, "Quantity to produce should be 1 + reordering rule max qty")
+
+ mo_form = Form(mo)
+ mo.move_raw_ids.quantity_done = 15
+ mo_form.qty_producing = 15
+ mo = mo_form.save()
+ mo.button_mark_done()
+
+ self.assertEqual(pick_output.move_ids_without_package.reserved_availability, 10, "Completed products should have been auto-reserved in picking")
diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py
new file mode 100644
index 00000000..64587e1c
--- /dev/null
+++ b/addons/mrp/tests/test_stock.py
@@ -0,0 +1,350 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import common
+from odoo.exceptions import UserError
+from odoo.tests import Form
+
+
+class TestWarehouse(common.TestMrpCommon):
+ def setUp(self):
+ super(TestWarehouse, self).setUp()
+
+ unit = self.env.ref("uom.product_uom_unit")
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.depot_location = self.env['stock.location'].create({
+ 'name': 'Depot',
+ 'usage': 'internal',
+ 'location_id': self.stock_location.id,
+ })
+ self.env["stock.putaway.rule"].create({
+ "location_in_id": self.stock_location.id,
+ "location_out_id": self.depot_location.id,
+ 'category_id': self.env.ref('product.product_category_all').id,
+ })
+ mrp_workcenter = self.env['mrp.workcenter'].create({
+ 'name': 'Assembly Line 1',
+ 'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id,
+ })
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'Initial inventory',
+ 'line_ids': [(0, 0, {
+ 'product_id': self.graphics_card.id,
+ 'product_uom_id': self.graphics_card.uom_id.id,
+ 'product_qty': 16.0,
+ 'location_id': self.stock_location_14.id,
+ })]
+ })
+ inventory.action_start()
+ inventory.action_validate()
+
+ self.bom_laptop = self.env['mrp.bom'].create({
+ 'product_tmpl_id': self.laptop.product_tmpl_id.id,
+ 'product_qty': 1,
+ 'product_uom_id': unit.id,
+ 'consumption': 'flexible',
+ 'bom_line_ids': [(0, 0, {
+ 'product_id': self.graphics_card.id,
+ 'product_qty': 1,
+ 'product_uom_id': unit.id
+ })],
+ 'operation_ids': [
+ (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
+ ],
+ })
+
+ def new_mo_laptop(self):
+ form = Form(self.env['mrp.production'])
+ form.product_id = self.laptop
+ form.product_qty = 1
+ form.bom_id = self.bom_laptop
+ p = form.save()
+ p.action_confirm()
+ p.action_assign()
+ return p
+
+ def test_manufacturing_route(self):
+ warehouse_1_stock_manager = self.warehouse_1.with_user(self.user_stock_manager)
+ manu_rule = self.env['stock.rule'].search([
+ ('action', '=', 'manufacture'),
+ ('warehouse_id', '=', self.warehouse_1.id)])
+ self.assertEqual(self.warehouse_1.manufacture_pull_id, manu_rule)
+ manu_route = manu_rule.route_id
+ self.assertIn(manu_route, warehouse_1_stock_manager._get_all_routes())
+ warehouse_1_stock_manager.write({
+ 'manufacture_to_resupply': False
+ })
+ self.assertFalse(self.warehouse_1.manufacture_pull_id.active)
+ self.assertFalse(self.warehouse_1.manu_type_id.active)
+ self.assertNotIn(manu_route, warehouse_1_stock_manager._get_all_routes())
+ warehouse_1_stock_manager.write({
+ 'manufacture_to_resupply': True
+ })
+ manu_rule = self.env['stock.rule'].search([
+ ('action', '=', 'manufacture'),
+ ('warehouse_id', '=', self.warehouse_1.id)])
+ self.assertEqual(self.warehouse_1.manufacture_pull_id, manu_rule)
+ self.assertTrue(self.warehouse_1.manu_type_id.active)
+ self.assertIn(manu_route, warehouse_1_stock_manager._get_all_routes())
+
+ def test_manufacturing_scrap(self):
+ """
+ Testing to do a scrap of consumed material.
+ """
+
+ # Update demo products
+ (self.product_4 | self.product_2).write({
+ 'tracking': 'lot',
+ })
+
+ # Update Bill Of Material to remove product with phantom bom.
+ self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink()
+
+ # Create Inventory Adjustment For Stick and Stone Tools with lot.
+ lot_product_4 = self.env['stock.production.lot'].create({
+ 'name': '0000000000001',
+ 'product_id': self.product_4.id,
+ 'company_id': self.env.company.id,
+ })
+ lot_product_2 = self.env['stock.production.lot'].create({
+ 'name': '0000000000002',
+ 'product_id': self.product_2.id,
+ 'company_id': self.env.company.id,
+ })
+
+ stock_inv_product_4 = self.env['stock.inventory'].create({
+ 'name': 'Stock Inventory for Stick',
+ 'product_ids': [(4, self.product_4.id)],
+ 'line_ids': [
+ (0, 0, {'product_id': self.product_4.id, 'product_uom_id': self.product_4.uom_id.id, 'product_qty': 8, 'prod_lot_id': lot_product_4.id, 'location_id': self.stock_location_14.id}),
+ ]})
+
+ stock_inv_product_2 = self.env['stock.inventory'].create({
+ 'name': 'Stock Inventory for Stone Tools',
+ 'product_ids': [(4, self.product_2.id)],
+ 'line_ids': [
+ (0, 0, {'product_id': self.product_2.id, 'product_uom_id': self.product_2.uom_id.id, 'product_qty': 12, 'prod_lot_id': lot_product_2.id, 'location_id': self.stock_location_14.id})
+ ]})
+ (stock_inv_product_4 | stock_inv_product_2)._action_start()
+ stock_inv_product_2.action_validate()
+ stock_inv_product_4.action_validate()
+
+ #Create Manufacturing order.
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.product_6
+ production_form.bom_id = self.bom_3
+ production_form.product_qty = 12
+ production_form.product_uom_id = self.product_6.uom_id
+ production_3 = production_form.save()
+ production_3.action_confirm()
+ production_3.action_assign()
+
+ # Check Manufacturing order's availability.
+ self.assertEqual(production_3.reservation_state, 'assigned', "Production order's availability should be Available.")
+
+ location_id = production_3.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) and production_3.location_src_id.id or production_3.location_dest_id.id,
+
+ # Scrap Product Wood without lot to check assert raise ?.
+ scrap_id = self.env['stock.scrap'].with_context(active_model='mrp.production', active_id=production_3.id).create({'product_id': self.product_2.id, 'scrap_qty': 1.0, 'product_uom_id': self.product_2.uom_id.id, 'location_id': location_id, 'production_id': production_3.id})
+ with self.assertRaises(UserError):
+ scrap_id.do_scrap()
+
+ # Scrap Product Wood with lot.
+ self.env['stock.scrap'].with_context(active_model='mrp.production', active_id=production_3.id).create({'product_id': self.product_2.id, 'scrap_qty': 1.0, 'product_uom_id': self.product_2.uom_id.id, 'location_id': location_id, 'lot_id': lot_product_2.id, 'production_id': production_3.id})
+
+ #Check scrap move is created for this production order.
+ #TODO: should check with scrap objects link in between
+
+# scrap_move = production_3.move_raw_ids.filtered(lambda x: x.product_id == self.product_2 and x.scrapped)
+# self.assertTrue(scrap_move, "There are no any scrap move created for production order.")
+
+ def test_putaway_after_manufacturing_3(self):
+ """ This test checks a tracked manufactured product will go to location
+ defined in putaway strategy when the production is recorded with
+ product.produce wizard.
+ """
+ self.laptop.tracking = 'serial'
+ mo_laptop = self.new_mo_laptop()
+ serial = self.env['stock.production.lot'].create({'product_id': self.laptop.id, 'company_id': self.env.company.id})
+
+ mo_form = Form(mo_laptop)
+ mo_form.qty_producing = 1
+ mo_form.lot_producing_id = serial
+ mo_laptop = mo_form.save()
+ mo_laptop.button_mark_done()
+
+ # We check if the laptop go in the depot and not in the stock
+ move = mo_laptop.move_finished_ids
+ location_dest = move.move_line_ids.location_dest_id
+ self.assertEqual(location_dest.id, self.depot_location.id)
+ self.assertNotEqual(location_dest.id, self.stock_location.id)
+
+class TestKitPicking(common.TestMrpCommon):
+ def setUp(self):
+ super(TestKitPicking, self).setUp()
+
+ def create_product(name):
+ p = Form(self.env['product.product'])
+ p.name = name
+ p.type = 'product'
+ return p.save()
+
+ # Create a kit 'kit_parent' :
+ # ---------------------------
+ #
+ # kit_parent --|- kit_2 x2 --|- component_d x1
+ # | |- kit_1 x2 -------|- component_a x2
+ # | |- component_b x1
+ # | |- component_c x3
+ # |
+ # |- kit_3 x1 --|- component_f x1
+ # | |- component_g x2
+ # |
+ # |- component_e x1
+ # Creating all components
+ component_a = create_product('Comp A')
+ component_b = create_product('Comp B')
+ component_c = create_product('Comp C')
+ component_d = create_product('Comp D')
+ component_e = create_product('Comp E')
+ component_f = create_product('Comp F')
+ component_g = create_product('Comp G')
+ # Creating all kits
+ kit_1 = create_product('Kit 1')
+ kit_2 = create_product('Kit 2')
+ kit_3 = create_product('kit 3')
+ self.kit_parent = create_product('Kit Parent')
+ # Linking the kits and the components via some 'phantom' BoMs
+ bom_kit_1 = self.env['mrp.bom'].create({
+ 'product_tmpl_id': kit_1.product_tmpl_id.id,
+ 'product_qty': 1.0,
+ 'type': 'phantom'})
+ BomLine = self.env['mrp.bom.line']
+ BomLine.create({
+ 'product_id': component_a.id,
+ 'product_qty': 2.0,
+ 'bom_id': bom_kit_1.id})
+ BomLine.create({
+ 'product_id': component_b.id,
+ 'product_qty': 1.0,
+ 'bom_id': bom_kit_1.id})
+ BomLine.create({
+ 'product_id': component_c.id,
+ 'product_qty': 3.0,
+ 'bom_id': bom_kit_1.id})
+ bom_kit_2 = self.env['mrp.bom'].create({
+ 'product_tmpl_id': kit_2.product_tmpl_id.id,
+ 'product_qty': 1.0,
+ 'type': 'phantom'})
+ BomLine.create({
+ 'product_id': component_d.id,
+ 'product_qty': 1.0,
+ 'bom_id': bom_kit_2.id})
+ BomLine.create({
+ 'product_id': kit_1.id,
+ 'product_qty': 2.0,
+ 'bom_id': bom_kit_2.id})
+ bom_kit_parent = self.env['mrp.bom'].create({
+ 'product_tmpl_id': self.kit_parent.product_tmpl_id.id,
+ 'product_qty': 1.0,
+ 'type': 'phantom'})
+ BomLine.create({
+ 'product_id': component_e.id,
+ 'product_qty': 1.0,
+ 'bom_id': bom_kit_parent.id})
+ BomLine.create({
+ 'product_id': kit_2.id,
+ 'product_qty': 2.0,
+ 'bom_id': bom_kit_parent.id})
+ bom_kit_3 = self.env['mrp.bom'].create({
+ 'product_tmpl_id': kit_3.product_tmpl_id.id,
+ 'product_qty': 1.0,
+ 'type': 'phantom'})
+ BomLine.create({
+ 'product_id': component_f.id,
+ 'product_qty': 1.0,
+ 'bom_id': bom_kit_3.id})
+ BomLine.create({
+ 'product_id': component_g.id,
+ 'product_qty': 2.0,
+ 'bom_id': bom_kit_3.id})
+ BomLine.create({
+ 'product_id': kit_3.id,
+ 'product_qty': 1.0,
+ 'bom_id': bom_kit_parent.id})
+
+ # We create an 'immediate transfer' receipt for x3 kit_parent
+ self.test_partner = self.env['res.partner'].create({
+ 'name': 'Notthat Guyagain',
+ })
+ self.test_supplier = self.env['stock.location'].create({
+ 'name': 'supplier',
+ 'usage': 'supplier',
+ 'location_id': self.env.ref('stock.stock_location_stock').id,
+ })
+
+ self.expected_quantities = {
+ component_a: 24,
+ component_b: 12,
+ component_c: 36,
+ component_d: 6,
+ component_e: 3,
+ component_f: 3,
+ component_g: 6
+ }
+
+ def test_kit_immediate_transfer(self):
+ """ Make sure a kit is split in the corrects quantity_done by components in case of an
+ immediate transfer.
+ """
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.test_supplier.id,
+ 'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
+ 'partner_id': self.test_partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ 'immediate_transfer': True
+ })
+ move_receipt_1 = self.env['stock.move'].create({
+ 'name': self.kit_parent.name,
+ 'product_id': self.kit_parent.id,
+ 'quantity_done': 3,
+ 'product_uom': self.kit_parent.uom_id.id,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ 'location_id': self.test_supplier.id,
+ 'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
+ })
+ picking.button_validate()
+
+ # We check that the picking has the correct quantities after its move were splitted.
+ self.assertEqual(len(picking.move_lines), 7)
+ for move_line in picking.move_lines:
+ self.assertEqual(move_line.quantity_done, self.expected_quantities[move_line.product_id])
+
+ def test_kit_planned_transfer(self):
+ """ Make sure a kit is split in the corrects product_qty by components in case of a
+ planned transfer.
+ """
+ picking = self.env['stock.picking'].create({
+ 'location_id': self.test_supplier.id,
+ 'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
+ 'partner_id': self.test_partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ 'immediate_transfer': False,
+ })
+ move_receipt_1 = self.env['stock.move'].create({
+ 'name': self.kit_parent.name,
+ 'product_id': self.kit_parent.id,
+ 'product_uom_qty': 3,
+ 'product_uom': self.kit_parent.uom_id.id,
+ 'picking_id': picking.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ 'location_id': self.test_supplier.id,
+ 'location_dest_id': self.warehouse_1.wh_input_stock_loc_id.id,
+ })
+ picking.action_confirm()
+
+ # We check that the picking has the correct quantities after its move were splitted.
+ self.assertEqual(len(picking.move_lines), 7)
+ for move_line in picking.move_lines:
+ self.assertEqual(move_line.product_qty, self.expected_quantities[move_line.product_id])
diff --git a/addons/mrp/tests/test_stock_report.py b/addons/mrp/tests/test_stock_report.py
new file mode 100644
index 00000000..ce97fad2
--- /dev/null
+++ b/addons/mrp/tests/test_stock_report.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import Form
+from odoo.addons.stock.tests.test_report import TestReportsCommon
+
+
+class TestSaleStockReports(TestReportsCommon):
+ def test_report_forecast_1_mo_count(self):
+ """ Creates and configures a product who could be produce and could be a component.
+ Plans some producing and consumming MO and check the report values.
+ """
+ # Create a variant attribute.
+ product_chocolate = self.env['product.product'].create({
+ 'name': 'Chocolate',
+ 'type': 'consu',
+ })
+ product_chococake = self.env['product.product'].create({
+ 'name': 'Choco Cake',
+ 'type': 'product',
+ })
+ product_double_chococake = self.env['product.product'].create({
+ 'name': 'Double Choco Cake',
+ 'type': 'product',
+ })
+
+ # Creates two BOM: one creating a regular slime, one using regular slimes.
+ bom_chococake = self.env['mrp.bom'].create({
+ 'product_id': product_chococake.id,
+ 'product_tmpl_id': product_chococake.product_tmpl_id.id,
+ 'product_uom_id': product_chococake.uom_id.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_chocolate.id, 'product_qty': 4}),
+ ],
+ })
+ bom_double_chococake = self.env['mrp.bom'].create({
+ 'product_id': product_double_chococake.id,
+ 'product_tmpl_id': product_double_chococake.product_tmpl_id.id,
+ 'product_uom_id': product_double_chococake.uom_id.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_chococake.id, 'product_qty': 2}),
+ ],
+ })
+
+ # Creates two MO: one for each BOM.
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product_chococake
+ mo_form.bom_id = bom_chococake
+ mo_form.product_qty = 10
+ mo_1 = mo_form.save()
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product_double_chococake
+ mo_form.bom_id = bom_double_chococake
+ mo_form.product_qty = 2
+ mo_2 = mo_form.save()
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ draft_production_qty = docs['draft_production_qty']
+ self.assertEqual(len(lines), 0, "Must have 0 line.")
+ self.assertEqual(draft_picking_qty['in'], 0)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ self.assertEqual(draft_production_qty['in'], 10)
+ self.assertEqual(draft_production_qty['out'], 4)
+
+ # Confirms the MO and checks the report lines.
+ mo_1.action_confirm()
+ mo_2.action_confirm()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids)
+ draft_picking_qty = docs['draft_picking_qty']
+ draft_production_qty = docs['draft_production_qty']
+ self.assertEqual(len(lines), 2, "Must have two line.")
+ line_1 = lines[0]
+ line_2 = lines[1]
+ self.assertEqual(line_1['document_in'].id, mo_1.id)
+ self.assertEqual(line_1['quantity'], 4)
+ self.assertEqual(line_1['document_out'].id, mo_2.id)
+ self.assertEqual(line_2['document_in'].id, mo_1.id)
+ self.assertEqual(line_2['quantity'], 6)
+ self.assertEqual(line_2['document_out'], False)
+ self.assertEqual(draft_picking_qty['in'], 0)
+ self.assertEqual(draft_picking_qty['out'], 0)
+ self.assertEqual(draft_production_qty['in'], 0)
+ self.assertEqual(draft_production_qty['out'], 0)
+
+ def test_report_forecast_2_production_backorder(self):
+ """ Creates a manufacturing order and produces half the quantity.
+ Then creates a backorder and checks the report.
+ """
+ # Configures the warehouse.
+ warehouse = self.env.ref('stock.warehouse0')
+ warehouse.manufacture_steps = 'pbm_sam'
+ # Configures a product.
+ product_apple_pie = self.env['product.product'].create({
+ 'name': 'Apple Pie',
+ 'type': 'product',
+ })
+ product_apple = self.env['product.product'].create({
+ 'name': 'Apple',
+ 'type': 'consu',
+ })
+ bom = self.env['mrp.bom'].create({
+ 'product_id': product_apple_pie.id,
+ 'product_tmpl_id': product_apple_pie.product_tmpl_id.id,
+ 'product_uom_id': product_apple_pie.uom_id.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_apple.id, 'product_qty': 5}),
+ ],
+ })
+ # Creates a MO and validates the pick components.
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product_apple_pie
+ mo_form.bom_id = bom
+ mo_form.product_qty = 4
+ mo_1 = mo_form.save()
+ mo_1.action_confirm()
+ pick = mo_1.move_raw_ids.move_orig_ids.picking_id
+ pick_form = Form(pick)
+ with pick_form.move_line_ids_without_package.edit(0) as move_line:
+ move_line.qty_done = 20
+ pick = pick_form.save()
+ pick.button_validate()
+ # Produces 3 products then creates a backorder for the remaining product.
+ mo_form = Form(mo_1)
+ mo_form.qty_producing = 3
+ mo_1 = mo_form.save()
+ action = mo_1.button_mark_done()
+ backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder = backorder_form.save()
+ backorder.action_backorder()
+
+ mo_2 = (mo_1.procurement_group_id.mrp_production_ids - mo_1)
+ # Checks the forecast report.
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids)
+ self.assertEqual(len(lines), 1, "Must have only one line about the backorder")
+ self.assertEqual(lines[0]['document_in'].id, mo_2.id)
+ self.assertEqual(lines[0]['quantity'], 1)
+ self.assertEqual(lines[0]['document_out'], False)
+
+ # Produces the last unit.
+ mo_form = Form(mo_2)
+ mo_form.qty_producing = 1
+ mo_2 = mo_form.save()
+ mo_2.button_mark_done()
+ # Checks the forecast report.
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids)
+ self.assertEqual(len(lines), 0, "Must have no line")
diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py
new file mode 100644
index 00000000..502cac05
--- /dev/null
+++ b/addons/mrp/tests/test_traceability.py
@@ -0,0 +1,308 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import Form, tagged
+from odoo.addons.mrp.tests.common import TestMrpCommon
+import uuid
+
+class TestTraceability(TestMrpCommon):
+ TRACKING_TYPES = ['none', 'serial', 'lot']
+
+ def _create_product(self, tracking):
+ return self.env['product.product'].create({
+ 'name': 'Product %s' % tracking,
+ 'type': 'product',
+ 'tracking': tracking,
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+
+ def test_tracking_types_on_mo(self):
+ finished_no_track = self._create_product('none')
+ finished_lot = self._create_product('lot')
+ finished_serial = self._create_product('serial')
+ consumed_no_track = self._create_product('none')
+ consumed_lot = self._create_product('lot')
+ consumed_serial = self._create_product('serial')
+ stock_id = self.env.ref('stock.stock_location_stock').id
+ inventory_adjustment = self.env['stock.inventory'].create({
+ 'name': 'Initial Inventory',
+ 'location_ids': [(4, stock_id)],
+ })
+ inventory_adjustment.action_start()
+ inventory_adjustment.write({
+ 'line_ids': [
+ (0,0, {'product_id': consumed_no_track.id, 'product_qty': 3, 'location_id': stock_id}),
+ (0,0, {'product_id': consumed_lot.id, 'product_qty': 3, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'L1', 'product_id': consumed_lot.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}),
+ (0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S1', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}),
+ (0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S2', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}),
+ (0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S3', 'product_id': consumed_serial.id, 'company_id': self.env.company.id}).id, 'location_id': stock_id}),
+ ]
+ })
+ inventory_adjustment.action_validate()
+ for finished_product in [finished_no_track, finished_lot, finished_serial]:
+ bom = self.env['mrp.bom'].create({
+ 'product_id': finished_product.id,
+ 'product_tmpl_id': finished_product.product_tmpl_id.id,
+ 'product_uom_id': self.env.ref('uom.product_uom_unit').id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': consumed_no_track.id, 'product_qty': 1}),
+ (0, 0, {'product_id': consumed_lot.id, 'product_qty': 1}),
+ (0, 0, {'product_id': consumed_serial.id, 'product_qty': 1}),
+ ],
+ })
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = finished_product
+ mo_form.bom_id = bom
+ mo_form.product_uom_id = self.env.ref('uom.product_uom_unit')
+ mo_form.product_qty = 1
+ mo = mo_form.save()
+ mo.action_confirm()
+ mo.action_assign()
+
+ # Start MO production
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1
+ if finished_product.tracking != 'none':
+ mo_form.lot_producing_id = self.env['stock.production.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id, 'company_id': self.env.company.id})
+ mo = mo_form.save()
+
+ details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 1
+ details_operation_form.save()
+ details_operation_form = Form(mo.move_raw_ids[2], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 1
+ details_operation_form.save()
+
+
+ mo.button_mark_done()
+
+ self.assertEqual(mo.state, 'done', "Production order should be in done state.")
+
+ # Check results of traceability
+ context = ({
+ 'active_id': mo.id,
+ 'model': 'mrp.production',
+ })
+ lines = self.env['stock.traceability.report'].with_context(context).get_lines()
+
+ self.assertEqual(len(lines), 1, "Should always return 1 line : the final product")
+
+ final_product = lines[0]
+ self.assertEqual(final_product['unfoldable'], True, "Final product should always be unfoldable")
+
+ # Find parts of the final products
+ lines = self.env['stock.traceability.report'].get_lines(final_product['id'], **{
+ 'level': final_product['level'],
+ 'model_id': final_product['model_id'],
+ 'model_name': final_product['model'],
+ })
+
+ self.assertEqual(len(lines), 3, "There should be 3 lines. 1 for untracked, 1 for lot, and 1 for serial")
+
+ for line in lines:
+ tracking = line['columns'][1].split(' ')[1]
+ self.assertEqual(
+ line['columns'][-1], "1.00 Units", 'Part with tracking type "%s", should have quantity = 1' % (tracking)
+ )
+ unfoldable = False if tracking == 'none' else True
+ self.assertEqual(
+ line['unfoldable'],
+ unfoldable,
+ 'Parts with tracking type "%s", should have be unfoldable : %s' % (tracking, unfoldable)
+ )
+
+ def test_tracking_on_byproducts(self):
+ product_final = self.env['product.product'].create({
+ 'name': 'Finished Product',
+ 'type': 'product',
+ 'tracking': 'serial',
+ })
+ product_1 = self.env['product.product'].create({
+ 'name': 'Raw 1',
+ 'type': 'product',
+ 'tracking': 'serial',
+ })
+ product_2 = self.env['product.product'].create({
+ 'name': 'Raw 2',
+ 'type': 'product',
+ 'tracking': 'serial',
+ })
+ byproduct_1 = self.env['product.product'].create({
+ 'name': 'Byproduct 1',
+ 'type': 'product',
+ 'tracking': 'serial',
+ })
+ byproduct_2 = self.env['product.product'].create({
+ 'name': 'Byproduct 2',
+ 'type': 'product',
+ 'tracking': 'serial',
+ })
+ bom_1 = self.env['mrp.bom'].create({
+ 'product_id': product_final.id,
+ 'product_tmpl_id': product_final.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'consumption': 'flexible',
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_1.id, 'product_qty': 1}),
+ (0, 0, {'product_id': product_2.id, 'product_qty': 1})
+ ],
+ 'byproduct_ids': [
+ (0, 0, {'product_id': byproduct_1.id, 'product_qty': 1, 'product_uom_id': byproduct_1.uom_id.id}),
+ (0, 0, {'product_id': byproduct_2.id, 'product_qty': 1, 'product_uom_id': byproduct_2.uom_id.id})
+ ]})
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product_final
+ mo_form.bom_id = bom_1
+ mo_form.product_qty = 2
+ mo = mo_form.save()
+ mo.action_confirm()
+
+ mo_form = Form(mo)
+ mo_form.lot_producing_id = self.env['stock.production.lot'].create({
+ 'product_id': product_final.id,
+ 'name': 'Final_lot_1',
+ 'company_id': self.env.company.id,
+ })
+ mo = mo_form.save()
+
+ details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.env['stock.production.lot'].create({
+ 'product_id': product_1.id,
+ 'name': 'Raw_1_lot_1',
+ 'company_id': self.env.company.id,
+ })
+ ml.qty_done = 1
+ details_operation_form.save()
+ details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.env['stock.production.lot'].create({
+ 'product_id': product_2.id,
+ 'name': 'Raw_2_lot_1',
+ 'company_id': self.env.company.id,
+ })
+ ml.qty_done = 1
+ details_operation_form.save()
+ details_operation_form = Form(
+ mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
+ view=self.env.ref('stock.view_stock_move_operations')
+ )
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.env['stock.production.lot'].create({
+ 'product_id': byproduct_1.id,
+ 'name': 'Byproduct_1_lot_1',
+ 'company_id': self.env.company.id,
+ })
+ ml.qty_done = 1
+ details_operation_form.save()
+ details_operation_form = Form(
+ mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
+ view=self.env.ref('stock.view_stock_move_operations')
+ )
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.env['stock.production.lot'].create({
+ 'product_id': byproduct_2.id,
+ 'name': 'Byproduct_2_lot_1',
+ 'company_id': self.env.company.id,
+ })
+ ml.qty_done = 1
+ details_operation_form.save()
+
+ action = mo.button_mark_done()
+ backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+ backorder.save().action_backorder()
+ mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
+ mo_form = Form(mo_backorder)
+ mo_form.lot_producing_id = self.env['stock.production.lot'].create({
+ 'product_id': product_final.id,
+ 'name': 'Final_lot_2',
+ 'company_id': self.env.company.id,
+ })
+ mo_form.qty_producing = 1
+ mo_backorder = mo_form.save()
+
+ details_operation_form = Form(
+ mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_1),
+ view=self.env.ref('stock.view_stock_move_operations')
+ )
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.env['stock.production.lot'].create({
+ 'product_id': product_1.id,
+ 'name': 'Raw_1_lot_2',
+ 'company_id': self.env.company.id,
+ })
+ ml.qty_done = 1
+ details_operation_form.save()
+ details_operation_form = Form(
+ mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_2),
+ view=self.env.ref('stock.view_stock_move_operations')
+ )
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.env['stock.production.lot'].create({
+ 'product_id': product_2.id,
+ 'name': 'Raw_2_lot_2',
+ 'company_id': self.env.company.id,
+ })
+ ml.qty_done = 1
+ details_operation_form.save()
+ details_operation_form = Form(
+ mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
+ view=self.env.ref('stock.view_stock_move_operations')
+ )
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.env['stock.production.lot'].create({
+ 'product_id': byproduct_1.id,
+ 'name': 'Byproduct_1_lot_2',
+ 'company_id': self.env.company.id,
+ })
+ ml.qty_done = 1
+ details_operation_form.save()
+ details_operation_form = Form(
+ mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
+ view=self.env.ref('stock.view_stock_move_operations')
+ )
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.lot_id = self.env['stock.production.lot'].create({
+ 'product_id': byproduct_2.id,
+ 'name': 'Byproduct_2_lot_2',
+ 'company_id': self.env.company.id,
+ })
+ ml.qty_done = 1
+ details_operation_form.save()
+
+ mo_backorder.button_mark_done()
+
+ # self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4)
+ # self.assertEqual(len(mo.move_finished_ids.mapped('move_line_ids')), 6)
+
+ mo = mo | mo_backorder
+ raw_move_lines = mo.move_raw_ids.mapped('move_line_ids')
+ raw_line_raw_1_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_1')
+ self.assertEqual(set(raw_line_raw_1_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
+ raw_line_raw_2_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_1')
+ self.assertEqual(set(raw_line_raw_2_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
+
+ finished_move_lines = mo.move_finished_ids.mapped('move_line_ids')
+ finished_move_line_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_1')
+ self.assertEqual(finished_move_line_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
+ finished_move_line_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_2')
+ raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2')
+ raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2')
+ self.assertEqual(finished_move_line_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
+
+ byproduct_move_line_1_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_1')
+ self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
+ byproduct_move_line_1_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_2')
+ self.assertEqual(byproduct_move_line_1_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
+
+ byproduct_move_line_2_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_1')
+ self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
+ byproduct_move_line_2_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_2')
+ self.assertEqual(byproduct_move_line_2_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
diff --git a/addons/mrp/tests/test_unbuild.py b/addons/mrp/tests/test_unbuild.py
new file mode 100644
index 00000000..159b4c3d
--- /dev/null
+++ b/addons/mrp/tests/test_unbuild.py
@@ -0,0 +1,599 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import Form
+from odoo.addons.mrp.tests.common import TestMrpCommon
+from odoo.exceptions import UserError
+
+
+class TestUnbuild(TestMrpCommon):
+ def setUp(self):
+ super(TestUnbuild, self).setUp()
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.env.ref('base.group_user').write({
+ 'implied_ids': [(4, self.env.ref('stock.group_production_lot').id)]
+ })
+
+ def test_unbuild_standart(self):
+ """ This test creates a MO and then creates 3 unbuild
+ orders for the final product. None of the products for this
+ test are tracked. It checks the stock state after each order
+ and ensure it is correct.
+ """
+ mo, bom, p_final, p1, p2 = self.generate_mo()
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+ mo.action_assign()
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 5.0
+ mo = mo_form.save()
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done', "Production order should be in done state.")
+
+ # Check quantity in stock before unbuild.
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
+
+ # ---------------------------------------------------
+ # unbuild
+ # ---------------------------------------------------
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 3
+ x.save().action_unbuild()
+
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 92, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 2
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 5
+ x.save().action_unbuild()
+
+ # Check quantity in stock after last unbuild.
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
+
+ def test_unbuild_with_final_lot(self):
+ """ This test creates a MO and then creates 3 unbuild
+ orders for the final product. Only the final product is tracked
+ by lot. It checks the stock state after each order
+ and ensure it is correct.
+ """
+ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot')
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ lot = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': p_final.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+ mo.action_assign()
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 5.0
+ mo_form.lot_producing_id = lot
+ mo = mo_form.save()
+
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done', "Production order should be in done state.")
+
+ # Check quantity in stock before unbuild.
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 5, 'You should have the 5 final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
+
+ # ---------------------------------------------------
+ # unbuild
+ # ---------------------------------------------------
+
+ # This should fail since we do not choose a lot to unbuild for final product.
+ with self.assertRaises(AssertionError):
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 3
+ unbuild_order = x.save()
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 3
+ x.lot_id = lot
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 2, 'You should have consumed 3 final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 92, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 2
+ x.lot_id = lot
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), 0, 'You should have 0 finalproduct in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 5
+ x.lot_id = lot
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
+
+ def test_unbuild_with_comnsumed_lot(self):
+ """ This test creates a MO and then creates 3 unbuild
+ orders for the final product. Only once of the two consumed
+ product is tracked by lot. It checks the stock state after each
+ order and ensure it is correct.
+ """
+ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot')
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ lot = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
+ mo.action_assign()
+ for ml in mo.move_raw_ids.mapped('move_line_ids'):
+ if ml.product_id.tracking != 'none':
+ self.assertEqual(ml.lot_id, lot, 'Wrong reserved lot.')
+
+ # FIXME sle: behavior change
+ mo_form = Form(mo)
+ mo_form.qty_producing = 5.0
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.lot_id = lot
+ ml.qty_done = 20
+ details_operation_form.save()
+
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done', "Production order should be in done state.")
+ # Check quantity in stock before unbuild.
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 80, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 0, 'You should have consumed all the 5 product in stock')
+
+ # ---------------------------------------------------
+ # unbuild
+ # ---------------------------------------------------
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 3
+ unbuild_order = x.save()
+
+ # This should fail since we do not provide the MO that we wanted to unbuild. (without MO we do not know which consumed lot we have to restore)
+ with self.assertRaises(UserError):
+ unbuild_order.action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have consumed 3 final product in stock')
+
+ unbuild_order.mo_id = mo.id
+ unbuild_order.action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 2, 'You should have consumed 3 final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 92, 'You should have 92 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 3, 'You should have consumed all the 5 product in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.mo_id = mo
+ x.product_qty = 2
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have 0 finalproduct in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 100, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 5, 'You should have consumed all the 5 product in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.mo_id = mo
+ x.product_qty = 5
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 120, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
+
+ def test_unbuild_with_everything_tracked(self):
+ """ This test creates a MO and then creates 3 unbuild
+ orders for the final product. All the products for this
+ test are tracked. It checks the stock state after each order
+ and ensure it is correct.
+ """
+ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_2='lot', tracking_base_1='lot')
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ lot_final = self.env['stock.production.lot'].create({
+ 'name': 'lot_final',
+ 'product_id': p_final.id,
+ 'company_id': self.env.company.id,
+ })
+ lot_1 = self.env['stock.production.lot'].create({
+ 'name': 'lot_consumed_1',
+ 'product_id': p1.id,
+ 'company_id': self.env.company.id,
+ })
+ lot_2 = self.env['stock.production.lot'].create({
+ 'name': 'lot_consumed_2',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot_1)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5, lot_id=lot_2)
+ mo.action_assign()
+
+ # FIXME sle: behavior change
+ mo_form = Form(mo)
+ mo_form.qty_producing = 5.0
+ mo_form.lot_producing_id = lot_final
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 5
+ details_operation_form.save()
+ details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = 20
+ details_operation_form.save()
+
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done', "Production order should be in done state.")
+ # Check quantity in stock before unbuild.
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have the 5 final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 80, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 5 product in stock')
+
+ # ---------------------------------------------------
+ # unbuild
+ # ---------------------------------------------------
+
+ x = Form(self.env['mrp.unbuild'])
+ with self.assertRaises(AssertionError):
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 3
+ x.save()
+
+ with self.assertRaises(AssertionError):
+ x.product_id = p_final
+ x.bom_id = bom
+ x.product_qty = 3
+ x.save()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock')
+
+ with self.assertRaises(AssertionError):
+ x.product_id = p_final
+ x.bom_id = bom
+ x.mo_id = mo
+ x.product_qty = 3
+ x.save()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 5, 'You should have consumed 3 final product in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.mo_id = mo
+ x.product_qty = 3
+ x.lot_id = lot_final
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 2, 'You should have consumed 3 final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 92, 'You should have 92 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have consumed all the 5 product in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.mo_id = mo
+ x.product_qty = 2
+ x.lot_id = lot_final
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 0, 'You should have 0 finalproduct in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 100, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 5, 'You should have consumed all the 5 product in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.mo_id = mo
+ x.product_qty = 5
+ x.lot_id = lot_final
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 120, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 10, 'You should have consumed all the 5 product in stock')
+
+ def test_unbuild_with_duplicate_move(self):
+ """ This test creates a MO from 3 different lot on a consumed product (p2).
+ The unbuild order should revert the correct quantity for each specific lot.
+ """
+ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='none', tracking_base_2='lot', tracking_base_1='none')
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+
+ lot_1 = self.env['stock.production.lot'].create({
+ 'name': 'lot_1',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ })
+ lot_2 = self.env['stock.production.lot'].create({
+ 'name': 'lot_2',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ })
+ lot_3 = self.env['stock.production.lot'].create({
+ 'name': 'lot_3',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ })
+ self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=lot_1)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_2)
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2, lot_id=lot_3)
+ mo.action_assign()
+
+ mo_form = Form(mo)
+ mo_form.qty_producing = 5.0
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids.filtered(lambda ml: ml.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.edit(0) as ml:
+ ml.qty_done = ml.product_uom_qty
+ with details_operation_form.move_line_ids.edit(1) as ml:
+ ml.qty_done = ml.product_uom_qty
+ with details_operation_form.move_line_ids.edit(2) as ml:
+ ml.qty_done = ml.product_uom_qty
+ details_operation_form.save()
+
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done', "Production order should be in done state.")
+ # Check quantity in stock before unbuild.
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 5, 'You should have the 5 final product in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 80, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 0, 'You should have consumed all the 1 product for lot 1 in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 0, 'You should have consumed all the 3 product for lot 2 in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 1, 'You should have consumed only 1 product for lot3 in stock')
+
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = p_final
+ x.bom_id = bom
+ x.mo_id = mo
+ x.product_qty = 5
+ x.save().action_unbuild()
+
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), 0, 'You should have no more final product in stock after unbuild')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 100, 'You should have 80 products in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 1, 'You should have get your product with lot 1 in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have the 3 basic product for lot 2 in stock')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 2, 'You should have get one product back for lot 3')
+
+ def test_production_links_with_non_tracked_lots(self):
+ """ This test produces an MO in two times and checks that the move lines are linked in a correct way
+ """
+ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_1='none', tracking_base_2='lot')
+ # Young Tom
+ # \ Botox - 4 - p1
+ # \ Old Tom - 1 - p2
+ lot_1 = self.env['stock.production.lot'].create({
+ 'name': 'lot_1',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_1)
+ lot_finished_1 = self.env['stock.production.lot'].create({
+ 'name': 'lot_finished_1',
+ 'product_id': p_final.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.assertEqual(mo.product_qty, 5)
+ mo_form = Form(mo)
+ mo_form.qty_producing = 3.0
+ mo_form.lot_producing_id = lot_finished_1
+ mo = mo_form.save()
+ self.assertEqual(mo.move_raw_ids[1].quantity_done, 12)
+ details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.qty_done = 3
+ ml.lot_id = lot_1
+ details_operation_form.save()
+ action = mo.button_mark_done()
+ backorder = Form(self.env[action['res_model']].with_context(**action['context']))
+ backorder.save().action_backorder()
+
+ lot_2 = self.env['stock.production.lot'].create({
+ 'name': 'lot_2',
+ 'product_id': p2.id,
+ 'company_id': self.env.company.id,
+ })
+
+ self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 4, lot_id=lot_2)
+ lot_finished_2 = self.env['stock.production.lot'].create({
+ 'name': 'lot_finished_2',
+ 'product_id': p_final.id,
+ 'company_id': self.env.company.id,
+ })
+
+ mo = mo.procurement_group_id.mrp_production_ids[1]
+ # FIXME sle: issue in backorder?
+ mo.move_raw_ids.move_line_ids.unlink()
+ self.assertEqual(mo.product_qty, 2)
+ mo_form = Form(mo)
+ mo_form.qty_producing = 2
+ mo_form.lot_producing_id = lot_finished_2
+ mo = mo_form.save()
+ details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
+ with details_operation_form.move_line_ids.new() as ml:
+ ml.qty_done = 2
+ ml.lot_id = lot_2
+ details_operation_form.save()
+ action = mo.button_mark_done()
+
+ mo1 = mo.procurement_group_id.mrp_production_ids[0]
+ ml = mo1.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_1 in m.produce_line_ids.lot_id)
+ self.assertEqual(sum(ml.mapped('qty_done')), 12.0, 'Should have consumed 12 for the first lot')
+ ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_2 in m.produce_line_ids.lot_id)
+ self.assertEqual(sum(ml.mapped('qty_done')), 8.0, 'Should have consumed 8 for the second lot')
+
+ def test_unbuild_with_routes(self):
+ """ This test creates a MO of a stockable product (Table). A new route for rule QC/Unbuild -> Stock
+ is created with Warehouse -> True.
+ The unbuild order should revert the consumed components into QC/Unbuild location for quality check
+ and then a picking should be generated for transferring components from QC/Unbuild location to stock.
+ """
+ StockQuant = self.env['stock.quant']
+ ProductObj = self.env['product.product']
+ # Create new QC/Unbuild location
+ warehouse = self.env.ref('stock.warehouse0')
+ unbuild_location = self.env['stock.location'].create({
+ 'name': 'QC/Unbuild',
+ 'usage': 'internal',
+ 'location_id': warehouse.view_location_id.id
+ })
+
+ # Create a product route containing a stock rule that will move product from QC/Unbuild location to stock
+ product_route = self.env['stock.location.route'].create({
+ 'name': 'QC/Unbuild -> Stock',
+ 'warehouse_selectable': True,
+ 'warehouse_ids': [(4, warehouse.id)],
+ 'rule_ids': [(0, 0, {
+ 'name': 'Send Matrial QC/Unbuild -> Stock',
+ 'action': 'push',
+ 'picking_type_id': self.ref('stock.picking_type_internal'),
+ 'location_src_id': unbuild_location.id,
+ 'location_id': self.stock_location.id,
+ })],
+ })
+
+ # Create a stockable product and its components
+ finshed_product = ProductObj.create({
+ 'name': 'Table',
+ 'type': 'product',
+ })
+ component1 = ProductObj.create({
+ 'name': 'Table head',
+ 'type': 'product',
+ })
+ component2 = ProductObj.create({
+ 'name': 'Table stand',
+ 'type': 'product',
+ })
+
+ # Create bom and add components
+ bom = self.env['mrp.bom'].create({
+ 'product_id': finshed_product.id,
+ 'product_tmpl_id': finshed_product.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1.0,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': component1.id, 'product_qty': 1}),
+ (0, 0, {'product_id': component2.id, 'product_qty': 1})
+ ]})
+
+ # Set on hand quantity
+ StockQuant._update_available_quantity(component1, self.stock_location, 1)
+ StockQuant._update_available_quantity(component2, self.stock_location, 1)
+
+ # Create mo
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = finshed_product
+ mo_form.bom_id = bom
+ mo_form.product_uom_id = finshed_product.uom_id
+ mo_form.product_qty = 1.0
+ mo = mo_form.save()
+ self.assertEqual(len(mo), 1, 'MO should have been created')
+ mo.action_confirm()
+ mo.action_assign()
+
+ # Produce the final product
+ mo_form = Form(mo)
+ mo_form.qty_producing = 1.0
+ produce_wizard = mo_form.save()
+
+ mo.button_mark_done()
+ self.assertEqual(mo.state, 'done', "Production order should be in done state.")
+
+ # Check quantity in stock before unbuild
+ self.assertEqual(StockQuant._get_available_quantity(finshed_product, self.stock_location), 1, 'Table should be available in stock')
+ self.assertEqual(StockQuant._get_available_quantity(component1, self.stock_location), 0, 'Table head should not be available in stock')
+ self.assertEqual(StockQuant._get_available_quantity(component2, self.stock_location), 0, 'Table stand should not be available in stock')
+
+ # ---------------------------------------------------
+ # Unbuild
+ # ---------------------------------------------------
+
+ # Create an unbuild order of the finished product and set the destination loacation = QC/Unbuild
+ x = Form(self.env['mrp.unbuild'])
+ x.product_id = finshed_product
+ x.bom_id = bom
+ x.mo_id = mo
+ x.product_qty = 1
+ x.location_id = self.stock_location
+ x.location_dest_id = unbuild_location
+ x.save().action_unbuild()
+
+ # Check the available quantity of components and final product in stock
+ self.assertEqual(StockQuant._get_available_quantity(finshed_product, self.stock_location), 0, 'Table should not be available in stock as it is unbuild')
+ self.assertEqual(StockQuant._get_available_quantity(component1, self.stock_location), 0, 'Table head should not be available in stock as it is in QC/Unbuild location')
+ self.assertEqual(StockQuant._get_available_quantity(component2, self.stock_location), 0, 'Table stand should not be available in stock as it is in QC/Unbuild location')
+
+ # Find new generated picking
+ picking = self.env['stock.picking'].search([('product_id', 'in', [component1.id, component2.id])])
+ self.assertEqual(picking.location_id.id, unbuild_location.id, 'Wrong source location in picking')
+ self.assertEqual(picking.location_dest_id.id, self.stock_location.id, 'Wrong destination location in picking')
+
+ # Transfer it
+ for ml in picking.move_ids_without_package:
+ ml.quantity_done = 1
+ picking._action_done()
+
+ # Check the available quantity of components and final product in stock
+ self.assertEqual(StockQuant._get_available_quantity(finshed_product, self.stock_location), 0, 'Table should not be available in stock')
+ self.assertEqual(StockQuant._get_available_quantity(component1, self.stock_location), 1, 'Table head should be available in stock as the picking is transferred')
+ self.assertEqual(StockQuant._get_available_quantity(component2, self.stock_location), 1, 'Table stand should be available in stock as the picking is transferred')
diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py
new file mode 100644
index 00000000..1257b5e4
--- /dev/null
+++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py
@@ -0,0 +1,507 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import Form
+from odoo.addons.mrp.tests.common import TestMrpCommon
+
+
+class TestMultistepManufacturingWarehouse(TestMrpCommon):
+
+ def setUp(self):
+ super(TestMultistepManufacturingWarehouse, self).setUp()
+ # Create warehouse
+ self.customer_location = self.env['ir.model.data'].xmlid_to_res_id('stock.stock_location_customers')
+ warehouse_form = Form(self.env['stock.warehouse'])
+ warehouse_form.name = 'Test Warehouse'
+ warehouse_form.code = 'TWH'
+ self.warehouse = warehouse_form.save()
+
+ self.uom_unit = self.env.ref('uom.product_uom_unit')
+
+ # Create manufactured product
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Stick'
+ product_form.uom_id = self.uom_unit
+ product_form.uom_po_id = self.uom_unit
+ product_form.type = 'product'
+ product_form.route_ids.clear()
+ product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
+ product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
+ self.finished_product = product_form.save()
+
+ # Create raw product for manufactured product
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Raw Stick'
+ product_form.type = 'product'
+ product_form.uom_id = self.uom_unit
+ product_form.uom_po_id = self.uom_unit
+ self.raw_product = product_form.save()
+
+ # Create bom for manufactured product
+ bom_product_form = Form(self.env['mrp.bom'])
+ bom_product_form.product_id = self.finished_product
+ bom_product_form.product_tmpl_id = self.finished_product.product_tmpl_id
+ bom_product_form.product_qty = 1.0
+ bom_product_form.type = 'normal'
+ with bom_product_form.bom_line_ids.new() as bom_line:
+ bom_line.product_id = self.raw_product
+ bom_line.product_qty = 2.0
+
+ self.bom = bom_product_form.save()
+
+ def _check_location_and_routes(self):
+ # Check manufacturing pull rule.
+ self.assertTrue(self.warehouse.manufacture_pull_id)
+ self.assertTrue(self.warehouse.manufacture_pull_id.active, self.warehouse.manufacture_to_resupply)
+ self.assertTrue(self.warehouse.manufacture_pull_id.route_id)
+ # Check new routes created or not.
+ self.assertTrue(self.warehouse.pbm_route_id)
+ # Check location should be created and linked to warehouse.
+ self.assertTrue(self.warehouse.pbm_loc_id)
+ self.assertEqual(self.warehouse.pbm_loc_id.active, self.warehouse.manufacture_steps != 'mrp_one_step', "Input location must be de-active for single step only.")
+ self.assertTrue(self.warehouse.manu_type_id.active)
+
+ def test_00_create_warehouse(self):
+ """ Warehouse testing for direct manufacturing """
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'mrp_one_step'
+ self._check_location_and_routes()
+ # Check locations of existing pull rule
+ self.assertFalse(self.warehouse.pbm_route_id.rule_ids, 'only the update of global manufacture route should happen.')
+ self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.lot_stock_id.id)
+
+ def test_01_warehouse_twostep_manufacturing(self):
+ """ Warehouse testing for picking before manufacturing """
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm'
+ self._check_location_and_routes()
+ self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 2)
+ self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.lot_stock_id.id)
+
+ def test_02_warehouse_twostep_manufacturing(self):
+ """ Warehouse testing for picking ans store after manufacturing """
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm_sam'
+ self._check_location_and_routes()
+ self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 3)
+ self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.sam_loc_id.id)
+
+ def test_manufacturing_3_steps(self):
+ """ Test MO/picking before manufacturing/picking after manufacturing
+ components and move_orig/move_dest. Ensure that everything is created
+ correctly.
+ """
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm_sam'
+
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.finished_product
+ production_form.picking_type_id = self.warehouse.manu_type_id
+ production = production_form.save()
+ production.action_confirm()
+
+ move_raw_ids = production.move_raw_ids
+ self.assertEqual(len(move_raw_ids), 1)
+ self.assertEqual(move_raw_ids.product_id, self.raw_product)
+ self.assertEqual(move_raw_ids.picking_type_id, self.warehouse.manu_type_id)
+ pbm_move = move_raw_ids.move_orig_ids
+ self.assertEqual(len(pbm_move), 1)
+ self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
+ self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
+ self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
+ self.assertFalse(pbm_move.move_orig_ids)
+
+ move_finished_ids = production.move_finished_ids
+ self.assertEqual(len(move_finished_ids), 1)
+ self.assertEqual(move_finished_ids.product_id, self.finished_product)
+ self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
+ sam_move = move_finished_ids.move_dest_ids
+ self.assertEqual(len(sam_move), 1)
+ self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
+ self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
+ self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
+ self.assertFalse(sam_move.move_dest_ids)
+
+ def test_manufacturing_flow(self):
+ """ Simulate a pick pack ship delivery combined with a picking before
+ manufacturing and store after manufacturing. Also ensure that the MO and
+ the moves to stock are created with the generic pull rules.
+ In order to trigger the rule we create a picking to the customer with
+ the 'make to order' procure method
+ """
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm_sam'
+ warehouse.delivery_steps = 'pick_pack_ship'
+ self.warehouse.flush()
+ self.env.ref('stock.route_warehouse0_mto').active = True
+ self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
+ picking_customer = self.env['stock.picking'].create({
+ 'location_id': self.warehouse.wh_output_stock_loc_id.id,
+ 'location_dest_id': self.customer_location,
+ 'partner_id': self.env['ir.model.data'].xmlid_to_res_id('base.res_partner_4'),
+ 'picking_type_id': self.warehouse.out_type_id.id,
+ })
+ self.env['stock.move'].create({
+ 'name': self.finished_product.name,
+ 'product_id': self.finished_product.id,
+ 'product_uom_qty': 2,
+ 'product_uom': self.uom_unit.id,
+ 'picking_id': picking_customer.id,
+ 'location_id': self.warehouse.wh_output_stock_loc_id.id,
+ 'location_dest_id': self.customer_location,
+ 'procure_method': 'make_to_order',
+ 'origin': 'SOURCEDOCUMENT',
+ 'state': 'draft',
+ })
+ picking_customer.action_confirm()
+ production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
+ self.assertTrue(production_order)
+ self.assertEqual(production_order.origin, 'SOURCEDOCUMENT', 'The MO origin should be the SO name')
+ self.assertNotEqual(production_order.name, 'SOURCEDOCUMENT', 'The MO name should not be the origin of the move')
+
+ picking_stock_preprod = self.env['stock.move'].search([
+ ('product_id', '=', self.raw_product.id),
+ ('location_id', '=', self.warehouse.lot_stock_id.id),
+ ('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
+ ('picking_type_id', '=', self.warehouse.pbm_type_id.id)
+ ]).picking_id
+ picking_stock_postprod = self.env['stock.move'].search([
+ ('product_id', '=', self.finished_product.id),
+ ('location_id', '=', self.warehouse.sam_loc_id.id),
+ ('location_dest_id', '=', self.warehouse.lot_stock_id.id),
+ ('picking_type_id', '=', self.warehouse.sam_type_id.id)
+ ]).picking_id
+
+ self.assertTrue(picking_stock_preprod)
+ self.assertTrue(picking_stock_postprod)
+ self.assertEqual(picking_stock_preprod.state, 'confirmed')
+ self.assertEqual(picking_stock_postprod.state, 'waiting')
+ self.assertEqual(picking_stock_preprod.origin, production_order.name, 'The pre-prod origin should be the MO name')
+ self.assertEqual(picking_stock_postprod.origin, 'SOURCEDOCUMENT', 'The post-prod origin should be the SO name')
+
+ picking_stock_preprod.action_assign()
+ picking_stock_preprod.move_line_ids.qty_done = 4
+ picking_stock_preprod._action_done()
+
+ self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.lot_stock_id).mapped('quantity')))
+ self.assertTrue(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id))
+
+ production_order.action_assign()
+ self.assertEqual(production_order.reservation_state, 'assigned')
+ self.assertEqual(picking_stock_postprod.state, 'waiting')
+
+ produce_form = Form(production_order)
+ produce_form.qty_producing = production_order.product_qty
+ production_order = produce_form.save()
+ production_order.button_mark_done()
+
+ self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id).mapped('quantity')))
+
+ self.assertEqual(picking_stock_postprod.state, 'assigned')
+
+ picking_stock_pick = self.env['stock.move'].search([
+ ('product_id', '=', self.finished_product.id),
+ ('location_id', '=', self.warehouse.lot_stock_id.id),
+ ('location_dest_id', '=', self.warehouse.wh_pack_stock_loc_id.id),
+ ('picking_type_id', '=', self.warehouse.pick_type_id.id)
+ ]).picking_id
+ self.assertEqual(picking_stock_pick.move_lines.move_orig_ids.picking_id, picking_stock_postprod)
+
+ def test_cancel_propagation(self):
+ """ Test cancelling moves in a 'picking before
+ manufacturing' and 'store after manufacturing' process. The propagation of
+ cancel depends on the default values on each rule of the chain.
+ """
+ self.warehouse.manufacture_steps = 'pbm_sam'
+ self.warehouse.flush()
+ self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
+ picking_customer = self.env['stock.picking'].create({
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'location_dest_id': self.customer_location,
+ 'partner_id': self.env['ir.model.data'].xmlid_to_res_id('base.res_partner_4'),
+ 'picking_type_id': self.warehouse.out_type_id.id,
+ })
+ self.env['stock.move'].create({
+ 'name': self.finished_product.name,
+ 'product_id': self.finished_product.id,
+ 'product_uom_qty': 2,
+ 'picking_id': picking_customer.id,
+ 'product_uom': self.uom_unit.id,
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'location_dest_id': self.customer_location,
+ 'procure_method': 'make_to_order',
+ })
+ picking_customer.action_confirm()
+ production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
+ self.assertTrue(production_order)
+
+ move_stock_preprod = self.env['stock.move'].search([
+ ('product_id', '=', self.raw_product.id),
+ ('location_id', '=', self.warehouse.lot_stock_id.id),
+ ('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
+ ('picking_type_id', '=', self.warehouse.pbm_type_id.id)
+ ])
+ move_stock_postprod = self.env['stock.move'].search([
+ ('product_id', '=', self.finished_product.id),
+ ('location_id', '=', self.warehouse.sam_loc_id.id),
+ ('location_dest_id', '=', self.warehouse.lot_stock_id.id),
+ ('picking_type_id', '=', self.warehouse.sam_type_id.id)
+ ])
+
+ self.assertTrue(move_stock_preprod)
+ self.assertTrue(move_stock_postprod)
+ self.assertEqual(move_stock_preprod.state, 'confirmed')
+ self.assertEqual(move_stock_postprod.state, 'waiting')
+
+ move_stock_preprod._action_cancel()
+ self.assertEqual(production_order.state, 'confirmed')
+ production_order.action_cancel()
+ self.assertTrue(move_stock_postprod.state, 'cancel')
+
+ def test_no_initial_demand(self):
+ """ Test MO/picking before manufacturing/picking after manufacturing
+ components and move_orig/move_dest. Ensure that everything is created
+ correctly.
+ """
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm_sam'
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.finished_product
+ production_form.picking_type_id = self.warehouse.manu_type_id
+ production = production_form.save()
+ production.move_raw_ids.product_uom_qty = 0
+ production.action_confirm()
+ production.action_assign()
+ self.assertFalse(production.move_raw_ids.move_orig_ids)
+ self.assertEqual(production.state, 'confirmed')
+ self.assertEqual(production.reservation_state, 'assigned')
+
+ def test_manufacturing_3_steps_flexible(self):
+ """ Test MO/picking before manufacturing/picking after manufacturing
+ components and move_orig/move_dest. Ensure that additional moves are put
+ in picking before manufacturing too.
+ """
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm_sam'
+ bom = self.env['mrp.bom'].search([
+ ('product_id', '=', self.finished_product.id)
+ ])
+ new_product = self.env['product.product'].create({
+ 'name': 'New product',
+ 'type': 'product',
+ })
+ bom.consumption = 'flexible'
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.finished_product
+ production_form.picking_type_id = self.warehouse.manu_type_id
+ production = production_form.save()
+
+ production.action_confirm()
+
+ production_form = Form(production)
+ with production_form.move_raw_ids.new() as move:
+ move.product_id = new_product
+ move.product_uom_qty = 2
+ production = production_form.save()
+ move_raw_ids = production.move_raw_ids
+ self.assertEqual(len(move_raw_ids), 2)
+ pbm_move = move_raw_ids.move_orig_ids
+ self.assertEqual(len(pbm_move), 2)
+ self.assertTrue(new_product in pbm_move.product_id)
+
+ def test_manufacturing_complex_product_3_steps(self):
+ """ Test MO/picking after manufacturing a complex product which uses
+ manufactured components. Ensure that everything is created and picked
+ correctly.
+ """
+
+ self.warehouse.mto_pull_id.route_id.active = True
+ # Creating complex product which trigger another manifacture
+
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Arrow'
+ product_form.type = 'product'
+ product_form.route_ids.clear()
+ product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
+ product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
+ self.complex_product = product_form.save()
+
+ ## Create raw product for manufactured product
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Raw Iron'
+ product_form.type = 'product'
+ product_form.uom_id = self.uom_unit
+ product_form.uom_po_id = self.uom_unit
+ self.raw_product_2 = product_form.save()
+
+ with Form(self.finished_product) as finished_product:
+ finished_product.route_ids.clear()
+ finished_product.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
+ finished_product.route_ids.add(self.warehouse.mto_pull_id.route_id)
+
+ ## Create bom for manufactured product
+ bom_product_form = Form(self.env['mrp.bom'])
+ bom_product_form.product_id = self.complex_product
+ bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id
+ with bom_product_form.bom_line_ids.new() as line:
+ line.product_id = self.finished_product
+ line.product_qty = 1.0
+ with bom_product_form.bom_line_ids.new() as line:
+ line.product_id = self.raw_product_2
+ line.product_qty = 1.0
+
+ self.complex_bom = bom_product_form.save()
+
+ with Form(self.warehouse) as warehouse:
+ warehouse.manufacture_steps = 'pbm_sam'
+
+ production_form = Form(self.env['mrp.production'])
+ production_form.product_id = self.complex_product
+ production_form.picking_type_id = self.warehouse.manu_type_id
+ production = production_form.save()
+ production.action_confirm()
+
+ move_raw_ids = production.move_raw_ids
+ self.assertEqual(len(move_raw_ids), 2)
+ sfp_move_raw_id, raw_move_raw_id = move_raw_ids
+ self.assertEqual(sfp_move_raw_id.product_id, self.finished_product)
+ self.assertEqual(raw_move_raw_id.product_id, self.raw_product_2)
+
+ for move_raw_id in move_raw_ids:
+ self.assertEqual(move_raw_id.picking_type_id, self.warehouse.manu_type_id)
+
+ pbm_move = move_raw_id.move_orig_ids
+ self.assertEqual(len(pbm_move), 1)
+ self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id)
+ self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id)
+ self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id)
+
+ # Check move locations
+ move_finished_ids = production.move_finished_ids
+ self.assertEqual(len(move_finished_ids), 1)
+ self.assertEqual(move_finished_ids.product_id, self.complex_product)
+ self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id)
+ sam_move = move_finished_ids.move_dest_ids
+ self.assertEqual(len(sam_move), 1)
+ self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id)
+ self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id)
+ self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id)
+ self.assertFalse(sam_move.move_dest_ids)
+
+ subproduction = self.env['mrp.production'].browse(production.id+1)
+ sfp_pickings = subproduction.picking_ids.sorted('id')
+
+ # SFP Production: 2 pickings, 1 group
+ self.assertEqual(len(sfp_pickings), 2)
+ self.assertEqual(sfp_pickings.mapped('group_id'), subproduction.procurement_group_id)
+
+ ## Move Raw Stick - Stock -> Preprocessing
+ picking = sfp_pickings[0]
+ self.assertEqual(len(picking.move_lines), 1)
+ picking.move_lines[0].product_id = self.raw_product
+
+ ## Move SFP - PostProcessing -> Stock
+ picking = sfp_pickings[1]
+ self.assertEqual(len(picking.move_lines), 1)
+ picking.move_lines[0].product_id = self.finished_product
+
+ # Main production 2 pickings, 1 group
+ pickings = production.picking_ids.sorted('id')
+ self.assertEqual(len(pickings), 2)
+ self.assertEqual(pickings.mapped('group_id'), production.procurement_group_id)
+
+ ## Move 2 components Stock -> Preprocessing
+ picking = pickings[0]
+ self.assertEqual(len(picking.move_lines), 2)
+ picking.move_lines[0].product_id = self.finished_product
+ picking.move_lines[1].product_id = self.raw_product_2
+
+ ## Move FP PostProcessing -> Stock
+ picking = pickings[1]
+ self.assertEqual(len(picking.move_lines), 1)
+ picking.product_id = self.complex_product
+
+ def test_3_steps_and_byproduct(self):
+ """ Suppose a warehouse with Manufacture option set to '3 setps' and a product P01 with a reordering rule.
+ Suppose P01 has a BoM and this BoM mentions that when some P01 are produced, some P02 are produced too.
+ This test ensures that when a MO is generated thanks to the reordering rule, 2 pickings are also
+ generated:
+ - One to bring the components
+ - Another to return the P01 and P02 produced
+ """
+ warehouse = self.warehouse
+ warehouse.manufacture_steps = 'pbm_sam'
+ warehouse_stock_location = warehouse.lot_stock_id
+ pre_production_location = warehouse.pbm_loc_id
+ post_production_location = warehouse.sam_loc_id
+
+ one_unit_uom = self.env['ir.model.data'].xmlid_to_object('uom.product_uom_unit')
+ [two_units_uom, four_units_uom] = self.env['uom.uom'].create([{
+ 'name': 'x%s' % i,
+ 'category_id': self.ref('uom.product_uom_categ_unit'),
+ 'uom_type': 'bigger',
+ 'factor_inv': i,
+ } for i in [2, 4]])
+
+ finished_product = self.env['product.product'].create({
+ 'name': 'Super Product',
+ 'route_ids': [(4, self.ref('mrp.route_warehouse0_manufacture'))],
+ 'type': 'product',
+ })
+ secondary_product = self.env['product.product'].create({
+ 'name': 'Secondary',
+ 'type': 'product',
+ })
+ component = self.env['product.product'].create({
+ 'name': 'Component',
+ 'type': 'consu',
+ })
+
+ bom = self.env['mrp.bom'].create({
+ 'product_tmpl_id': finished_product.product_tmpl_id.id,
+ 'product_qty': 1,
+ 'product_uom_id': two_units_uom.id,
+ 'bom_line_ids': [(0, 0, {
+ 'product_id': component.id,
+ 'product_qty': 1,
+ 'product_uom_id': one_unit_uom.id,
+ })],
+ 'byproduct_ids': [(0, 0, {
+ 'product_id': secondary_product.id,
+ 'product_qty': 1,
+ 'product_uom_id': four_units_uom.id,
+ })],
+ })
+
+ orderpoint = self.env['stock.warehouse.orderpoint'].create({
+ 'warehouse_id': warehouse.id,
+ 'location_id': warehouse_stock_location.id,
+ 'product_id': finished_product.id,
+ 'product_min_qty': 2,
+ 'product_max_qty': 2,
+ })
+
+ self.env['procurement.group'].run_scheduler()
+ mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)])
+ pickings = mo.picking_ids
+ self.assertEqual(len(pickings), 2)
+
+ preprod_picking = pickings[0] if pickings[0].location_id == warehouse_stock_location else pickings[1]
+ self.assertEqual(preprod_picking.location_id, warehouse_stock_location)
+ self.assertEqual(preprod_picking.location_dest_id, pre_production_location)
+
+ postprod_picking = pickings - preprod_picking
+ self.assertEqual(postprod_picking.location_id, post_production_location)
+ self.assertEqual(postprod_picking.location_dest_id, warehouse_stock_location)
+
+ postprod_SML = postprod_picking.move_lines
+ self.assertEqual(len(postprod_SML), 2)
+ self.assertEqual(postprod_SML.location_id, post_production_location)
+ self.assertEqual(postprod_SML.location_dest_id, warehouse_stock_location)
+
+ finished_product_SML = postprod_SML[0] if postprod_SML[0].product_id == finished_product else postprod_SML[1]
+ secondary_product_SML = postprod_SML - finished_product_SML
+ self.assertEqual(finished_product_SML.product_uom.id, one_unit_uom.id)
+ self.assertEqual(finished_product_SML.product_uom_qty, 2)
+ self.assertEqual(secondary_product_SML.product_uom.id, four_units_uom.id)
+ self.assertEqual(secondary_product_SML.product_uom_qty, 1)