diff --git a/addons/mrp/models/mrp_routing.py b/addons/mrp/models/mrp_routing.py index d7188958af2..95dd7838662 100644 --- a/addons/mrp/models/mrp_routing.py +++ b/addons/mrp/models/mrp_routing.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models, _ +from odoo import api, fields, models, _, tools class MrpRoutingWorkcenter(models.Model): @@ -60,14 +60,24 @@ class MrpRoutingWorkcenter(models.Model): for operation in manual_ops: operation.time_cycle = operation.time_cycle_manual for operation in self - manual_ops: - data = self.env['mrp.workorder'].read_group([ + data = self.env['mrp.workorder'].search([ ('operation_id', '=', operation.id), ('qty_produced', '>', 0), - ('state', '=', 'done')], ['operation_id', 'duration', 'qty_produced'], ['operation_id'], - limit=operation.time_mode_batch) - count_data = dict((item['operation_id'][0], (item['duration'], item['qty_produced'])) for item in data) - if count_data.get(operation.id) and count_data[operation.id][1]: - operation.time_cycle = (count_data[operation.id][0] / count_data[operation.id][1]) * (operation.workcenter_id.capacity or 1.0) + ('state', '=', 'done')], + limit=operation.time_mode_batch, + order="date_finished desc") + # To compute the time_cycle, we can take the total duration of previous operations + # but for the quantity, we will take in consideration the qty_produced like if the capacity was 1. + # So producing 50 in 00:10 with capacity 2, for the time_cycle, we assume it is 25 in 00:10 + # When recomputing the expected duration, the capacity is used again to divide the qty to produce + # so that if we need 50 with capacity 2, it will compute the expected of 25 which is 00:10 + total_duration = 0 # Can be 0 since it's not an invalid duration for BoM + cycle_number = 0 # Never 0 unless infinite item['workcenter_id'].capacity + for item in data: + total_duration += item['duration'] + cycle_number += tools.float_round((item['qty_produced'] / item['workcenter_id'].capacity or 1.0), precision_digits=0, rounding_method='UP') + if cycle_number: + operation.time_cycle = total_duration / cycle_number else: operation.time_cycle = operation.time_cycle_manual diff --git a/addons/mrp/tests/common.py b/addons/mrp/tests/common.py index 16b83ab251b..bcd6e693af4 100644 --- a/addons/mrp/tests/common.py +++ b/addons/mrp/tests/common.py @@ -84,6 +84,20 @@ class TestMrpCommon(common2.TestStockCommon): 'time_stop': 5, 'time_efficiency': 80, }) + cls.workcenter_2 = cls.env['mrp.workcenter'].create({ + 'name': 'Simple Workcenter', + 'capacity': 1, + 'time_start': 0, + 'time_stop': 0, + 'time_efficiency': 100, + }) + cls.workcenter_3 = cls.env['mrp.workcenter'].create({ + 'name': 'Double Workcenter', + 'capacity': 2, + 'time_start': 0, + 'time_stop': 0, + 'time_efficiency': 100, + }) cls.bom_1 = cls.env['mrp.bom'].create({ 'product_id': cls.product_4.id, @@ -129,6 +143,45 @@ class TestMrpCommon(common2.TestStockCommon): (0, 0, {'product_id': cls.product_4.id, 'product_qty': 8}), (0, 0, {'product_id': cls.product_2.id, 'product_qty': 12}) ]}) + cls.bom_4 = cls.env['mrp.bom'].create({ + 'product_id': cls.product_6.id, + 'product_tmpl_id': cls.product_6.product_tmpl_id.id, + 'consumption': 'flexible', + 'product_qty': 1.0, + 'operation_ids': [ + (0, 0, {'name': 'Rub it gently with a cloth', 'workcenter_id': cls.workcenter_2.id, + 'time_mode_batch': 1, 'time_mode': "auto", 'sequence': 1}), + ], + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}), + ]}) + cls.bom_5 = cls.env['mrp.bom'].create({ + 'product_id': cls.product_6.id, + 'product_tmpl_id': cls.product_6.product_tmpl_id.id, + 'consumption': 'flexible', + 'product_qty': 1.0, + 'operation_ids': [ + (0, 0, {'name': 'Rub it gently with a cloth two at once', 'workcenter_id': cls.workcenter_3.id, + 'time_mode_batch': 2, 'time_mode': "auto", 'sequence': 1}), + ], + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}), + ]}) + cls.bom_6 = cls.env['mrp.bom'].create({ + 'product_id': cls.product_6.id, + 'product_tmpl_id': cls.product_6.product_tmpl_id.id, + 'consumption': 'flexible', + 'product_qty': 1.0, + 'operation_ids': [ + (0, 0, {'name': 'Rub it gently with a cloth two at once', 'workcenter_id': cls.workcenter_3.id, + 'time_mode_batch': 1, 'time_mode': "auto", 'sequence': 1}), + ], + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': cls.product_1.id, 'product_qty': 1}), + ]}) cls.stock_location_14 = cls.env['stock.location'].create({ 'name': 'Shelf 2', diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 578e8e071a9..0c437a0568e 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -2231,3 +2231,134 @@ class TestMrpOrder(TestMrpCommon): 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') + + def test_compute_tracked_time_1(self): + """ + Checks that the Duration Computation (`time_mode` of mrp.routing.workcenter) with value `auto` with Based On + (`time_mode_batch`) set to 1 actually compute the time based on the last 1 operation, and not more. + Create a first production in 15 minutes (expected should go from 60 to 15 + Create a second one in 10 minutes (expected should NOT go from 15 to 12.5, it should go from 15 to 10) + """ + # First production, the default is 60 and there is 0 productions of that operation + production_form = Form(self.env['mrp.production']) + production_form.bom_id = self.bom_4 + production = production_form.save() + self.assertEqual(production.workorder_ids[0].duration_expected, 60.0, "Default duration is 0+0+1*60.0") + production.action_confirm() + production.button_plan() + # Production planned, time to start, I produce all the 1 product + production_form.qty_producing = 1 + with production_form.workorder_ids.edit(0) as wo: + wo.duration = 15 # in 15 minutes + production = production_form.save() + production.button_mark_done() + # It is saved and done, registered in the db. There are now 1 productions of that operation + + # Same production, let's see what the duration_expected is, last prod was 15 minutes for 1 item + production_form = Form(self.env['mrp.production']) + production_form.bom_id = self.bom_4 + production = production_form.save() + self.assertEqual(production.workorder_ids[0].duration_expected, 15.0, "Duration is now 0+0+1*15") + production.action_confirm() + production.button_plan() + # Production planned, time to start, I produce all the 1 product + production_form.qty_producing = 1 + with production_form.workorder_ids.edit(0) as wo: + wo.duration = 10 # In 10 minutes this time + production = production_form.save() + production.button_mark_done() + # It is saved and done, registered in the db. There are now 2 productions of that operation + + # Same production, let's see what the duration_expected is, last prod was 10 minutes for 1 item + # Total average time would be 12.5 but we compute the duration based on the last 1 item + production_form = Form(self.env['mrp.production']) + production_form.bom_id = self.bom_4 + production = production_form.save() + self.assertNotEqual(production.workorder_ids[0].duration_expected, 12.5, "Duration expected is based on the last 1 production, not last 2") + self.assertEqual(production.workorder_ids[0].duration_expected, 10.0, "Duration is now 0+0+1*10") + + def test_compute_tracked_time_2_under_capacity(self): + """ + Test that when tracking the 2 last production, if we make one with under capacity, and one with normal capacity, + the two are equivalent (1 done with capacity 2 in 10mn = 2 done with capacity 2 in 10mn) + """ + production_form = Form(self.env['mrp.production']) + production_form.bom_id = self.bom_5 + production = production_form.save() + production.action_confirm() + production.button_plan() + + # Production planned, time to start, I produce all the 1 product + production_form.qty_producing = 1 + with production_form.workorder_ids.edit(0) as wo: + wo.duration = 10 # in 10 minutes + production = production_form.save() + production.button_mark_done() + # It is saved and done, registered in the db. There are now 1 productions of that operation + + # Same production, let's see what the duration_expected is, last prod was 10 minutes for 1 item + production_form = Form(self.env['mrp.production']) + production_form.bom_id = self.bom_5 + production_form.product_qty = 2 # We want to produce 2 items (the capacity) now + production = production_form.save() + self.assertNotEqual(production.workorder_ids[0].duration_expected, 20.0, "We made 1 item with capacity 2 in 10mn -> so 2 items shouldn't be double that") + self.assertEqual(production.workorder_ids[0].duration_expected, 10.0, "Producing 1 or 2 items with capacity 2 is the same duration") + production.action_confirm() + production.button_plan() + # Production planned, time to start, I produce all the 2 product + production_form.qty_producing = 2 + with production_form.workorder_ids.edit(0) as wo: + wo.duration = 10 # In 10 minutes this time + production = production_form.save() + production.button_mark_done() + # It is saved and done, registered in the db. There are now 2 productions of that operation but they have the same duration + + production_form = Form(self.env['mrp.production']) + production_form.bom_id = self.bom_5 + production = production_form.save() + self.assertNotEqual(production.workorder_ids[0].duration_expected, 15, "Producing 1 or 2 in 10mn with capacity 2 take the same amount of time : 10mn") + self.assertEqual(production.workorder_ids[0].duration_expected, 10.0, "Duration is indeed (10+10)/2") + + def test_capacity_duration_expected(self): + """ + Test that the duration expected is correctly computed when dealing with below or above capacity + 1 -> 10mn + 2 -> 10mn + 3 -> 20mn + 4 -> 20mn + 5 -> 30mn + ... + """ + production_form = Form(self.env['mrp.production']) + production_form.bom_id = self.bom_6 + production = production_form.save() + production.action_confirm() + production.button_plan() + + # Production planned, time to start, I produce all the 1 product + production_form.qty_producing = 1 + with production_form.workorder_ids.edit(0) as wo: + wo.duration = 10 # in 10 minutes + production = production_form.save() + production.button_mark_done() + + production_form = Form(self.env['mrp.production']) + production_form.bom_id = self.bom_6 + production = production_form.save() + # production_form.product_qty = 1 [BY DEFAULT] + self.assertEqual(production.workorder_ids[0].duration_expected, 10.0, "Produce 1 with capacity 2, expected is 10mn for each run -> 10mn") + production_form.product_qty = 2 + production = production_form.save() + self.assertEqual(production.workorder_ids[0].duration_expected, 10.0, "Produce 2 with capacity 2, expected is 10mn for each run -> 10mn") + + production_form.product_qty = 3 + production = production_form.save() + self.assertEqual(production.workorder_ids[0].duration_expected, 20.0, "Produce 3 with capacity 2, expected is 10mn for each run -> 20mn") + + production_form.product_qty = 4 + production = production_form.save() + self.assertEqual(production.workorder_ids[0].duration_expected, 20.0, "Produce 4 with capacity 2, expected is 10mn for each run -> 20mn") + + production_form.product_qty = 5 + production = production_form.save() + self.assertEqual(production.workorder_ids[0].duration_expected, 30.0, "Produce 5 with capacity 2, expected is 10mn for each run -> 30mn")