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:
@@ -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:
|
||||
|
||||
@@ -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.'),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user