# -*- coding: utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. from datetime import datetime, timedelta from flectra.fields import Datetime as Dt from flectra.addons.mrp.tests.common import TestMrpCommon class TestMrpOrder(TestMrpCommon): def test_access_rights_manager(self): man_order = self.env['mrp.production'].sudo(self.user_mrp_manager).create({ 'name': 'Stick-0', 'product_id': self.product_4.id, 'product_uom_id': self.product_4.uom_id.id, 'product_qty': 5.0, 'bom_id': self.bom_1.id, 'location_src_id': self.location_1.id, 'location_dest_id': self.warehouse_1.wh_output_stock_loc_id.id, }) 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): man_order = self.env['mrp.production'].sudo(self.user_mrp_user).create({ 'name': 'Stick-0', 'product_id': self.product_4.id, 'product_uom_id': self.product_4.uom_id.id, 'product_qty': 5.0, 'bom_id': self.bom_1.id, 'location_src_id': self.location_1.id, 'location_dest_id': self.warehouse_1.wh_output_stock_loc_id.id, }) man_order.action_cancel() self.assertEqual(man_order.state, 'cancel', "Production order should be in cancel state.") man_order.unlink() def test_basic(self): """ Basic order test: no routing (thus no workorders), no lot """ self.product_1.type = 'product' self.product_2.type = 'product' inventory = self.env['stock.inventory'].create({ 'name': 'Initial inventory', 'filter': 'partial', '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_done() test_date_planned = datetime.now() - timedelta(days=1) test_quantity = 2.0 self.bom_1.routing_id = False man_order = self.env['mrp.production'].sudo(self.user_mrp_user).create({ 'name': 'Stick-0', 'product_id': self.product_4.id, 'product_uom_id': self.product_4.uom_id.id, 'product_qty': test_quantity, 'bom_id': self.bom_1.id, 'date_planned_start': test_date_planned, 'location_src_id': self.location_1.id, 'location_dest_id': self.warehouse_1.wh_output_stock_loc_id.id, }) self.assertEqual(man_order.state, 'confirmed', "Production order should be in confirmed state.") # check production move production_move = man_order.move_finished_ids self.assertEqual(production_move.date, Dt.to_string(test_date_planned)) 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, Dt.to_string(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) # waste some material, create a scrap # scrap = self.env['stock.scrap'].with_context( # active_model='mrp.production', active_id=man_order.id # ).create({}) # scrap = self.env['stock.scrap'].create({ # 'production_id': man_order.id, # 'product_id': first_move.product_id.id, # 'product_uom_id': first_move.product_uom.id, # 'scrap_qty': 5.0, # }) # check created scrap # procurements = self.env['procurement.order'].search([('move_dest_id', 'in', man_order.move_raw_ids.ids)]) # print procurements # procurements = self.env['procurement.order'].search([('production_id', '=', man_order.id)]) # print procurements # for proc in self.env['procurement.order'].browse(procurements): # date_planned = self.mrp_production_test1.date_planned # if proc.product_id.type not in ('product', 'consu'): # continue # if proc.product_id.id == order_line.product_id.id: # self.assertEqual(proc.date_planned, date_planned, "Planned date does not correspond") # # procurement state should be `confirmed` at this stage, except if procurement_jit is installed, in which # # case it could already be in `running` or `exception` state (not enough stock) # expected_states = ('confirmed', 'running', 'exception') # self.assertEqual(proc.state in expected_states, 'Procurement state is `%s` for %s, expected one of %s' % (proc.state, proc.product_id.name, expected_states)) # Change production quantity qty_wizard = self.env['change.production.qty'].create({ 'mo_id': man_order.id, 'product_qty': 3.0, }) # qty_wizard.change_prod_qty() # # I check qty after changed in production order. # #self.assertEqual(self.mrp_production_test1.product_qty, 3, "Qty is not changed in order.") # move = self.mrp_production_test1.move_finished_ids[0] # self.assertEqual(move.product_qty, self.mrp_production_test1.product_qty, "Qty is not changed in move line.") # # I run scheduler. # self.env['procurement.order'].run_scheduler() # # The production order is Waiting Goods, will force production which should set consume lines as available # self.mrp_production_test1.button_plan() # # I check that production order in ready state after forcing production. # #self.assertEqual(self.mrp_production_test1.availability, 'assigned', 'Production order availability should be set as available') # produce product produce_wizard = self.env['mrp.product.produce'].sudo(self.user_mrp_user).with_context({ 'active_id': man_order.id, 'active_ids': [man_order.id], }).create({ 'product_qty': 1.0, }) produce_wizard.do_produce() # man_order.button_mark_done() man_order.button_mark_done() self.assertEqual(man_order.state, 'done', "Production order should be in done state.") def test_explode_from_order(self): # # bom3 produces 2 Dozen of Doors (p6), aka 24 # To produce 24 Units of Doors (p6) # - 2 Units of Tools (p5) -> need 4 # - 8 Dozen of Sticks (p4) -> need 16 # - 12 Units of Wood (p2) -> need 24 # bom2 produces 1 Unit of Sticks (p4) # To produce 1 Unit of Sticks (p4) # - 2 Dozen of Sticks (p4) -> need 8 # - 3 Dozen of Stones (p3) -> need 12 # Update capacity, start time, stop time, and time efficiency. # ------------------------------------------------------------ self.workcenter_1.write({'capacity': 1, 'time_start': 0, 'time_stop': 0, 'time_efficiency': 100}) # Set manual time cycle 20 and 10. # -------------------------------- self.operation_1.write({'time_cycle_manual': 20}) (self.operation_2 | self.operation_3).write({'time_cycle_manual': 10}) man_order = self.env['mrp.production'].create({ 'name': 'MO-Test', 'product_id': self.product_6.id, 'product_uom_id': self.product_6.uom_id.id, 'product_qty': 48, 'bom_id': self.bom_3.id, }) # reset quantities self.product_1.type = "product" self.env['stock.change.product.qty'].create({ 'product_id': self.product_1.id, 'new_quantity': 0.0, 'location_id': self.warehouse_1.lot_stock_id.id, }).change_product_qty() (self.product_2 | self.product_4).write({ 'tracking': 'none', }) # assign consume material man_order.action_assign() self.assertEqual(man_order.availability, 'waiting', "Production order should be in waiting state.") # check consume materials of manufacturing order self.assertEqual(len(man_order.move_raw_ids), 4, "Consume material lines are not generated proper.") product_2_consume_moves = man_order.move_raw_ids.filtered(lambda x: x.product_id == self.product_2) product_3_consume_moves = man_order.move_raw_ids.filtered(lambda x: x.product_id == self.product_3) product_4_consume_moves = man_order.move_raw_ids.filtered(lambda x: x.product_id == self.product_4) product_5_consume_moves = man_order.move_raw_ids.filtered(lambda x: x.product_id == self.product_5) consume_qty_2 = product_2_consume_moves.product_uom_qty self.assertEqual(consume_qty_2, 24.0, "Consume material quantity of Wood should be 24 instead of %s" % str(consume_qty_2)) consume_qty_3 = product_3_consume_moves.product_uom_qty self.assertEqual(consume_qty_3, 12.0, "Consume material quantity of Stone should be 12 instead of %s" % str(consume_qty_3)) self.assertEqual(len(product_4_consume_moves), 2, "Consume move are not generated proper.") for consume_moves in product_4_consume_moves: consume_qty_4 = consume_moves.product_uom_qty self.assertIn(consume_qty_4, [8.0, 16.0], "Consume material quantity of Stick should be 8 or 16 instead of %s" % str(consume_qty_4)) self.assertFalse(product_5_consume_moves, "Move should not create for phantom bom") # create required lots lot_product_2 = self.env['stock.production.lot'].create({'product_id': self.product_2.id}) lot_product_4 = self.env['stock.production.lot'].create({'product_id': self.product_4.id}) # refuel stock inventory = self.env['stock.inventory'].create({ 'name': 'Inventory For Product C', 'filter': 'partial', 'line_ids': [(0, 0, { 'product_id': self.product_2.id, 'product_uom_id': self.product_2.uom_id.id, 'product_qty': 30, 'prod_lot_id': lot_product_2.id, 'location_id': self.ref('stock.stock_location_14') }), (0, 0, { 'product_id': self.product_3.id, 'product_uom_id': self.product_3.uom_id.id, 'product_qty': 60, 'location_id': self.ref('stock.stock_location_14') }), (0, 0, { 'product_id': self.product_4.id, 'product_uom_id': self.product_4.uom_id.id, 'product_qty': 60, 'prod_lot_id': lot_product_4.id, 'location_id': self.ref('stock.stock_location_14') })] }) inventory.action_start() inventory.action_done() # re-assign consume material man_order.action_assign() # Check production order status after assign. self.assertEqual(man_order.availability, 'assigned', "Production order should be in assigned state.") # Plan production order. man_order.button_plan() # check workorders # - main bom: Door: 2 operations # operation 1: Cutting # operation 2: Welding, waiting for the previous one # - kit bom: Stone Tool: 1 operation # operation 1: Gift Wrapping workorders = man_order.workorder_ids kit_wo = man_order.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_1) door_wo_1 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_2) door_wo_2 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_3) for workorder in workorders: self.assertEqual(workorder.workcenter_id, self.workcenter_1, "Workcenter does not match.") self.assertEqual(kit_wo.state, 'ready', "Workorder should be in ready state.") self.assertEqual(door_wo_1.state, 'ready', "Workorder should be in ready state.") self.assertEqual(door_wo_2.state, 'pending', "Workorder should be in pending state.") self.assertEqual(kit_wo.duration_expected, 80, "Workorder duration should be 80 instead of %s." % str(kit_wo.duration_expected)) self.assertEqual(door_wo_1.duration_expected, 20, "Workorder duration should be 20 instead of %s." % str(door_wo_1.duration_expected)) self.assertEqual(door_wo_2.duration_expected, 20, "Workorder duration should be 20 instead of %s." % str(door_wo_2.duration_expected)) # subbom: kit for stone tools kit_wo.button_start() finished_lot = self.env['stock.production.lot'].create({'product_id': man_order.product_id.id}) kit_wo.write({ 'final_lot_id': finished_lot.id, 'qty_producing': 48 }) kit_wo.record_production() self.assertEqual(kit_wo.state, 'done', "Workorder should be in done state.") # first operation of main bom finished_lot = self.env['stock.production.lot'].create({'product_id': man_order.product_id.id}) door_wo_1.write({ 'final_lot_id': finished_lot.id, 'qty_producing': 48 }) door_wo_1.record_production() self.assertEqual(door_wo_1.state, 'done', "Workorder should be in done state.") # second operation of main bom self.assertEqual(door_wo_2.state, 'ready', "Workorder should be in ready state.") door_wo_2.record_production() self.assertEqual(door_wo_2.state, 'done', "Workorder should be in done state.") def test_production_avialability(self): """ Test availability of production order. """ 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() production_2 = self.env['mrp.production'].create({ 'name': 'MO-Test001', 'product_id': self.product_6.id, 'product_qty': 5.0, 'bom_id': self.bom_3.id, 'product_uom_id': self.product_6.uom_id.id, }) production_2.action_assign() # check sub product availability state is waiting self.assertEqual(production_2.availability, 'waiting', 'Production order should be availability for waiting state') # Update Inventory inventory_wizard = self.env['stock.change.product.qty'].create({ 'product_id': self.product_2.id, 'new_quantity': 2.0, }) inventory_wizard.change_product_qty() production_2.action_assign() # check sub product availability state is partially available self.assertEqual(production_2.availability, 'partially_available', 'Production order should be availability for partially available state') # Update Inventory inventory_wizard = self.env['stock.change.product.qty'].create({ 'product_id': self.product_2.id, 'new_quantity': 5.0, }) inventory_wizard.change_product_qty() production_2.action_assign() # check sub product availability state is assigned self.assertEqual(production_2.availability, 'assigned', 'Production order should be availability for assigned state') def test_empty_routing(self): """ Check what happens when you work with an empty routing""" routing = self.env['mrp.routing'].create({'name': 'Routing without operations', 'location_id': self.warehouse_1.wh_input_stock_loc_id.id,}) self.bom_3.routing_id = routing.id production = self.env['mrp.production'].create({'name': 'MO test', 'product_id': self.product_6.id, 'product_qty': 3, 'bom_id': self.bom_3.id, 'product_uom_id': self.product_6.uom_id.id,}) self.assertEqual(production.routing_id.id, False, 'The routing field should be empty on the mo') self.assertEqual(production.move_raw_ids[0].location_id.id, self.warehouse_1.wh_input_stock_loc_id.id, 'Raw moves start location should have altered.') def test_multiple_post_inventory(self): """ Check the consumed quants of the produced quants when intermediate calls to `post_inventory` during a MO.""" # create a bom for `custom_laptop` with components that aren't tracked unit = self.ref("product.product_uom_unit") custom_laptop = self.env.ref("product.product_product_27") custom_laptop.tracking = 'none' product_charger = self.env['product.product'].create({ 'name': 'Charger', 'type': 'product', 'uom_id': unit, 'uom_po_id': unit}) product_keybord = self.env['product.product'].create({ 'name': 'Usb Keybord', 'type': 'product', 'uom_id': unit, 'uom_po_id': unit}) bom_custom_laptop = self.env['mrp.bom'].create({ 'product_tmpl_id': custom_laptop.product_tmpl_id.id, 'product_qty': 1, 'product_uom_id': unit, 'bom_line_ids': [(0, 0, { 'product_id': product_charger.id, 'product_qty': 1, 'product_uom_id': unit }), (0, 0, { 'product_id': product_keybord.id, 'product_qty': 1, 'product_uom_id': unit })] }) # put the needed products in stock source_location_id = self.ref('stock.stock_location_14') inventory = self.env['stock.inventory'].create({ 'name': 'Inventory Product Table', 'filter': 'partial', 'line_ids': [(0, 0, { 'product_id': product_charger.id, 'product_uom_id': product_charger.uom_id.id, 'product_qty': 2, 'location_id': source_location_id }), (0, 0, { 'product_id': product_keybord.id, 'product_uom_id': product_keybord.uom_id.id, 'product_qty': 2, 'location_id': source_location_id })] }) inventory.action_done() # create a mo for this bom mo_custom_laptop = self.env['mrp.production'].create({ 'product_id': custom_laptop.id, 'product_qty': 2, 'product_uom_id': unit, 'bom_id': bom_custom_laptop.id }) mo_custom_laptop.action_assign() self.assertEqual(mo_custom_laptop.availability, 'assigned') # produce one item, call `post_inventory` context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} custom_laptop_produce = self.env['mrp.product.produce'].with_context(context).create({'product_qty': 1.00}) custom_laptop_produce.do_produce() mo_custom_laptop.post_inventory() # check the consumed quants of the produced quant first_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'done') second_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'confirmed') # produce the second item, call `post_inventory` context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} custom_laptop_produce = self.env['mrp.product.produce'].with_context(context).create({'product_qty': 1.00}) custom_laptop_produce.do_produce() mo_custom_laptop.post_inventory() def test_rounding(self): """ In previous versions we had rounding and efficiency fields. We check if we can still do the same, but with only the rounding on the UoM""" 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 = self.env['mrp.production'].create({'name': 'MO efficiency test', 'product_id': self.product_6.id, 'product_qty': 20, 'bom_id': bom_eff.id, 'product_uom_id': self.product_6.uom_id.id,}) #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 produce_wizard = self.env['mrp.product.produce'].with_context({ 'active_id': production.id, 'active_ids': [production.id], }).create({ 'product_qty': 8, }) produce_wizard.do_produce() 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): """ Check that no produce line are created when the consumed products are not tracked """ 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() product_produce = self.env['mrp.product.produce'].with_context({ 'active_id': mo.id, 'active_ids': [mo.id], }).create({}) self.assertEqual(len(product_produce.produce_line_ids), 0, 'You should not have any produce lines since the consumed products are not tracked.') def test_product_produce_2(self): """ Check that line are created when the consumed products are tracked by serial and the lot proposed are correct. """ 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, }) lot_p1_2 = self.env['stock.production.lot'].create({ 'name': 'lot2', 'product_id': p1.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() product_produce = self.env['mrp.product.produce'].with_context({ 'active_id': mo.id, 'active_ids': [mo.id], }).create({}) self.assertEqual(len(product_produce.produce_line_ids), 2, 'You should have 2 produce lines. One for each serial to consume') product_produce.product_qty = 1 produce_line_1 = product_produce.produce_line_ids[0] produce_line_1.qty_done = 1 remaining_lot = (lot_p1_1 | lot_p1_2) - produce_line_1.lot_id product_produce.do_produce() product_produce = self.env['mrp.product.produce'].with_context({ 'active_id': mo.id, 'active_ids': [mo.id], }).create({}) self.assertEqual(len(product_produce.produce_line_ids), 1, 'You should have 1 produce lines since one has already be consumed.') self.assertEqual(product_produce.produce_line_ids[0].lot_id, remaining_lot, 'Wrong lot proposed.') def test_product_produce_3(self): """ Check that line are created when the consumed products are tracked by serial and the lot proposed are correct. """ self.stock_location = self.env.ref('stock.stock_location_stock') self.stock_shelf_1 = self.env.ref('stock.stock_location_components') self.stock_shelf_2 = self.env.ref('stock.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, }) second_lot_for_p1 = self.env['stock.production.lot'].create({ 'name': 'lot2', 'product_id': p1.id, }) final_product_lot = self.env['stock.production.lot'].create({ 'name': 'lot1', 'product_id': p_final.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() product_produce = self.env['mrp.product.produce'].with_context({ 'active_id': mo.id, 'active_ids': [mo.id], }).create({ 'product_qty': 1.0, 'lot_id': final_product_lot.id, }) # product 1 lot 1 shelf1 # product 1 lot 1 shelf2 # product 1 lot 2 self.assertEqual(len(product_produce.produce_line_ids), 3, 'You should have 3 produce lines. lot 1 shelf_1, lot 1 shelf_2, lot2') for produce_line in product_produce.produce_line_ids: produce_line.qty_done = produce_line.qty_to_consume + 1 product_produce.do_produce() 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.")