IMP] mrp: add workcenter capacity per product

Allow workcenter capacities based on product instead of
applying the same capacity for all.

closes odoo/odoo#87131

Task: 2691328
Related: odoo/enterprise#25532
Related: odoo/upgrade#3370
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
This commit is contained in:
JF Aubert
2022-06-03 16:40:09 +02:00
parent bb5805542a
commit fcc7b3d46a
13 changed files with 58 additions and 23 deletions
+2 -1
View File
@@ -87,7 +87,8 @@ class MrpRoutingWorkcenter(models.Model):
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')
capacity = item['workcenter_id']._get_capacity(item.product_id)
cycle_number += tools.float_round((item['qty_produced'] / capacity or 1.0), precision_digits=0, rounding_method='UP')
if cycle_number:
operation.time_cycle = total_duration / cycle_number
else:
+26 -4
View File
@@ -30,9 +30,9 @@ class MrpWorkcenter(models.Model):
note = fields.Html(
'Description',
help="Description of the Work Center.")
capacity = fields.Float(
default_capacity = fields.Float(
'Capacity', default=1.0,
help="Number of pieces that can be produced in parallel. In case the work center has a capacity of 5 and you have to produce 10 units on your work order, the usual operation time will be multiplied by 2.")
help="Default number of pieces that can be produced in parallel. In case the work center has a capacity of 5 and you have to produce 10 units on your work order, the usual operation time will be multiplied by 2.")
sequence = fields.Integer(
'Sequence', default=1, required=True,
help="Gives the sequence order when displaying a list of work centers.")
@@ -73,6 +73,8 @@ class MrpWorkcenter(models.Model):
help="Alternative workcenters that can be substituted to this one in order to dispatch production"
)
tag_ids = fields.Many2many('mrp.workcenter.tag')
capacity_ids = fields.One2many('mrp.workcenter.capacity', 'workcenter_id', string='Product Capacities',
help="Specific number of pieces that can be produced in parallel per product.", copy=True)
@api.constrains('alternative_workcenter_ids')
def _check_alternative_workcenter(self):
@@ -172,9 +174,9 @@ class MrpWorkcenter(models.Model):
else:
workcenter.performance = 0.0
@api.constrains('capacity')
@api.constrains('default_capacity')
def _check_capacity(self):
if any(workcenter.capacity <= 0.0 for workcenter in self):
if any(workcenter.default_capacity <= 0.0 for workcenter in self):
raise exceptions.UserError(_('The capacity must be strictly positive.'))
def unblock(self):
@@ -287,6 +289,10 @@ class MrpWorkcenter(models.Model):
}
return res
def _get_capacity(self, product):
product_capacity = self.capacity_ids.filtered(lambda capacity: capacity.product_id == product)
return product_capacity.capacity if product_capacity else self.default_capacity
class WorkcenterTag(models.Model):
_name = 'mrp.workcenter.tag'
@@ -405,3 +411,19 @@ class MrpWorkcenterProductivity(models.Model):
def button_block(self):
self.ensure_one()
self.workcenter_id.order_ids.end_all()
class MrpWorkCenterCapacity(models.Model):
_name = 'mrp.workcenter.capacity'
_description = 'Work Center Capacity'
_check_company_auto = True
workcenter_id = fields.Many2one('mrp.workcenter', string='Work Center', required=True)
product_id = fields.Many2one('product.product', string='Product', required=True)
product_uom_id = fields.Many2one('uom.uom', string='Product UoM', related='product_id.uom_id')
capacity = fields.Float('Capacity', default=1.0, help="Number of pieces that can be produced in parallel for this product.")
_sql_constraints = [
('positive_capacity', 'CHECK(capacity > 0)', 'Capacity should be a positive number.'),
('unique_product', 'UNIQUE(workcenter_id, product_id)', 'Product capacity should be unique for each workcenter.'),
]
+4 -2
View File
@@ -736,13 +736,15 @@ class MrpWorkorder(models.Model):
duration_expected_working = 0
return self.workcenter_id.time_start + self.workcenter_id.time_stop + duration_expected_working * ratio * 100.0 / self.workcenter_id.time_efficiency
qty_production = self.production_id.product_uom_id._compute_quantity(self.qty_production, self.production_id.product_id.uom_id)
cycle_number = float_round(qty_production / self.workcenter_id.capacity, precision_digits=0, rounding_method='UP')
capacity = self.workcenter_id._get_capacity(self.product_id)
cycle_number = float_round(qty_production / capacity, precision_digits=0, rounding_method='UP')
if alternative_workcenter:
# TODO : find a better alternative : the settings of workcenter can change
duration_expected_working = (self.duration_expected - self.workcenter_id.time_start - self.workcenter_id.time_stop) * self.workcenter_id.time_efficiency / (100.0 * cycle_number)
if duration_expected_working < 0:
duration_expected_working = 0
alternative_wc_cycle_nb = float_round(qty_production / alternative_workcenter.capacity, precision_digits=0, rounding_method='UP')
capacity = alternative_workcenter._get_capacity(self.product_id)
alternative_wc_cycle_nb = float_round(qty_production / capacity, precision_digits=0, rounding_method='UP')
return alternative_workcenter.time_start + alternative_workcenter.time_stop + alternative_wc_cycle_nb * duration_expected_working * 100.0 / alternative_workcenter.time_efficiency
time_cycle = self.operation_id.time_cycle
return self.workcenter_id.time_start + self.workcenter_id.time_stop + cycle_number * time_cycle * 100.0 / self.workcenter_id.time_efficiency
+1 -1
View File
@@ -175,7 +175,7 @@ class MrpWorkcenter(models.Model):
('resource_calendar_id', populate.compute(get_resource_calendar_id)),
('active', populate.iterate([True, False], [0.9, 0.1])),
('code', populate.constant("W/{counter}")),
('capacity', populate.iterate([0.5, 1.0, 2.0, 5.0], [0.2, 0.4, 0.2, 0.2])),
('default_capacity', populate.iterate([0.5, 1.0, 2.0, 5.0], [0.2, 0.4, 0.2, 0.2])),
('sequence', populate.randint(1, 1000)),
('color', populate.randint(1, 12)),
('costs_hour', populate.randint(5, 25)),
@@ -220,7 +220,8 @@ class ReportBomStructure(models.AbstractModel):
for operation in bom.operation_ids:
if operation._skip_operation_line(product):
continue
operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP')
capacity = operation.workcenter_id._get_capacity(product)
operation_cycle = float_round(qty / capacity, precision_rounding=1, rounding_method='UP')
duration_expected = (operation_cycle * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) + (operation.workcenter_id.time_stop + operation.workcenter_id.time_start)
total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour)
operations.append({
+1
View File
@@ -61,3 +61,4 @@ access_mrp_workcenter_tag_manager,access.mrp.workcenter.tag,model_mrp_workcenter
access_mrp_production_split_multi,access.mrp.production.split.multi,model_mrp_production_split_multi,mrp.group_mrp_user,1,1,1,0
access_mrp_production_split,access.mrp.production.split,model_mrp_production_split,mrp.group_mrp_user,1,1,1,0
access_mrp_production_split_line,access.mrp.production.split.line,model_mrp_production_split_line,mrp.group_mrp_user,1,1,1,1
access_mrp_workcenter_capacity_manager,mrp.workcenter.capacity.manager,model_mrp_workcenter_capacity,mrp.group_mrp_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
61 access_mrp_production_split_multi access.mrp.production.split.multi model_mrp_production_split_multi mrp.group_mrp_user 1 1 1 0
62 access_mrp_production_split access.mrp.production.split model_mrp_production_split mrp.group_mrp_user 1 1 1 0
63 access_mrp_production_split_line access.mrp.production.split.line model_mrp_production_split_line mrp.group_mrp_user 1 1 1 1
64 access_mrp_workcenter_capacity_manager mrp.workcenter.capacity.manager model_mrp_workcenter_capacity mrp.group_mrp_manager 1 1 1 1
+3 -3
View File
@@ -79,21 +79,21 @@ class TestMrpCommon(common2.TestStockCommon):
cls.workcenter_1 = cls.env['mrp.workcenter'].create({
'name': 'Nuclear Workcenter',
'capacity': 2,
'default_capacity': 2,
'time_start': 10,
'time_stop': 5,
'time_efficiency': 80,
})
cls.workcenter_2 = cls.env['mrp.workcenter'].create({
'name': 'Simple Workcenter',
'capacity': 1,
'default_capacity': 1,
'time_start': 0,
'time_stop': 0,
'time_efficiency': 100,
})
cls.workcenter_3 = cls.env['mrp.workcenter'].create({
'name': 'Double Workcenter',
'capacity': 2,
'default_capacity': 2,
'time_start': 0,
'time_stop': 0,
'time_efficiency': 100,
+3 -3
View File
@@ -2673,7 +2673,7 @@ class TestMrpOrder(TestMrpCommon):
"""
workcenter_1 = self.env['mrp.workcenter'].create({
'name': 'wc1',
'capacity': 2,
'default_capacity': 2,
'time_start': 1,
'time_stop': 1,
'time_efficiency': 100,
@@ -2681,7 +2681,7 @@ class TestMrpOrder(TestMrpCommon):
workcenter_2 = self.env['mrp.workcenter'].create({
'name': 'wc2',
'capacity': 2,
'default_capacity': 2,
'time_start': 10,
'time_stop': 5,
'time_efficiency': 100,
@@ -2725,7 +2725,7 @@ class TestMrpOrder(TestMrpCommon):
mo.button_unplan()
# Update the production capcity
workcenter_2.capacity = 4
workcenter_2.default_capacity = 4
#MO_2
mo_form = Form(self.env['mrp.production'])
+11 -3
View File
@@ -40,7 +40,7 @@
<field name="alternative_workcenter_ids" widget="many2many_tags" optional="show"/>
<field name="productive_time" optional="hide"/>
<field name="costs_hour" optional="show"/>
<field name="capacity" optional="show"/>
<field name="default_capacity" optional="show"/>
<field name="time_efficiency" optional="show"/>
<field name="oee_target" optional="show"/>
<field name="time_start" optional="hide"/>
@@ -364,8 +364,7 @@
<div class="o_row">
<field name="time_efficiency"/> %
</div>
<field name="capacity"/>
<field name="default_capacity"/>
<label for="oee_target"/>
<div class="o_row">
<field name="oee_target"/> %
@@ -388,6 +387,15 @@
<separator string="Description"/>
<field name="note" nolabel="1" placeholder="Description of the work center..."/>
</page>
<page string="Specific Capacities" name="capacity">
<field name="capacity_ids" context="{'default_workcenter_id': id, 'default_company_id': company_id}">
<tree editable="bottom">
<field name="product_id"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<field name="capacity"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
@@ -15,7 +15,7 @@ class TestAnalyticAccount(TransactionCase):
cls.analytic_account = cls.env['account.analytic.account'].create({'name': 'test_analytic_account'})
cls.workcenter = cls.env['mrp.workcenter'].create({
'name': 'Workcenter',
'capacity': 1,
'default_capacity': 1,
'time_efficiency': 100,
'costs_hour': 10,
})
+1 -1
View File
@@ -110,7 +110,7 @@ class TestBomPrice(common.TransactionCase):
workcenter_from1 = Form(self.env['mrp.workcenter'])
workcenter_from1.name = 'Workcenter'
workcenter_from1.time_efficiency = 100
workcenter_from1.capacity = 2
workcenter_from1.default_capacity = 2
workcenter_from1.oee_target = 100
workcenter_from1.time_start = 0
workcenter_from1.time_stop = 0
+2 -2
View File
@@ -16,14 +16,14 @@ class TestMrpAccount(TestMrpCommon):
# setting up alternative workcenters
cls.wc_alt_1 = cls.env['mrp.workcenter'].create({
'name': 'Nuclear Workcenter bis',
'capacity': 3,
'default_capacity': 3,
'time_start': 9,
'time_stop': 5,
'time_efficiency': 80,
})
cls.wc_alt_2 = cls.env['mrp.workcenter'].create({
'name': 'Nuclear Workcenter ter',
'capacity': 1,
'default_capacity': 1,
'time_start': 10,
'time_stop': 5,
'time_efficiency': 85,
@@ -60,7 +60,7 @@ class TestStockLot(TestStockCommon):
# Creation of a routing
cls.workcenter = cls.env['mrp.workcenter'].create({
'name': 'Bakery',
'capacity': 2,
'default_capacity': 2,
'time_start': 10,
'time_stop': 5,
'time_efficiency': 80,