[FIX] mrp : wrong expected duration in WC with capacity
Issue: In work centers with a capacity different than 1, the expected
duration was wrongly computed
Steps to reproduce :
1) Install Manufacture, enable Work Centers in Settings
2) Create a Work Center, Capacity = 2
3) Create a Product, with Manufacture as route
4) Create a Bill of Material :
Add any component
Add a operation line :
Work Center = created at 2)
Duration Computation = based on tracked time last 1 WO
5) Create a Manufacturing Order for the product 3), confirm
6) Set the real duration to 00:10, mark as done
7) Check the BoM Structure & Cost of the BoM 4)
-> Operations is 00:20
Why is that a bug:
To compute the expected duration, in mrp_workorder.py L691 we already
take into consideration the capacity of the work center.
When computing the time_cycle, we must give a time_cycle for 1 product
made with capacity 1, so we must first normalize the qty_produced fetch
from the DB to make like if we only produced the quantity once but in
the same amount of time
Side-Note: This also solves an issue with the Duration Computation based
on tracked time. The `limit` keyword was applied after the `group by`
so we effectively grouped all the operation_ids together, summing
their quantities and then limiting on the different operation_ids
opw-2562983
closes odoo/odoo#75557
X-original-commit: c78a25f92716bd6014de381c34f3bc268d1528ae
Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
Signed-off-by: Nathan Marotte <nmarotte@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user