738 lines
33 KiB
Python
738 lines
33 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import unittest
|
|
|
|
from odoo import Command, fields
|
|
from odoo.addons.mrp_workorder.tests.common import TestMrpWorkorderCommon
|
|
from odoo.addons.base.tests.common import HttpCase
|
|
from odoo.tests import Form, tagged
|
|
from odoo.tools import mute_logger
|
|
from odoo.exceptions import UserError
|
|
from datetime import timedelta
|
|
from freezegun import freeze_time
|
|
import logging
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
class TestWorkOrder(TestMrpWorkorderCommon):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestWorkOrder, cls).setUpClass()
|
|
cls.env.ref('base.group_user').write({'implied_ids': [
|
|
Command.link(cls.env.ref('mrp.group_mrp_routings').id),
|
|
Command.link(cls.env.ref('stock.group_production_lot').id),
|
|
]})
|
|
# Products and lots
|
|
cls.submarine_pod = cls.env['product.product'].create({
|
|
'name': 'Submarine pod',
|
|
'is_storable': True,
|
|
'tracking': 'serial'})
|
|
cls.sp1 = cls.env['stock.lot'].create({
|
|
'product_id': cls.submarine_pod.id,
|
|
'name': 'sp1',
|
|
})
|
|
cls.sp2 = cls.env['stock.lot'].create({
|
|
'product_id': cls.submarine_pod.id,
|
|
'name': 'sp2',
|
|
})
|
|
cls.sp3 = cls.env['stock.lot'].create({
|
|
'product_id': cls.submarine_pod.id,
|
|
'name': 'sp3',
|
|
})
|
|
cls.elon_musk = cls.env['product.product'].create({
|
|
'name': 'Elon Musk',
|
|
'is_storable': True,
|
|
'tracking': 'serial'})
|
|
cls.elon1 = cls.env['stock.lot'].create({
|
|
'product_id': cls.elon_musk.id,
|
|
'name': 'elon1',
|
|
})
|
|
cls.elon2 = cls.env['stock.lot'].create({
|
|
'product_id': cls.elon_musk.id,
|
|
'name': 'elon2',
|
|
})
|
|
cls.elon3 = cls.env['stock.lot'].create({
|
|
'product_id': cls.elon_musk.id,
|
|
'name': 'elon3',
|
|
})
|
|
cls.metal_cylinder = cls.env['product.product'].create({
|
|
'name': 'Metal cylinder',
|
|
'is_storable': True,
|
|
'tracking': 'lot'})
|
|
cls.mc1 = cls.env['stock.lot'].create({
|
|
'product_id': cls.metal_cylinder.id,
|
|
'name': 'mc1',
|
|
})
|
|
cls.trapped_child = cls.env['product.product'].create({
|
|
'name': 'Trapped child',
|
|
'is_storable': True,
|
|
'tracking': 'none'})
|
|
# Bill of material
|
|
cls.bom_submarine = cls.env['mrp.bom'].create({
|
|
'product_tmpl_id': cls.submarine_pod.product_tmpl_id.id,
|
|
'product_qty': 1.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}),
|
|
]})
|
|
cls.env['mrp.bom.line'].create({
|
|
'product_id': cls.elon_musk.id,
|
|
'product_qty': 1.0,
|
|
'bom_id': cls.bom_submarine.id,
|
|
'operation_id': cls.bom_submarine.operation_ids[1].id})
|
|
cls.env['mrp.bom.line'].create({
|
|
'product_id': cls.trapped_child.id,
|
|
'product_qty': 12.0,
|
|
'bom_id': cls.bom_submarine.id})
|
|
cls.env['mrp.bom.line'].create({
|
|
'product_id': cls.metal_cylinder.id,
|
|
'product_qty': 2.0,
|
|
'bom_id': cls.bom_submarine.id,
|
|
'operation_id': cls.bom_submarine.operation_ids[0].id})
|
|
cls.operation_4 = cls.env['mrp.routing.workcenter'].create({
|
|
'name': 'Rescue operation',
|
|
'workcenter_id': cls.workcenter_1.id,
|
|
'bom_id': cls.bom_submarine.id,
|
|
'time_cycle': 13,
|
|
'sequence': 2})
|
|
#cls.mrp_routing_0 = cls.env['mrp.routing'].create({
|
|
# 'name': 'Primary Assembly',
|
|
#})
|
|
#cls.mrp_routing_1 = cls.env['mrp.routing'].create({
|
|
# 'name': 'Secondary Assembly',
|
|
#})
|
|
cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({
|
|
'name': 'Drill Station 1',
|
|
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
|
})
|
|
cls.mrp_workcenter_3 = cls.env['mrp.workcenter'].create({
|
|
'name': 'Assembly Line 1',
|
|
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
|
})
|
|
# --------------
|
|
# .mrp_routing_0
|
|
# --------------
|
|
# cls.mrp_routing_workcenter_0 = cls.env['mrp.routing.workcenter'].create({
|
|
# #'routing_id': cls.mrp_routing_0.id,
|
|
# 'workcenter_id': cls.mrp_workcenter_3.id,
|
|
# 'name': 'Manual Assembly',
|
|
# 'time_cycle': 60,
|
|
# })
|
|
#
|
|
# # -------------
|
|
# # mpr_routing_1
|
|
# # -------------
|
|
# cls.mrp_routing_workcenter_1 = cls.env['mrp.routing.workcenter'].create({
|
|
# #'routing_id': cls.mrp_routing_1.id,
|
|
# 'workcenter_id': cls.mrp_workcenter_3.id,
|
|
# 'name': 'Long time assembly',
|
|
# 'time_cycle': 180,
|
|
# 'sequence': 15,
|
|
# })
|
|
# cls.mrp_routing_workcenter_3 = cls.env['mrp.routing.workcenter'].create({
|
|
# #'routing_id': cls.mrp_routing_1.id,
|
|
# 'workcenter_id': cls.mrp_workcenter_3.id,
|
|
# 'name': 'Testing',
|
|
# 'time_cycle': 60,
|
|
# 'sequence': 10,
|
|
# })
|
|
# cls.mrp_routing_workcenter_4 = cls.env['mrp.routing.workcenter'].create({
|
|
# #'routing_id': cls.mrp_routing_1.id,
|
|
# 'workcenter_id': cls.mrp_workcenter_1.id,
|
|
# 'name': 'Packing',
|
|
# 'time_cycle': 30,
|
|
# 'sequence': 5,
|
|
# })
|
|
|
|
# Update quantities
|
|
Quant = cls.env['stock.quant']
|
|
Quant._update_available_quantity(cls.elon_musk, cls.stock_location, 1.0, lot_id=cls.elon1)
|
|
Quant._update_available_quantity(cls.elon_musk, cls.stock_location, 1.0, lot_id=cls.elon2)
|
|
Quant._update_available_quantity(cls.elon_musk, cls.stock_location, 1.0, lot_id=cls.elon3)
|
|
Quant._update_available_quantity(cls.metal_cylinder, cls.stock_location, 6.0, lot_id=cls.mc1)
|
|
Quant._update_available_quantity(cls.trapped_child, cls.stock_location, 36.0)
|
|
|
|
def test_assign_1(self):
|
|
unit = self.uom_unit.id
|
|
custom_laptop = self.env['product.product'].create({
|
|
'name': 'Drawer',
|
|
'is_storable': True,
|
|
'uom_id': unit,
|
|
})
|
|
custom_laptop.tracking = 'none'
|
|
product_charger = self.env['product.product'].create({
|
|
'name': 'Charger',
|
|
'is_storable': True,
|
|
'tracking': 'lot',
|
|
'uom_id': unit,
|
|
})
|
|
product_keybord = self.env['product.product'].create({
|
|
'name': 'Usb Keybord',
|
|
'is_storable': True,
|
|
'uom_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,
|
|
#'routing_id': self.mrp_routing_0.id,
|
|
# inlined mrp_routing0
|
|
'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
|
|
})],
|
|
'operation_ids': [(0, 0, {
|
|
'workcenter_id': self.mrp_workcenter_3.id,
|
|
'name': 'Manual Assembly',
|
|
'time_cycle': 60,
|
|
})]
|
|
})
|
|
|
|
production_form = Form(self.env['mrp.production'])
|
|
production_form.product_id = custom_laptop
|
|
production_form.bom_id = bom_custom_laptop
|
|
production_form.product_qty = 2
|
|
production = production_form.save()
|
|
production.action_confirm()
|
|
production.button_plan()
|
|
workorder = production.workorder_ids
|
|
self.assertTrue(workorder)
|
|
|
|
self.env['stock.quant']._update_available_quantity(product_charger, self.stock_location, 5)
|
|
self.env['stock.quant']._update_available_quantity(product_keybord, self.stock_location, 5)
|
|
|
|
production.action_assign()
|
|
|
|
def test_flexible_consumption_2(self):
|
|
""" Production with a flexible consumption
|
|
Check that consuming different quantities than planned doensn't trigger
|
|
any error"""
|
|
self.bom_submarine.consumption = 'flexible'
|
|
|
|
mo_form = Form(self.env['mrp.production'])
|
|
mo_form.product_id = self.submarine_pod
|
|
mo_form.bom_id = self.bom_submarine
|
|
mo_form.product_qty = 1
|
|
mo = mo_form.save()
|
|
|
|
mo.action_confirm()
|
|
mo.action_assign()
|
|
mo.button_plan()
|
|
|
|
sorted_workorder_ids = mo.workorder_ids.sorted()
|
|
wo = sorted_workorder_ids[0]
|
|
wo.button_start()
|
|
wo.finished_lot_ids = self.sp1
|
|
self.assertEqual(wo.move_raw_ids.move_line_ids[0].lot_id, self.mc1, 'The suggested lot is wrong')
|
|
wo.move_raw_ids.move_line_ids[0].quantity = 1
|
|
wo.move_raw_ids.picked = True
|
|
wo.do_finish()
|
|
|
|
wo = sorted_workorder_ids[1]
|
|
wo.button_start()
|
|
self.assertEqual(wo.finished_lot_ids, self.sp1, 'The suggested final product is wrong')
|
|
self.assertEqual(wo.move_raw_ids.move_line_ids[0].lot_id, self.elon1, 'The suggested lot is wrong')
|
|
wo.move_raw_ids.move_line_ids[0].quantity = 1
|
|
wo.move_raw_ids.picked = True
|
|
wo.move_raw_ids.move_line_ids[0].copy({'lot_id': self.elon2.id, 'quantity': 1})
|
|
wo.do_finish()
|
|
|
|
wo = sorted_workorder_ids[2]
|
|
wo.button_start()
|
|
self.assertEqual(wo.finished_lot_ids, self.sp1, 'The suggested final product is wrong')
|
|
wo.do_finish()
|
|
|
|
mo.move_raw_ids.filtered(lambda m: not m.operation_id).picked = True
|
|
mo.button_mark_done()
|
|
move_1 = mo.move_raw_ids.filtered(lambda move: move.product_id == self.metal_cylinder and move.state == 'done')
|
|
self.assertEqual(sum(move_1.mapped('quantity')), 1, 'Only one cylinder was consumed')
|
|
move_2 = mo.move_raw_ids.filtered(lambda move: move.product_id == self.elon_musk and move.state == 'done')
|
|
self.assertEqual(sum(move_2.mapped('quantity')), 2, '2 Elon Musk was consumed')
|
|
move_3 = mo.move_raw_ids.filtered(lambda move: move.product_id == self.trapped_child and move.state == 'done')
|
|
self.assertEqual(sum(move_3.mapped('quantity')), 12, '12 child was consumed')
|
|
self.assertEqual(mo.state, 'done', 'Final state of the MO should be "done"')
|
|
|
|
def test_workorder_1(self):
|
|
# get the computer sc234 demo data
|
|
prod = self.submarine_pod
|
|
bom = self.bom_submarine
|
|
|
|
mrp_order_form = Form(self.env['mrp.production'])
|
|
mrp_order_form.product_id = self.submarine_pod
|
|
mrp_order_form.product_qty = 1
|
|
production = mrp_order_form.save()
|
|
|
|
# plan the work orders
|
|
production.button_plan()
|
|
|
|
def test_suggested_lot_in_multi_step(self):
|
|
"""Suggest the assigned lot in multi step system."""
|
|
self.env['quality.point'].create({
|
|
'product_ids': [Command.link(self.submarine_pod.id)],
|
|
'picking_type_ids': [Command.link(self.picking_type_manu.id)],
|
|
'operation_id': self.bom_submarine.operation_ids[0].id,
|
|
'test_type_id': self.env.ref('mrp_workorder.test_type_register_consumed_materials').id,
|
|
'component_id': self.elon_musk.id,
|
|
})
|
|
self.warehouse_1.manufacture_steps = 'pbm'
|
|
self.submarine_pod.tracking = 'none'
|
|
self.bom_submarine.bom_line_ids.filtered(lambda l: l.product_id.id != self.elon_musk.id).unlink()
|
|
self.bom_submarine.operation_ids[1:].unlink()
|
|
|
|
mrp_order_form = Form(self.env['mrp.production'])
|
|
mrp_order_form.product_id = self.submarine_pod
|
|
mrp_order_form.product_qty = 1
|
|
production = mrp_order_form.save()
|
|
production.action_confirm()
|
|
production.button_plan()
|
|
|
|
production.picking_ids.action_assign()
|
|
production.picking_ids.move_line_ids.lot_id = self.elon2
|
|
production.picking_ids.button_validate()
|
|
|
|
wo = production.workorder_ids
|
|
wo.button_start()
|
|
self.assertEqual(production.move_raw_ids.move_line_ids.lot_id, self.elon2, "Lot should be assigned.")
|
|
self.assertEqual(wo.current_quality_check_id.lot_ids, self.elon2, "Lot should be set in the step")
|
|
|
|
def test_step_by_product_variant(self):
|
|
who_attr = self.env['product.attribute'].create({'name': 'Who?'})
|
|
a1 = self.env['product.attribute.value'].create({'name': 'V0hFCg==', 'attribute_id': who_attr.id})
|
|
a2 = self.env['product.attribute.value'].create({'name': 'QVJN', 'attribute_id': who_attr.id})
|
|
a3 = self.env['product.attribute.value'].create({'name': 'UllW', 'attribute_id': who_attr.id})
|
|
product_who = self.env['product.template'].create({
|
|
'name': 'Odoo staff',
|
|
'attribute_line_ids': [(0, 0, {
|
|
'attribute_id': who_attr.id,
|
|
'value_ids': [(6, 0, [a1.id, a2.id, a3.id])],
|
|
})]
|
|
})
|
|
product_a1 = product_who.product_variant_ids.filtered(lambda v: a1 in v.product_template_attribute_value_ids.product_attribute_value_id)
|
|
product_a2 = product_who.product_variant_ids.filtered(lambda v: a2 in v.product_template_attribute_value_ids.product_attribute_value_id)
|
|
product_a3 = product_who.product_variant_ids.filtered(lambda v: a3 in v.product_template_attribute_value_ids.product_attribute_value_id)
|
|
|
|
bom_who = self.env['mrp.bom'].create({
|
|
'code': 'To be ready',
|
|
'product_tmpl_id': product_who.id,
|
|
'product_qty': 1.0,
|
|
'bom_line_ids': [
|
|
(0, 0, {'product_id': self.trapped_child.id, 'product_qty': 1}),
|
|
],
|
|
'operation_ids': [
|
|
(0, 0, {'name': 'Be ready', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 60, 'sequence': 1}),
|
|
]
|
|
})
|
|
|
|
manufacture_picking_type = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')], limit=1)
|
|
p1 = self.env['quality.point'].create({
|
|
'product_ids': [Command.link(product_a1.id)],
|
|
'picking_type_ids': [Command.link(manufacture_picking_type.id)],
|
|
'operation_id': bom_who.operation_ids[0].id,
|
|
'test_type_id': self.env.ref('quality.test_type_instructions').id,
|
|
'note': 'Installing VIM (pcs xi ipzth adi du ixbt)',
|
|
})
|
|
# Check that the operation is removed when the picking type is updated and none have the mrp_operation code.
|
|
self.assertTrue(p1.operation_id)
|
|
p1.picking_type_ids = self.env['stock.picking.type'].search([('code', '=', 'internal'), ('company_id', '=', self.env.company.id)], limit=1)
|
|
self.assertFalse(p1.operation_id)
|
|
# reset manufacture_picking_type
|
|
p1.picking_type_ids = manufacture_picking_type
|
|
p1.operation_id = bom_who.operation_ids[0].id,
|
|
self.assertTrue(p1.operation_id)
|
|
|
|
p2 = self.env['quality.point'].create({
|
|
'product_ids': [Command.link(product_a2.id)],
|
|
'picking_type_ids': [Command.link(self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')], limit=1).id)],
|
|
'operation_id': bom_who.operation_ids[0].id,
|
|
'test_type_id': self.env.ref('quality.test_type_instructions').id,
|
|
'note': 'Taking lot of coffee with UElN',
|
|
})
|
|
|
|
mrp_order_form = Form(self.env['mrp.production'])
|
|
mrp_order_form.product_id = product_a1
|
|
production_a1 = mrp_order_form.save()
|
|
|
|
mrp_order_form = Form(self.env['mrp.production'])
|
|
mrp_order_form.product_id = product_a2
|
|
production_a2 = mrp_order_form.save()
|
|
|
|
mrp_order_form = Form(self.env['mrp.production'])
|
|
mrp_order_form.product_id = product_a3
|
|
production_a3 = mrp_order_form.save()
|
|
|
|
self.assertEqual(production_a1.bom_id, bom_who)
|
|
self.assertEqual(production_a2.bom_id, bom_who)
|
|
self.assertEqual(production_a3.bom_id, bom_who)
|
|
|
|
production_a1.action_confirm()
|
|
(production_a2 | production_a3).action_confirm()
|
|
|
|
self.assertEqual(len(production_a1.workorder_ids.check_ids), 1)
|
|
self.assertEqual(production_a1.workorder_ids.check_ids.point_id, p1)
|
|
self.assertEqual(len(production_a2.workorder_ids.check_ids), 1)
|
|
self.assertEqual(production_a2.workorder_ids.check_ids.point_id, p2)
|
|
self.assertEqual(len(production_a3.workorder_ids.check_ids), 0)
|
|
|
|
def test_add_workorder_into_a_backorder(self):
|
|
""" Checks a new workorder can be created and processed into a backorder."""
|
|
(self.submarine_pod | self.elon_musk | self.metal_cylinder).tracking = 'none'
|
|
self.bom_submarine.consumption = 'flexible'
|
|
|
|
def process_workorder(workorders, qty, next_and_finish=False):
|
|
for wo in workorders:
|
|
wo.button_start()
|
|
wo_form = Form(wo, view='mrp_workorder.mrp_workorder_view_form_tablet')
|
|
wo_form.qty_producing = qty
|
|
wo = wo_form.save()
|
|
if next_and_finish:
|
|
if wo.current_quality_check_id:
|
|
wo.current_quality_check_id._next()
|
|
wo.do_finish()
|
|
|
|
# Creates a MO with 2 WO.
|
|
mo_form = Form(self.env['mrp.production'])
|
|
mo_form.product_id = self.submarine_pod
|
|
mo_form.product_qty = 10
|
|
mo = mo_form.save()
|
|
mo.action_confirm()
|
|
mo.button_plan()
|
|
|
|
# Processes partially the workorders.
|
|
process_workorder(mo.workorder_ids.sorted(), 2)
|
|
|
|
# Marks the MO as done and creates a backorder.
|
|
Form.from_action(self.env, mo.button_mark_done()).save().action_backorder()
|
|
|
|
backorder = mo.production_group_id.production_ids[-1]
|
|
self.assertEqual(mo.state, 'done')
|
|
self.assertEqual(backorder.state, 'confirmed')
|
|
self.assertEqual(len(backorder.workorder_ids), 3)
|
|
self.assertEqual(backorder.workorder_ids.mapped('qty_production'), [8.0, 8.0, 8.0])
|
|
|
|
# Adds a new WO in the backorder.
|
|
mo_form = Form(backorder)
|
|
with mo_form.workorder_ids.new() as wo_line:
|
|
wo_line.name = "OP-SP"
|
|
wo_line.workcenter_id = self.workcenter_1
|
|
mo_form.save()
|
|
|
|
# Again, processes partially the workorders.
|
|
process_workorder(backorder.workorder_ids.sorted(), 5, True)
|
|
|
|
def test_backorder_with_reserved_qty_in_sublocation(self):
|
|
"""
|
|
Let's produce a MO based on a BoM with a storable component C and a
|
|
workorder. There are some C available in a sublocation SL. The user
|
|
reserves the needed quantities, then processes a part of the MO and
|
|
creates a backorder. On the backorder, the production should be
|
|
pre-completed: the qty_producing should be set and the consumed quantity
|
|
of C should come from SL
|
|
"""
|
|
location = self.shelf_1
|
|
compo = self.bom_4.bom_line_ids.product_id
|
|
compo.is_storable = True
|
|
|
|
self.env['stock.quant']._update_available_quantity(compo, location, 3)
|
|
|
|
mo_form = Form(self.env['mrp.production'])
|
|
mo_form.bom_id = self.bom_4
|
|
mo_form.product_qty = 3
|
|
mo = mo_form.save()
|
|
mo.action_confirm()
|
|
mo.action_assign()
|
|
|
|
self.assertEqual(mo.move_raw_ids.move_line_ids.quantity, 3)
|
|
self.assertEqual(mo.move_raw_ids.move_line_ids.location_id, location)
|
|
|
|
with Form(mo) as mo_form:
|
|
mo_form.qty_producing = 1
|
|
|
|
action = mo.button_mark_done()
|
|
backorder_form = Form.from_action(self.env, action)
|
|
backorder_form.save().action_backorder()
|
|
backorder = mo.production_group_id.production_ids[1]
|
|
|
|
self.assertEqual(mo.move_raw_ids.move_line_ids.quantity, 1)
|
|
self.assertEqual(mo.move_raw_ids.move_line_ids.location_id, location)
|
|
self.assertEqual(backorder.move_raw_ids.move_line_ids.quantity, 2)
|
|
self.assertEqual(backorder.move_raw_ids.move_line_ids.location_id, location)
|
|
|
|
with Form(backorder) as bo_form:
|
|
bo_form.qty_producing = 2
|
|
self.assertEqual(backorder.move_raw_ids.move_line_ids.quantity, 2)
|
|
self.assertEqual(backorder.move_raw_ids.move_line_ids.location_id, location)
|
|
|
|
backorder.button_mark_done()
|
|
|
|
self.assertEqual(backorder.state, 'done')
|
|
self.assertEqual(backorder.move_raw_ids.state, 'done')
|
|
self.assertEqual(backorder.move_raw_ids.move_line_ids.quantity, 2)
|
|
self.assertEqual(backorder.move_raw_ids.move_line_ids.location_id, location)
|
|
|
|
def test_split_mo_finished_wo_transition(self):
|
|
""" Check that if WOs are done out of order, then backordered/split WOs are not
|
|
started when they should not be started
|
|
"""
|
|
simple_bom = self.env['mrp.bom'].create({
|
|
'product_tmpl_id': self.product_1.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'operation_ids': [
|
|
(0, 0, {'name': 'OP1', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
|
(0, 0, {'name': 'OP2', 'workcenter_id': self.workcenter_2.id, 'time_cycle': 12, 'sequence': 1}),
|
|
]})
|
|
mo_form = Form(self.env['mrp.production'])
|
|
mo_form.product_id = self.product_1
|
|
mo_form.bom_id = simple_bom
|
|
mo_form.product_qty = 2
|
|
mo = mo_form.save()
|
|
mo.action_confirm()
|
|
|
|
action = mo.action_split()
|
|
wizard = Form.from_action(self.env, action)
|
|
wizard.max_batch_size = 1
|
|
action = wizard.save().action_split()
|
|
# Should have 2 mos w/ 2 wos each
|
|
self.assertEqual(len(mo.production_group_id.production_ids), 2)
|
|
mo2 = mo.production_group_id.production_ids[1]
|
|
self.assertEqual(len(mo.workorder_ids), 2)
|
|
self.assertEqual(len(mo2.workorder_ids), 2)
|
|
self.assertEqual(mo.state, 'confirmed')
|
|
self.assertEqual(mo2.state, 'confirmed')
|
|
wo1_1, wo1_2 = mo.workorder_ids.sorted()
|
|
wo2_1, wo2_2 = mo2.workorder_ids.sorted()
|
|
self.assertEqual(wo1_1.state, 'ready')
|
|
self.assertEqual(wo1_2.state, 'blocked')
|
|
self.assertEqual(wo2_1.state, 'ready')
|
|
self.assertEqual(wo2_2.state, 'blocked')
|
|
|
|
wo1_1.qty_producing = 1
|
|
wo1_1.do_finish()
|
|
self.assertEqual(wo1_1.state, 'done')
|
|
self.assertEqual(wo1_2.state, 'ready')
|
|
self.assertEqual(wo2_1.state, 'ready', "Completion of first MO's WO no longer auto-start second MO's first WO")
|
|
self.assertEqual(wo2_2.state, 'blocked')
|
|
wo1_2.do_finish()
|
|
self.assertEqual(wo1_1.state, 'done')
|
|
self.assertEqual(wo1_2.state, 'done')
|
|
self.assertEqual(wo2_1.state, 'ready')
|
|
self.assertEqual(wo2_2.state, 'blocked')
|
|
self.assertEqual(mo.state, 'to_close')
|
|
|
|
def test_workorder_tracked_final_product(self):
|
|
"""
|
|
Suppose that you have an MO for a prodcut tracked by SN with two operations.
|
|
Check that you can mark the operations as done without registering an SN.
|
|
"""
|
|
tracked_product = self.env['product.product'].create({
|
|
'name': 'Final product',
|
|
'is_storable': True,
|
|
'tracking': 'serial',
|
|
})
|
|
comp_1, comp_2 = self.product_1, self.product_2
|
|
(comp_1 | comp_2).is_storable = True
|
|
self.env['stock.quant']._update_available_quantity(comp_1, self.stock_location, 5)
|
|
self.env['stock.quant']._update_available_quantity(comp_2, self.stock_location, 5)
|
|
bom = self.env['mrp.bom'].create({
|
|
'product_tmpl_id': tracked_product.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'operation_ids': [
|
|
Command.create({'name': 'OP1', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
|
Command.create({'name': 'OP2', 'workcenter_id': self.workcenter_2.id, 'time_cycle': 12, 'sequence': 1}),
|
|
],
|
|
'bom_line_ids': [
|
|
Command.create({'product_id': comp_1.id, 'product_qty': 1}),
|
|
Command.create({'product_id': comp_2.id, 'product_qty': 1}),
|
|
],
|
|
})
|
|
bom.bom_line_ids[0].operation_id = bom.operation_ids[0]
|
|
bom.bom_line_ids[1].operation_id = bom.operation_ids[1]
|
|
|
|
mo_form = Form(self.env['mrp.production'])
|
|
mo_form.product_id = self.product_1
|
|
mo_form.bom_id = bom
|
|
mo_form.product_qty = 1
|
|
mo = mo_form.save()
|
|
mo.action_confirm()
|
|
|
|
# Operation 1 & 2: consume components and mark as done
|
|
raw_move_1, raw_move_2 = mo.move_raw_ids
|
|
operation_1, operation_2 = mo.workorder_ids
|
|
operation_1.button_start()
|
|
operation_1.qty_producing = 1.0
|
|
raw_move_1.move_line_ids[0].quantity = 1.0
|
|
operation_1.do_finish()
|
|
self.assertRecordValues(operation_1, [{'qty_produced': 1.0, 'finished_lot_ids': [], 'state': 'done'}])
|
|
operation_2.button_start()
|
|
operation_2.qty_producing = 1.0
|
|
raw_move_2.move_line_ids[0].quantity = 1.0
|
|
operation_2.do_finish()
|
|
self.assertRecordValues(operation_2, [{'qty_produced': 1.0, 'finished_lot_ids': [], 'state': 'done'}])
|
|
self.assertEqual(mo.state, 'to_close')
|
|
# Try to finish the production without assigning an SN
|
|
mo.move_raw_ids.filtered(lambda m: not m.operation_id).picked = True
|
|
|
|
mo.button_mark_done()
|
|
self.assertEqual(operation_1.finished_lot_ids, mo.lot_producing_ids)
|
|
self.assertEqual(operation_2.finished_lot_ids, mo.lot_producing_ids)
|
|
self.assertEqual(mo.state, 'done')
|
|
|
|
@freeze_time('2025-10-01')
|
|
def test_workorder_duration_inverse_time_ids_consistency(self):
|
|
"""
|
|
Verify that the inverse method of the workorder duration field correctly
|
|
mirrors the logic of its compute method.
|
|
|
|
Specifically, the duration is computed as the total interval duration,
|
|
counting overlapping time_ids only once. This ensures that the inverse
|
|
method does not accidentally unlink any time_ids when updating durations.
|
|
"""
|
|
now = fields.Datetime.now()
|
|
mo = self.env['mrp.production'].create({
|
|
'product_qty': 1,
|
|
'product_id': self.product_1.id,
|
|
'workorder_ids': [Command.create({
|
|
'name': 'workorder_1',
|
|
'workcenter_id': self.workcenter_1.id,
|
|
'time_ids': [
|
|
Command.create({
|
|
'workcenter_id': self.workcenter_1.id,
|
|
'date_start': now - timedelta(minutes=30),
|
|
'date_end': now,
|
|
'loss_id': self.ref('mrp.block_reason4'),
|
|
}),
|
|
Command.create({
|
|
'workcenter_id': self.workcenter_1.id,
|
|
'date_start': now - timedelta(minutes=20),
|
|
'date_end': now - timedelta(minutes=5),
|
|
'loss_id': self.ref('mrp.block_reason4'),
|
|
}),
|
|
],
|
|
})],
|
|
})
|
|
wo = mo.workorder_ids
|
|
wo.duration = wo.duration
|
|
self.assertEqual(wo.duration, 30.0)
|
|
self.assertRecordValues(wo.time_ids, [{'duration': 30.0}, {'duration': 15.0}])
|
|
|
|
def test_workorder_timelog_for_assigned_employee(self):
|
|
"""This test ensures that when a work order is assigned to an employee (not the current/logged-in user)
|
|
and is marked as done by the current user, the time log for workorder is correctly recorded
|
|
under the assigned employee instead of the current user.
|
|
"""
|
|
# Create employees
|
|
assigned_employee, current_employee = self.env['hr.employee'].create([
|
|
{'name': 'Assigned Employee'},
|
|
{'name': 'Current User Employee', 'user_id': self.env.user.id},
|
|
])
|
|
|
|
# Create and confirm a Manufacturing Order with workorders.
|
|
mo_form = Form(self.env['mrp.production'])
|
|
mo_form.product_id = self.submarine_pod
|
|
mo_form.bom_id = self.bom_submarine
|
|
mo = mo_form.save()
|
|
mo.action_confirm()
|
|
|
|
workorder_1, workorder_2 = mo.workorder_ids[:2]
|
|
# Assign a different employee to the workorder_1 and keep workorder_2 unassigned.
|
|
workorder_1.employee_assigned_ids = assigned_employee
|
|
# mark the workorders as done.
|
|
mo.workorder_ids.action_mark_as_done()
|
|
|
|
# The time log should be assigned to the employee assigned in the workorder_1,
|
|
# and to the current user in workorder_2.
|
|
self.assertEqual(workorder_1.time_ids[0].employee_id, assigned_employee)
|
|
self.assertEqual(workorder_2.time_ids[0].employee_id, current_employee)
|
|
|
|
def test_update_bom_update_operation_workorder(self):
|
|
"""
|
|
When an operation is edited from the BOM and action_update_bom is called on a
|
|
confirmed MO, the workorder related to that operation must be updated
|
|
"""
|
|
final_product = self.env['product.product'].create({'name': 'Final Product', 'is_storable': True})
|
|
component_1 = self.env['product.product'].create({'name': 'Component 1', 'is_storable': True})
|
|
component_2 = self.env['product.product'].create({'name': 'Component 2', 'is_storable': True})
|
|
bom = self.env['mrp.bom'].create({
|
|
'product_tmpl_id': final_product.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'bom_line_ids': [
|
|
Command.create({'product_id': component_1.id, 'product_qty': 1}),
|
|
Command.create({'product_id': component_2.id, 'product_qty': 1}),
|
|
],
|
|
'operation_ids': [
|
|
Command.create({'name': 'Operation 1', 'workcenter_id': self.workcenter_1.id}),
|
|
Command.create({'name': 'Operation 2', 'workcenter_id': self.workcenter_1.id}),
|
|
],
|
|
})
|
|
operation_to_update = bom.operation_ids[0]
|
|
mo = self.env['mrp.production'].create({
|
|
'product_id': final_product.id,
|
|
'bom_id': bom.id,
|
|
'product_qty': 1.0,
|
|
})
|
|
mo.action_confirm()
|
|
self.assertEqual(mo.state, 'confirmed')
|
|
|
|
instruction = self.env['quality.point'].create({
|
|
'product_ids': [Command.link(final_product.id)],
|
|
'operation_id': operation_to_update.id,
|
|
'picking_type_ids': [Command.link(mo.picking_type_id.id)],
|
|
'test_type_id': self.ref('mrp_workorder.test_type_register_consumed_materials'),
|
|
'component_id': component_1.id
|
|
})
|
|
operation_to_update.quality_point_ids = instruction
|
|
mo.action_update_bom()
|
|
self.assertRecordValues(mo.workorder_ids, [
|
|
{'operation_id': operation_to_update.id, 'quality_point_ids': instruction.ids},
|
|
{'operation_id': bom.operation_ids.ids[1], 'quality_point_ids': []},
|
|
])
|
|
|
|
|
|
@tagged("post_install", "-at_install")
|
|
class TestShopFloor(HttpCase, TestMrpWorkorderCommon):
|
|
@unittest.skip # TODO: tour needs to be updated.
|
|
def test_access_shop_floor_with_multicompany(self):
|
|
"""
|
|
test the flow when we have multicompany situation and
|
|
we want to access shop floor from a company after switching
|
|
from the other one.
|
|
"""
|
|
company1 = self.env['res.company'].create({'name': 'Test Company'})
|
|
user_admin = self.env.ref('base.user_admin')
|
|
user_admin.write({
|
|
'company_ids': [Command.link(company1.id)],
|
|
'group_ids': [Command.link(self.env.ref('mrp.group_mrp_routings').id)],
|
|
})
|
|
submarine_pod = self.env['product.product'].with_company(company1).with_user(user_admin).create({
|
|
'name': 'Submarine pod',
|
|
'is_storable': True,
|
|
'tracking': 'serial'})
|
|
workcenter_2 = self.env['mrp.workcenter'].with_company(company1).with_user(user_admin).create({
|
|
'name': 'Nuclear Workcenter',
|
|
'time_start': 10,
|
|
'time_stop': 5,
|
|
'time_efficiency': 80,
|
|
})
|
|
bom_submarine = self.env['mrp.bom'].with_company(company1).with_user(user_admin).create({
|
|
'product_tmpl_id': submarine_pod.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'operation_ids': [
|
|
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': workcenter_2.id,
|
|
'time_cycle': 12, 'sequence': 1}),
|
|
]})
|
|
mo_form = Form(self.env['mrp.production'].with_company(
|
|
company1).with_user(user_admin))
|
|
mo_form.product_id = submarine_pod
|
|
mo_form.bom_id = bom_submarine
|
|
mo_form.product_qty = 1
|
|
mo = mo_form.save()
|
|
mo.action_confirm()
|
|
mo.action_assign()
|
|
mo.button_plan()
|
|
self.start_tour(
|
|
"/", 'test_access_shop_floor_with_multicompany', login="admin")
|