[REF] mrp: remove routing model

Set the operations directly on the Bill of Material.
Duplicate the demo data where a routing was shared.
Adapt the tests.

Remove the following feature:
    - set the same routing on parent and kit child bom
    - when planning, if the component of the kit have the same operation
      than a component of the parent bom, merge these operations

task-2241471
This commit is contained in:
Simon Lejeune
2020-06-15 16:59:40 +02:00
parent beccf82e09
commit c660770ebd
30 changed files with 491 additions and 620 deletions
-9
View File
@@ -2,15 +2,6 @@
<odoo>
<data noupdate="1">
<record id="sequence_mrp_route" model="ir.sequence">
<field name="name">Routing</field>
<field name="code">mrp.routing</field>
<field name="prefix">RO/</field>
<field name="padding">5</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
<field name="company_id" eval="False"/>
</record>
<function model="res.company" name="create_missing_unbuild_sequences" />
<!--
Stock rules and routes
+77 -82
View File
@@ -28,83 +28,6 @@
<field name="resource_calendar_id" ref="resource.resource_calendar_std"/>
</record>
<!-- Resource: mrp.routing -->
<record id="mrp_routing_0" model="mrp.routing">
<field name="name">Primary Assembly</field>
</record>
<record id="mrp_routing_1" model="mrp.routing">
<field name="name">Secondary Assembly</field>
</record>
<record id="mrp_routing_2" model="mrp.routing">
<field name="name">Manual Component's Assembly</field>
</record>
<record id="mrp_routing_3" model="mrp.routing">
<field name="name">Assemble Furniture</field>
</record>
<!-- Resource: mrp.routing.workcenter -->
<record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter">
<field name="routing_id" ref="mrp_routing_0"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="name">Manual Assembly</field>
<field name="time_cycle">60</field>
<field name="sequence">5</field>
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
</record>
<record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter">
<field name="routing_id" ref="mrp_routing_1"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="name">Long time assembly</field>
<field name="time_cycle">180</field>
<field name="sequence">15</field>
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
</record>
<record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter">
<field name="routing_id" ref="mrp_routing_1"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="name">Testing</field>
<field name="time_cycle">60</field>
<field name="sequence">10</field>
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
</record>
<record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter">
<field name="routing_id" ref="mrp_routing_1"/>
<field name="workcenter_id" ref="mrp_workcenter_1"/>
<field name="name">Packing</field>
<field name="time_cycle">30</field>
<field name="sequence">5</field>
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
</record>
<record id="mrp_routing_workcenter_2" model="mrp.routing.workcenter">
<field name="routing_id" ref="mrp_routing_2"/>
<field name="workcenter_id" ref="mrp_workcenter_2"/>
<field name="time_cycle">120</field>
<field name="sequence">5</field>
<field name="name">Manual Assembly</field>
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
</record>
<record id="mrp_routing_workcenter_5" model="mrp.routing.workcenter">
<field name="routing_id" ref="mrp_routing_3"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="time_cycle">120</field>
<field name="sequence">10</field>
<field name="name">Assembly</field>
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
</record>
<!-- Resource: mrp.bom -->
<record id="product.product_product_3_product_template" model="product.template">
@@ -114,7 +37,14 @@
<field name="product_tmpl_id" ref="product.product_product_3_product_template"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="sequence">1</field>
<field name="routing_id" ref="mrp_routing_0"/>
</record>
<record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter">
<field name="bom_id" ref="mrp_bom_manufacture"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="name">Manual Assembly</field>
<field name="time_cycle">60</field>
<field name="sequence">5</field>
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
</record>
<record id="mrp_bom_manufacture_line_1" model="mrp.bom.line">
@@ -296,7 +226,14 @@
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="sequence">3</field>
<field name="consumption">flexible</field>
<field name="routing_id" ref="mrp_routing_3"/>
</record>
<record id="mrp_routing_workcenter_5" model="mrp.routing.workcenter">
<field name="bom_id" ref="mrp_bom_desk"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="time_cycle">120</field>
<field name="sequence">10</field>
<field name="name">Assembly</field>
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
</record>
<record id="mrp_bom_desk_line_1" model="mrp.bom.line">
@@ -350,8 +287,16 @@
<field name="product_tmpl_id" ref="product_product_computer_desk_head_product_template"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="sequence">1</field>
<field name="routing_id" ref="mrp_routing_0"/>
</record>
<record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter">
<field name="bom_id" ref="mrp_bom_table_top"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="name">Manual Assembly</field>
<field name="time_cycle">60</field>
<field name="sequence">5</field>
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
</record>
<record id="mrp_bom_line_wood_panel" model="mrp.bom.line">
<field name="product_id" ref="product_product_wood_panel"/>
<field name="product_qty">2</field>
@@ -371,7 +316,32 @@
<field name="product_tmpl_id" ref="product_product_plastic_laminate_product_template"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="sequence">1</field>
<field name="routing_id" ref="mrp_routing_1"/>
</record>
<record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter">
<field name="bom_id" ref="mrp_bom_plastic_laminate"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="name">Long time assembly</field>
<field name="time_cycle">180</field>
<field name="sequence">15</field>
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
</record>
<record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter">
<field name="bom_id" ref="mrp_bom_plastic_laminate"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="name">Testing</field>
<field name="time_cycle">60</field>
<field name="sequence">10</field>
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
</record>
<record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter">
<field name="bom_id" ref="mrp_bom_plastic_laminate"/>
<field name="workcenter_id" ref="mrp_workcenter_1"/>
<field name="name">Packing</field>
<field name="time_cycle">30</field>
<field name="sequence">5</field>
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
</record>
<record id="mrp_bom_line_plastic_laminate" model="mrp.bom.line">
<field name="product_id" ref="product_product_ply_veneer"/>
@@ -596,9 +566,34 @@
<field name="product_tmpl_id" ref="product.product_product_27_product_template"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="sequence">2</field>
<field name="routing_id" ref="mrp_routing_1"/>
<field name="code">SEC-ASSEM</field>
</record>
<record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter">
<field name="bom_id" ref="mrp_bom_laptop_cust_rout"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="name">Long time assembly</field>
<field name="time_cycle">180</field>
<field name="sequence">15</field>
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
</record>
<record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter">
<field name="bom_id" ref="mrp_bom_laptop_cust_rout"/>
<field name="workcenter_id" ref="mrp_workcenter_3"/>
<field name="name">Testing</field>
<field name="time_cycle">60</field>
<field name="sequence">10</field>
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
</record>
<record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter">
<field name="bom_id" ref="mrp_bom_laptop_cust_rout"/>
<field name="workcenter_id" ref="mrp_workcenter_1"/>
<field name="name">Packing</field>
<field name="time_cycle">30</field>
<field name="sequence">5</field>
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
</record>
<record id="mrp_bom_laptop_cust_rout_line_1" model="mrp.bom.line">
<field name="product_id" ref="product_product_drawer_drawer"/>
<field name="product_qty">1</field>
+33 -19
View File
@@ -48,10 +48,7 @@ class MrpBom(models.Model):
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]")
product_uom_category_id = fields.Many2one(related='product_tmpl_id.uom_id.category_id')
sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of bills of material.")
routing_id = fields.Many2one(
'mrp.routing', 'Routing', check_company=True,
help="The operations for producing this BoM. When a routing is specified, the production orders will "
" be executed through work orders, otherwise everything is processed in the production order itself. ")
operation_ids = fields.One2many('mrp.routing.workcenter', 'bom_id', 'Operations', copy=True)
ready_to_produce = fields.Selection([
('all_available', ' When all components are available'),
('asap', 'When components for 1st operation are available')], string='Manufacturing Readiness',
@@ -119,11 +116,6 @@ class MrpBom(models.Model):
for line in self.bom_line_ids:
line.bom_product_template_attribute_value_ids = False
@api.onchange('routing_id')
def onchange_routing_id(self):
for line in self.bom_line_ids:
line.operation_id = False
@api.model
def name_create(self, name):
# prevent to use string as product_tmpl_id
@@ -261,12 +253,6 @@ class MrpBomLine(models.Model):
sequence = fields.Integer(
'Sequence', default=1,
help="Gives the sequence order when displaying.")
routing_id = fields.Many2one(
'mrp.routing', 'Routing',
related='bom_id.routing_id', store=True, readonly=False,
help="The list of operations to produce the finished product. The routing is mainly used to "
"compute work center costs during operations and to plan future loads on work centers "
"based on production planning.")
bom_id = fields.Many2one(
'mrp.bom', 'Parent BoM',
index=True, ondelete='cascade', required=True)
@@ -276,9 +262,10 @@ class MrpBomLine(models.Model):
'product.template.attribute.value', string="Apply on Variants", ondelete='restrict',
domain="[('id', 'in', possible_bom_product_template_attribute_value_ids)]",
help="BOM Product Variants needed to apply this line.")
allowed_operation_ids = fields.Many2many('mrp.routing.workcenter', compute='_compute_allowed_operation_ids')
operation_id = fields.Many2one(
'mrp.routing.workcenter', 'Consumed in Operation', check_company=True,
domain="[('routing_id', '=', routing_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]",
domain="[('id', 'in', allowed_operation_ids)]",
help="The operation where the components are consumed, or the finished products created.")
child_bom_id = fields.Many2one(
'mrp.bom', 'Sub BoM', compute='_compute_child_bom_id')
@@ -327,6 +314,20 @@ class MrpBomLine(models.Model):
for line in self:
line.child_line_ids = line.child_bom_id.bom_line_ids.ids or False
@api.depends('bom_id')
def _compute_allowed_operation_ids(self):
for bom_line in self:
if not bom_line.bom_id.operation_ids:
bom_line.allowed_operation_ids = self.env['mrp.routing.workcenter']
else:
operation_domain = [
('id', 'in', bom_line.bom_id.operation_ids.ids),
'|',
('company_id', '=', bom_line.company_id.id),
('company_id', '=', False)
]
bom_line.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain)
@api.onchange('product_uom_id')
def onchange_product_uom_id(self):
res = {}
@@ -403,11 +404,24 @@ class MrpByProduct(models.Model):
default=1.0, digits='Product Unit of Measure', required=True)
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True)
bom_id = fields.Many2one('mrp.bom', 'BoM', ondelete='cascade')
routing_id = fields.Many2one(
'mrp.routing', 'Routing', store=True, related='bom_id.routing_id')
allowed_operation_ids = fields.Many2many('mrp.routing.workcenter', compute='_compute_allowed_operation_ids')
operation_id = fields.Many2one(
'mrp.routing.workcenter', 'Produced in Operation', check_company=True,
domain="[('routing_id', '=', routing_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]")
domain="[('id', 'in', allowed_operation_ids)]")
@api.depends('bom_id')
def _compute_allowed_operation_ids(self):
for byproduct in self:
if not byproduct.bom_id.operation_ids:
byproduct.allowed_operation_ids = self.env['mrp.routing.workcenter']
else:
operation_domain = [
('id', 'in', byproduct.bom_id.operation_ids.ids),
'|',
('company_id', '=', byproduct.company_id.id),
('company_id', '=', False)
]
byproduct.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain)
@api.onchange('product_id')
def onchange_product_id(self):
+25 -34
View File
@@ -130,12 +130,7 @@ class MrpProduction(models.Model):
('type', '=', 'normal')]""",
check_company=True,
help="Bill of Materials allow you to define the list of required components to make a finished product.")
routing_id = fields.Many2one(
'mrp.routing', 'Routing',
readonly=True, compute='_compute_routing', store=True,
help="The list of operations (list of work centers) to produce the finished product. The routing "
"is mainly used to compute work center costs during operations and to plan future loads on "
"work centers based on production planning.")
bom_has_operations = fields.Boolean(compute='_compute_bom_has_operations')
state = fields.Selection([
('draft', 'Draft'),
@@ -277,6 +272,14 @@ class MrpProduction(models.Model):
def _set_date_planned_finished(self):
self.move_finished_ids.write({'date_expected': self.date_planned_finished})
@api.depends('bom_id')
def _compute_bom_has_operations(self):
for production in self:
if not production.bom_id:
production.bom_has_operations = False
else:
production.bom_has_operations = len(production.bom_id.operation_ids) > 0
@api.depends('move_raw_ids.delay_alert_date')
def _compute_delay_alert_date(self):
delay_alert_date_data = self.env['stock.move'].read_group([('id', 'in', self.move_raw_ids.ids), ('delay_alert_date', '!=', False)], ['delay_alert_date:max'], 'raw_material_production_id')
@@ -367,14 +370,6 @@ class MrpProduction(models.Model):
for production in self:
production.finished_move_line_ids = production.move_finished_ids.mapped('move_line_ids')
@api.depends('bom_id.routing_id', 'bom_id.routing_id.operation_ids')
def _compute_routing(self):
for production in self:
if production.bom_id.routing_id.operation_ids:
production.routing_id = production.bom_id.routing_id.id
else:
production.routing_id = False
@api.depends('workorder_ids')
def _compute_workorder_count(self):
data = self.env['mrp.workorder'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id'])
@@ -418,7 +413,7 @@ class MrpProduction(models.Model):
production.state = 'done'
elif production.move_finished_ids.filtered(lambda m: m.state not in ('cancel', 'done') and m.product_id.id == production.product_id.id)\
and (production.qty_produced >= production.product_qty)\
and (not production.routing_id or all(wo_state in ('cancel', 'done') for wo_state in production.workorder_ids.mapped('state'))):
and (not production.bom_id.operation_ids or all(wo_state in ('cancel', 'done') for wo_state in production.workorder_ids.mapped('state'))):
production.state = 'to_close'
elif production.workorder_ids and any(wo_state in ('progress') for wo_state in production.workorder_ids.mapped('state'))\
or production.qty_produced > 0 and production.qty_produced < production.product_qty:
@@ -435,7 +430,7 @@ class MrpProduction(models.Model):
if production.state not in ('draft', 'done', 'cancel'):
relevant_move_state = production.move_raw_ids._get_relevant_state_among_moves()
if relevant_move_state == 'partially_available':
if production.routing_id and production.routing_id.operation_ids and production.bom_id.ready_to_produce == 'asap':
if production.bom_id.operation_ids and production.bom_id.ready_to_produce == 'asap':
production.reservation_state = production._get_ready_to_produce_state()
else:
production.reservation_state = 'confirmed'
@@ -516,7 +511,7 @@ class MrpProduction(models.Model):
@api.onchange('date_planned_start')
def _onchange_date_planned_start(self):
if not self.routing_id:
if not self.bom_id.operation_ids:
self.date_planned_finished = self.date_planned_start + datetime.timedelta(hours=1)
@api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id')
@@ -537,7 +532,7 @@ class MrpProduction(models.Model):
else:
self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)]
@api.onchange('location_src_id', 'move_raw_ids', 'routing_id')
@api.onchange('location_src_id', 'move_raw_ids', 'bom_id')
def _onchange_location(self):
source_location = self.location_src_id
self.move_raw_ids.update({
@@ -567,7 +562,7 @@ class MrpProduction(models.Model):
raise UserError(_('You cannot move a planned manufacturing order.'))
if 'move_raw_ids' in vals and production.state != 'draft':
production._autoconfirm_production()
if not production.routing_id and vals.get('date_planned_start') and not vals.get('date_planned_finished'):
if not production.bom_id.operation_ids and vals.get('date_planned_start') and not vals.get('date_planned_finished'):
new_date_planned_start = fields.Datetime.to_datetime(vals.get('date_planned_start'))
if not production.date_planned_finished or new_date_planned_start >= production.date_planned_finished:
production.date_planned_finished = new_date_planned_start + datetime.timedelta(hours=1)
@@ -738,14 +733,12 @@ class MrpProduction(models.Model):
def _get_ready_to_produce_state(self):
""" returns 'assigned' if enough components are reserved in order to complete
the first operation in the routing. If not returns 'waiting'
the first operation of the bom. If not returns 'waiting'
"""
self.ensure_one()
first_operation = self.routing_id.operation_ids[0]
# Get BoM line related to first opeation in rounting. If there is only
# one opeation in the routing then it will need all BoM lines.
first_operation = self.bom_id.operation_ids[0]
bom_line_ids = self.env['mrp.bom.line']
if len(self.routing_id.operation_ids) == 1:
if len(self.bom_id.operation_ids) == 1:
moves_in_first_operation = self.move_raw_ids
else:
moves_in_first_operation = self.move_raw_ids.filtered(lambda move: move.operation_id == first_operation)
@@ -852,7 +845,7 @@ class MrpProduction(models.Model):
def button_plan(self):
""" Create work orders. And probably do stuff, like things. """
orders_to_plan = self.filtered(lambda order: order.routing_id and order.state == 'confirmed')
orders_to_plan = self.filtered(lambda order: order.bom_id.operation_ids and order.state == 'confirmed')
for order in orders_to_plan:
order.move_raw_ids.filtered(lambda m: m.state == 'draft')._action_confirm()
# `propagate_date` enables the automatic rescheduling which could lead to hard to
@@ -952,8 +945,8 @@ class MrpProduction(models.Model):
workorders = self.env['mrp.workorder']
original_one = False
for bom, bom_data in exploded_boms:
# If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing
if bom.routing_id.id and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.routing_id.id != bom.routing_id.id):
# If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders.
if bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids):
temp_workorders = self._workorders_create(bom, bom_data)
workorders += temp_workorders
if temp_workorders: # In order to avoid two "ending work orders"
@@ -975,7 +968,7 @@ class MrpProduction(models.Model):
if self.product_id.tracking == 'serial':
quantity = 1.0
for operation in bom.routing_id.operation_ids:
for operation in bom.operation_ids:
workorder = workorders.create({
'name': operation.name,
'production_id': self.id,
@@ -994,9 +987,7 @@ class MrpProduction(models.Model):
# get the raw moves to attach to this operation
moves_raw = self.env['stock.move']
for move in self.move_raw_ids:
if move.operation_id == operation and move.bom_line_id.bom_id.routing_id == bom.routing_id:
moves_raw |= move
if move.operation_id == operation and not move.bom_line_id:
if move.operation_id == operation:
moves_raw |= move
moves_finished = self.move_finished_ids.filtered(lambda move: move.operation_id == operation)
@@ -1004,9 +995,9 @@ class MrpProduction(models.Model):
# be consumed at the last workorder of the linked routing.
# - Raw moves from a BoM where no rounting was set should be consumed at the last
# workorder of the main routing.
if len(workorders) == len(bom.routing_id.operation_ids):
moves_raw |= self.move_raw_ids.filtered(lambda move: not move.operation_id and move.bom_line_id.bom_id.routing_id == bom.routing_id)
moves_raw |= self.move_raw_ids.filtered(lambda move: not move.workorder_id and not move.bom_line_id.bom_id.routing_id)
if len(workorders) == len(bom.operation_ids):
moves_raw |= self.move_raw_ids.filtered(lambda move: not move.operation_id and move.bom_line_id.bom_id.operation_ids and move.bom_line_id.bom_id == bom)
moves_raw |= self.move_raw_ids.filtered(lambda move: not move.workorder_id and not move.bom_line_id.bom_id.operation_ids)
moves_finished |= self.move_finished_ids.filtered(lambda move: move.product_id != self.product_id and not move.operation_id)
+4 -31
View File
@@ -4,32 +4,6 @@
from odoo import api, fields, models, _
class MrpRouting(models.Model):
""" Specifies routings of work centers """
_name = 'mrp.routing'
_description = 'Routings'
name = fields.Char('Routing', required=True)
active = fields.Boolean(
'Active', default=True,
help="If the active field is set to False, it will allow you to hide the routing without removing it.")
code = fields.Char(
'Reference',
copy=False, default=lambda self: _('New'), readonly=True)
note = fields.Text('Description')
operation_ids = fields.One2many(
'mrp.routing.workcenter', 'routing_id', 'Operations',
copy=True)
company_id = fields.Many2one(
'res.company', 'Company', default=lambda self: self.env.company)
@api.model
def create(self, vals):
if 'code' not in vals or vals['code'] == _('New'):
vals['code'] = self.env['ir.sequence'].next_by_code('mrp.routing') or _('New')
return super(MrpRouting, self).create(vals)
class MrpRoutingWorkcenter(models.Model):
_name = 'mrp.routing.workcenter'
_description = 'Work Center Usage'
@@ -41,14 +15,13 @@ class MrpRoutingWorkcenter(models.Model):
sequence = fields.Integer(
'Sequence', default=100,
help="Gives the sequence order when displaying a list of routing Work Centers.")
routing_id = fields.Many2one(
'mrp.routing', 'Parent Routing',
bom_id = fields.Many2one(
'mrp.bom', 'Bill of Material',
index=True, ondelete='cascade', required=True,
help="The routing contains all the Work Centers used and for how long. This will create work orders afterwards "
"which alters the execution of the manufacturing order.")
help="The Bill of Material this operation is linked to")
company_id = fields.Many2one(
'res.company', 'Company',
readonly=True, related='routing_id.company_id', store=True)
readonly=True, related='bom_id.company_id', store=True)
worksheet_type = fields.Selection([
('pdf', 'PDF'), ('google_slide', 'Google Slide'), ('text', 'Text')],
string="Work Sheet", default="pdf",
+20 -2
View File
@@ -76,10 +76,10 @@ class StockMove(models.Model):
'mrp.unbuild', 'Disassembly Order', check_company=True)
consume_unbuild_id = fields.Many2one(
'mrp.unbuild', 'Consumed Disassembly Order', check_company=True)
allowed_operation_ids = fields.Many2many('mrp.routing.workcenter', compute='_compute_allowed_operation_ids')
operation_id = fields.Many2one(
'mrp.routing.workcenter', 'Operation To Consume', check_company=True,
domain="[('routing_id', '=', routing_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]")
routing_id = fields.Many2one(related='raw_material_production_id.routing_id')
domain="[('id', 'in', allowed_operation_ids)]")
workorder_id = fields.Many2one(
'mrp.workorder', 'Work Order To Consume', check_company=True)
# Quantities to process, in normalized UoMs
@@ -118,6 +118,24 @@ class StockMove(models.Model):
move.order_finished_lot_ids = False
move.finished_lots_exist = False
@api.depends('raw_material_production_id.bom_id')
def _compute_allowed_operation_ids(self):
for move in self:
if (
not move.raw_material_production_id or
not move.raw_material_production_id.bom_id or not
move.raw_material_production_id.bom_id.operation_ids
):
move.allowed_operation_ids = self.env['mrp.routing.workcenter']
else:
operation_domain = [
('id', 'in', move.raw_material_production_id.bom_id.operation_ids.ids),
'|',
('company_id', '=', self.company_id.id),
('company_id', '=', False)
]
move.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain)
@api.depends('product_id.tracking')
def _compute_needs_lots(self):
for move in self:
@@ -55,7 +55,7 @@ class ReportBomStructure(models.AbstractModel):
@api.model
def get_operations(self, bom_id=False, qty=0, level=0):
bom = self.env['mrp.bom'].browse(bom_id)
lines = self._get_operation_line(bom.routing_id, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
lines = self._get_operation_line(bom, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
values = {
'bom_id': bom_id,
'currency': self.env.company.currency_id,
@@ -106,7 +106,7 @@ class ReportBomStructure(models.AbstractModel):
else:
product = bom.product_tmpl_id
attachments = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', product.id)])
operations = self._get_operation_line(bom.routing_id, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0)
operations = self._get_operation_line(bom, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0)
company = bom.company_id or self.env.company
lines = {
'bom': bom,
@@ -163,10 +163,10 @@ class ReportBomStructure(models.AbstractModel):
total += sub_total
return components, total
def _get_operation_line(self, routing, qty, level):
def _get_operation_line(self, bom, qty, level):
operations = []
total = 0.0
for operation in routing.operation_ids:
for operation in bom.operation_ids:
operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP')
duration_expected = operation_cycle * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour)
@@ -181,7 +181,7 @@ class ReportBomStructure(models.AbstractModel):
def _get_price(self, bom, factor, product):
price = 0
if bom.routing_id:
if bom.operation_ids:
# routing are defined on a BoM and don't have a concept of quantity.
# It means that the operation time are defined for the quantity on
# the BoM (the user produces a batch of products). E.g the user
@@ -189,7 +189,7 @@ class ReportBomStructure(models.AbstractModel):
# will be the 5 for a quantity between 1-10, then doubled for
# 11-20,...
operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP')
operations = self._get_operation_line(bom.routing_id, operation_cycle, 0)
operations = self._get_operation_line(bom, operation_cycle, 0)
price += sum([op['total'] for op in operations])
for line in bom.bom_line_ids:
-2
View File
@@ -4,14 +4,12 @@ access_mrp_workcenter_productivity_loss,mrp.workcenter.productivity.loss,model_m
access_mrp_workcenter_productivity_loss_type,mrp.workcenter.productivity.loss.type,model_mrp_workcenter_productivity_loss_type,mrp.group_mrp_user,1,0,0,0
access_mrp_workcenter_productivity,mrp.workcenter.productivity,model_mrp_workcenter_productivity,mrp.group_mrp_user,1,1,1,1
access_mrp_workcenter,mrp.workcenter,model_mrp_workcenter,mrp.group_mrp_user,1,0,0,0
access_mrp_routing,mrp.routing,model_mrp_routing,mrp.group_mrp_user,1,0,0,0
access_mrp_routing_workcenter,mrp.routing.workcenter,model_mrp_routing_workcenter,mrp.group_mrp_user,1,0,0,0
access_mrp_bom,mrp.bom,model_mrp_bom,group_mrp_user,1,0,0,0
access_mrp_bom_line,mrp.bom.line,model_mrp_bom_line,group_mrp_user,1,0,0,0
access_mrp_bom_byproduct_user,mrp.bom.byproduct,model_mrp_bom_byproduct,mrp.group_mrp_user,1,0,0,0
access_mrp_production,mrp.production user,model_mrp_production,mrp.group_mrp_user,1,1,1,1
access_mrp_workcenter_manager,mrp.workcenter.manager,model_mrp_workcenter,mrp.group_mrp_manager,1,1,1,1
access_mrp_routing_manager,mrp.routing.manager,model_mrp_routing,mrp.group_mrp_manager,1,1,1,1
access_mrp_routing_workcenter_manager,mrp.routing.workcenter.manager,model_mrp_routing_workcenter,mrp.group_mrp_manager,1,1,1,1
access_mrp_bom_manager,mrp.bom.manager,model_mrp_bom,mrp.group_mrp_manager,1,1,1,1
access_mrp_bom_line_manager,mrp.bom.line.manager,model_mrp_bom_line,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
4 access_mrp_workcenter_productivity_loss_type mrp.workcenter.productivity.loss.type model_mrp_workcenter_productivity_loss_type mrp.group_mrp_user 1 0 0 0
5 access_mrp_workcenter_productivity mrp.workcenter.productivity model_mrp_workcenter_productivity mrp.group_mrp_user 1 1 1 1
6 access_mrp_workcenter mrp.workcenter model_mrp_workcenter mrp.group_mrp_user 1 0 0 0
access_mrp_routing mrp.routing model_mrp_routing mrp.group_mrp_user 1 0 0 0
7 access_mrp_routing_workcenter mrp.routing.workcenter model_mrp_routing_workcenter mrp.group_mrp_user 1 0 0 0
8 access_mrp_bom mrp.bom model_mrp_bom group_mrp_user 1 0 0 0
9 access_mrp_bom_line mrp.bom.line model_mrp_bom_line group_mrp_user 1 0 0 0
10 access_mrp_bom_byproduct_user mrp.bom.byproduct model_mrp_bom_byproduct mrp.group_mrp_user 1 0 0 0
11 access_mrp_production mrp.production user model_mrp_production mrp.group_mrp_user 1 1 1 1
12 access_mrp_workcenter_manager mrp.workcenter.manager model_mrp_workcenter mrp.group_mrp_manager 1 1 1 1
access_mrp_routing_manager mrp.routing.manager model_mrp_routing mrp.group_mrp_manager 1 1 1 1
13 access_mrp_routing_workcenter_manager mrp.routing.workcenter.manager model_mrp_routing_workcenter mrp.group_mrp_manager 1 1 1 1
14 access_mrp_bom_manager mrp.bom.manager model_mrp_bom mrp.group_mrp_manager 1 1 1 1
15 access_mrp_bom_line_manager mrp.bom.line.manager model_mrp_bom_line mrp.group_mrp_manager 1 1 1 1
-6
View File
@@ -78,12 +78,6 @@
<field name="domain_force">['|',('company_id', 'in', company_ids),('company_id','=',False)]</field>
</record>
<record model="ir.rule" id="mrp_routing_rule">
<field name="name">mrp_routing multi-company</field>
<field name="model_id" search="[('model','=','mrp.routing')]" model="ir.model"/>
<field name="domain_force">['|',('company_id', 'in', company_ids),('company_id','=',False)]</field>
</record>
<record model="ir.rule" id="mrp_routing_workcenter_rule">
<field name="name">mrp_routing_workcenter multi-company</field>
<field name="model_id" search="[('model','=','mrp.routing.workcenter')]" model="ir.model"/>
+11 -31
View File
@@ -91,40 +91,14 @@ class TestMrpCommon(common2.TestStockCommon):
'time_stop': 5,
'time_efficiency': 80,
})
cls.routing_1 = cls.env['mrp.routing'].create({
'name': 'Simple Line',
})
cls.routing_2 = cls.env['mrp.routing'].create({
'name': 'Complicated Line',
})
cls.operation_1 = cls.env['mrp.routing.workcenter'].create({
'name': 'Gift Wrap Maching',
'workcenter_id': cls.workcenter_1.id,
'routing_id': cls.routing_1.id,
'time_cycle': 15,
'sequence': 1,
})
cls.operation_2 = cls.env['mrp.routing.workcenter'].create({
'name': 'Cutting Machine',
'workcenter_id': cls.workcenter_1.id,
'routing_id': cls.routing_2.id,
'time_cycle': 12,
'sequence': 1,
})
cls.operation_3 = cls.env['mrp.routing.workcenter'].create({
'name': 'Weld Machine',
'workcenter_id': cls.workcenter_1.id,
'routing_id': cls.routing_2.id,
'time_cycle': 18,
'sequence': 2,
})
cls.bom_1 = cls.env['mrp.bom'].create({
'product_id': cls.product_4.id,
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
'product_uom_id': cls.uom_unit.id,
'product_qty': 4.0,
'routing_id': cls.routing_2.id,
'operation_ids': [
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
@@ -135,7 +109,9 @@ class TestMrpCommon(common2.TestStockCommon):
'product_tmpl_id': cls.product_5.product_tmpl_id.id,
'product_uom_id': cls.product_5.uom_id.id,
'product_qty': 1.0,
'routing_id': cls.routing_1.id,
'operation_ids': [
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
],
'type': 'phantom',
'sequence': 2,
'bom_line_ids': [
@@ -147,13 +123,17 @@ class TestMrpCommon(common2.TestStockCommon):
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
'product_uom_id': cls.uom_dozen.id,
'product_qty': 2.0,
'routing_id': cls.routing_2.id,
'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}),
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.product_5.id, 'product_qty': 2}),
(0, 0, {'product_id': cls.product_4.id, 'product_qty': 8}),
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 12})
]})
cls.stock_location_14 = cls.env['stock.location'].create({
'name': 'Shelf 2',
'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
@@ -177,4 +157,4 @@ class TestMrpCommon(common2.TestStockCommon):
'type': 'product',
'tracking': 'none',
'categ_id': cls.env.ref('product.product_category_all').id,
})
})
+34 -35
View File
@@ -26,9 +26,14 @@ class TestBoM(TestMrpCommon):
'product_tmpl_id': self.product_7_template.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 4.0,
'routing_id': self.routing_2.id,
'type': 'normal',
})
test_bom.write({
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
],
})
test_bom_l1 = self.env['mrp.bom.line'].create({
'bom_id': test_bom.id,
'product_id': self.product_2.id,
@@ -78,9 +83,13 @@ class TestBoM(TestMrpCommon):
'product_tmpl_id': self.product_5.product_tmpl_id.id,
'product_uom_id': self.product_5.uom_id.id,
'product_qty': 1.0,
'routing_id': self.routing_1.id,
'type': 'phantom'
})
test_bom_1.write({
'operation_ids': [
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
],
})
test_bom_1_l1 = self.env['mrp.bom.line'].create({
'bom_id': test_bom_1.id,
'product_id': self.product_3.id,
@@ -92,9 +101,14 @@ class TestBoM(TestMrpCommon):
'product_tmpl_id': self.product_7_template.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 4.0,
'routing_id': self.routing_2.id,
'type': 'normal',
})
test_bom_2.write({
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
]
})
test_bom_2_l1 = self.env['mrp.bom.line'].create({
'bom_id': test_bom_2.id,
'product_id': self.product_2.id,
@@ -278,6 +292,11 @@ class TestBoM(TestMrpCommon):
bom_form_crumble.product_uom_id = uom_kg
bom_crumble = bom_form_crumble.save()
workcenter = self.env['mrp.workcenter'].create({
'costs_hour': 10,
'name': 'Deserts Table'
})
with Form(bom_crumble) as bom:
with bom.bom_line_ids.new() as line:
line.product_id = butter
@@ -287,32 +306,19 @@ class TestBoM(TestMrpCommon):
line.product_id = biscuit
line.product_uom_id = uom_kg
line.product_qty = 6
workcenter = self.env['mrp.workcenter'].create({
'costs_hour': 10,
'name': 'Deserts Table'
})
routing_form = Form(self.env['mrp.routing'])
routing_form.name = "Crumble process"
routing_crumble = routing_form.save()
with Form(routing_crumble) as routing:
with routing.operation_ids.new() as operation:
with bom.operation_ids.new() as operation:
operation.workcenter_id = workcenter
operation.name = 'Prepare biscuits'
operation.time_cycle_manual = 5
with routing.operation_ids.new() as operation:
with bom.operation_ids.new() as operation:
operation.workcenter_id = workcenter
operation.name = 'Prepare butter'
operation.time_cycle_manual = 3
with routing.operation_ids.new() as operation:
with bom.operation_ids.new() as operation:
operation.workcenter_id = workcenter
operation.name = 'Mix manually'
operation.time_cycle_manual = 5
bom_crumble.routing_id = routing_crumble.id
# TEST BOM STRUCTURE VALUE WITH BOM QUANTITY
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=11, searchVariant=False)
# 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
@@ -381,6 +387,13 @@ class TestBoM(TestMrpCommon):
bom_form_cheese_cake.product_uom_id = self.uom_unit
bom_cheese_cake = bom_form_cheese_cake.save()
workcenter_2 = self.env['mrp.workcenter'].create({
'name': 'cake mounting',
'costs_hour': 20,
'time_start': 10,
'time_stop': 15
})
with Form(bom_cheese_cake) as bom:
with bom.bom_line_ids.new() as line:
line.product_id = cream
@@ -390,29 +403,15 @@ class TestBoM(TestMrpCommon):
line.product_id = crumble
line.product_uom_id = uom_kg
line.product_qty = 5.4
workcenter_2 = self.env['mrp.workcenter'].create({
'name': 'cake mounting',
'costs_hour': 20,
'time_start': 10,
'time_stop': 15
})
routing_form = Form(self.env['mrp.routing'])
routing_form.name = "Cheese cake process"
routing_cheese = routing_form.save()
with Form(routing_cheese) as routing:
with routing.operation_ids.new() as operation:
with bom.operation_ids.new() as operation:
operation.workcenter_id = workcenter
operation.name = 'Mix cheese and crumble'
operation.time_cycle_manual = 10
with routing.operation_ids.new() as operation:
with bom.operation_ids.new() as operation:
operation.workcenter_id = workcenter_2
operation.name = 'Cake mounting'
operation.time_cycle_manual = 5
bom_cheese_cake.routing_id = routing_cheese.id
# TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False)
+5 -1
View File
@@ -87,7 +87,11 @@ class TestMrpCancelMO(TestMrpCommon):
mo_data = self.generate_mo()
manufacturing_order = mo_data[0]
bom = mo_data[1]
bom.routing_id = self.routing_1
bom.write({
'operation_ids': [
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
],
})
manufacturing_order.button_plan()
workorder = manufacturing_order.workorder_ids
-19
View File
@@ -177,25 +177,6 @@ class TestMrpMulticompany(common.TransactionCase):
with self.assertRaises(UserError):
produce_form.save()
def test_workcenter_1(self):
"""Check it is not possible to use a routing of Company B in a
workcenter of Company A. """
workcenter = self.env['mrp.workcenter'].create({
'name': 'WC1',
'company_id': self.company_a.id,
'resource_calendar_id': self.company_a.resource_calendar_id.id,
})
with self.assertRaises(UserError):
self.env['mrp.routing'].create({
'name': 'WC1',
'company_id': self.company_b.id,
'operation_ids': [(0, 0, {
'name': 'operation_1',
'workcenter_id': workcenter.id,
})]
})
def test_partner_1(self):
""" On a product without company, as a user of Company B, check it is not possible to use a
location limited to Company A as `property_stock_production` """
-16
View File
@@ -63,7 +63,6 @@ class TestMrpOrder(TestMrpCommon):
test_date_planned = Dt.now() - timedelta(days=1)
test_quantity = 2.0
self.bom_1.routing_id = False
man_order_form = Form(self.env['mrp.production'].with_user(self.user_mrp_user))
man_order_form.product_id = self.product_4
man_order_form.bom_id = self.bom_1
@@ -149,15 +148,6 @@ class TestMrpOrder(TestMrpCommon):
# check sub product availability state is assigned
self.assertEqual(production_2.reservation_state, 'assigned', 'Production order should be availability for assigned state')
def test_empty_routing(self):
""" Check what happens when you work with an empty routing"""
routing = self.env['mrp.routing'].create({'name': 'Routing without operations'})
self.bom_3.routing_id = routing.id
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.product_6
production = production_form.save()
self.assertEqual(production.routing_id.id, False, 'The routing field should be empty on the mo')
def test_split_move_line(self):
""" Consume more component quantity than the initial demand.
It should create extra move and share the quantity between the two stock
@@ -911,8 +901,6 @@ class TestMrpOrder(TestMrpCommon):
bp.product_qty = 2.0
bp.product_uom_id = dozen
self.bom_1.routing_id = False
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_4
mo_form.bom_id = self.bom_1
@@ -1231,14 +1219,10 @@ class TestMrpOrder(TestMrpCommon):
'uom_id': unit.id,
'uom_po_id': unit.id,
})
routing = self.env['mrp.routing'].create({
'name': 'Secondary Assembly',
})
bom = self.env['mrp.bom'].create({
'product_tmpl_id': plastic_laminate.product_tmpl_id.id,
'product_uom_id': unit.id,
'sequence': 1,
'routing_id': routing.id,
'bom_line_ids': [(0, 0, {
'product_id': ply_veneer.id,
'product_qty': 1,
+8 -2
View File
@@ -340,7 +340,10 @@ class TestProcurement(TestMrpCommon):
'product_tmpl_id': parent_product.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 4.0,
'routing_id': self.routing_2.id,
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
],
'type': 'normal',
})
self.env['mrp.bom.line'].create({
@@ -353,7 +356,10 @@ class TestProcurement(TestMrpCommon):
'product_tmpl_id': child_product.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 4.0,
'routing_id': self.routing_2.id,
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
],
'type': 'normal',
})
self.env['mrp.bom.line'].create({
+3 -8
View File
@@ -26,13 +26,6 @@ class TestWarehouse(common.TestMrpCommon):
'name': 'Assembly Line 1',
'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id,
})
mrp_routing = self.env['mrp.routing'].create({
'name': 'Primary Assembly',
'operation_ids': [(0, 0, {
'workcenter_id': mrp_workcenter.id,
'name': 'Manual Assembly',
})]
})
inventory = self.env['stock.inventory'].create({
'name': 'Initial inventory',
'line_ids': [(0, 0, {
@@ -54,7 +47,9 @@ class TestWarehouse(common.TestMrpCommon):
'product_qty': 1,
'product_uom_id': unit.id
})],
'routing_id': mrp_routing.id
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
],
})
def new_mo_laptop(self):
+150 -117
View File
@@ -36,7 +36,9 @@ class TestWorkOrderProcessCommon(TestMrpCommon):
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
'product_uom_id': cls.uom_unit.id,
'product_qty': 4.0,
'routing_id': cls.routing_1.id,
'operation_ids': [
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
@@ -70,44 +72,41 @@ class TestWorkOrderProcessCommon(TestMrpCommon):
'name': 'Assembly Line 1',
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
})
cls.routing = cls.env['mrp.routing'].create({
'name': 'Assemble Furniture',
'operation_ids': [(0, 0, {
'workcenter_id': cls.mrp_workcenter.id,
'name': 'Manual Assembly',
})]
})
cls.mrp_bom_desk = cls.env['mrp.bom'].create({
'product_tmpl_id': cls.dining_table.product_tmpl_id.id,
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
'sequence': 3,
'consumption': 'flexible',
'routing_id': cls.routing.id,
'operation_ids': [
(0, 0, {'workcenter_id': cls.mrp_workcenter.id, 'name': 'Manual Assembly'}),
],
})
cls.mrp_bom_desk.write({
'bom_line_ids': [
(0, 0, {
'product_id': cls.product_table_sheet.id,
'product_qty': 1,
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
'sequence': 1,
'operation_id': cls.routing.operation_ids.id}),
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
(0, 0, {
'product_id': cls.product_table_leg.id,
'product_qty': 4,
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
'sequence': 2,
'operation_id': cls.routing.operation_ids.id}),
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
(0, 0, {
'product_id': cls.product_bolt.id,
'product_qty': 4,
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
'sequence': 3,
'operation_id': cls.routing.operation_ids.id}),
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
(0, 0, {
'product_id': cls.product_screw.id,
'product_qty': 10,
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
'sequence': 4,
'operation_id': cls.routing.operation_ids.id}),
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
]
})
cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({
@@ -118,26 +117,6 @@ class TestWorkOrderProcessCommon(TestMrpCommon):
'name': 'Assembly Line 1',
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
})
cls.routing_1 = cls.env['mrp.routing'].create({
'name': 'Secondary Assembly',
'operation_ids': [
(0, 0, {
'workcenter_id': cls.mrp_workcenter_1.id,
'name': 'Packing',
'time_cycle': 30,
'sequence': 5}),
(0, 0, {
'workcenter_id': cls.mrp_workcenter_3.id,
'name': 'Testing',
'time_cycle': 60,
'sequence': 10}),
(0, 0, {
'workcenter_id': cls.mrp_workcenter_3.id,
'name': 'Long time assembly',
'time_cycle': 180,
'sequence': 15}),
]
})
class TestWorkOrderProcess(TestWorkOrderProcessCommon):
@@ -270,11 +249,30 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
self.env['stock.move'].search([('product_id', '=', product_bolt.id)])._do_unreserve()
bom.routing_id = self.routing_1
bom.operation_ids = False
bom.write({
'operation_ids': [
(0, 0, {
'workcenter_id': self.mrp_workcenter_1.id,
'name': 'Packing',
'time_cycle': 30,
'sequence': 5}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Testing',
'time_cycle': 60,
'sequence': 10}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Long time assembly',
'time_cycle': 180,
'sequence': 15}),
]
})
bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_sheet).operation_id = bom.routing_id.operation_ids[0]
bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_leg).operation_id = bom.routing_id.operation_ids[1]
bom.bom_line_ids.filtered(lambda p: p.product_id == product_bolt).operation_id = bom.routing_id.operation_ids[2]
bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_sheet).operation_id = bom.operation_ids[0]
bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_leg).operation_id = bom.operation_ids[1]
bom.bom_line_ids.filtered(lambda p: p.product_id == product_bolt).operation_id = bom.operation_ids[2]
production_table_form = Form(self.env['mrp.production'])
production_table_form.product_id = dining_table
@@ -399,8 +397,9 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
# Set manual time cycle 20 and 10.
# --------------------------------
self.operation_1.write({'time_cycle_manual': 20})
(self.operation_2 | self.operation_3).write({'time_cycle_manual': 10})
self.bom_3.operation_ids[0].time_cycle_manual = 10
self.bom_3.operation_ids[1].time_cycle_manual = 10
self.bom_2.operation_ids.time_cycle_manual = 20
man_order_form = Form(self.env['mrp.production'])
man_order_form.product_id = self.product_6
@@ -484,9 +483,10 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
# - kit bom: Stone Tool: 1 operation
# operation 1: Gift Wrapping
workorders = man_order.workorder_ids
kit_wo = man_order.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_1)
door_wo_1 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_2)
door_wo_2 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_3)
kit_wo = man_order.workorder_ids.filtered(lambda wo: wo.operation_id.name == "Gift Wrap Maching")
door_wo_1 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id.name == "Cutting Machine")
door_wo_2 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id.name == "Weld Machine")
for workorder in workorders:
self.assertEqual(workorder.workcenter_id, self.workcenter_1, "Workcenter does not match.")
self.assertEqual(kit_wo.state, 'ready', "Workorder should be in ready state.")
@@ -524,7 +524,6 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
door_wo_2.record_production()
self.assertEqual(door_wo_2.state, 'done', "Workorder should be in done state.")
def test_01_without_workorder(self):
""" Testing consume quants and produced quants without workorder """
unit = self.ref("uom.product_uom_unit")
@@ -816,7 +815,6 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
laptop = self.laptop
graphics_card = self.graphics_card
unit = self.env.ref("uom.product_uom_unit")
three_step_routing = self.routing_1
laptop.tracking = 'serial'
@@ -829,7 +827,23 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
'product_qty': 1,
'product_uom_id': unit.id
})],
'routing_id': three_step_routing.id
'operation_ids': [
(0, 0, {
'workcenter_id': self.mrp_workcenter_1.id,
'name': 'Packing',
'time_cycle': 30,
'sequence': 5}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Testing',
'time_cycle': 60,
'sequence': 10}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Long time assembly',
'time_cycle': 180,
'sequence': 15}),
],
})
mo_laptop_form = Form(self.env['mrp.production'])
@@ -891,7 +905,23 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
'product_tmpl_id': drawer.product_tmpl_id.id,
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
'sequence': 2,
'routing_id': self.routing_1.id,
'operation_ids': [
(0, 0, {
'workcenter_id': self.mrp_workcenter_1.id,
'name': 'Packing',
'time_cycle': 30,
'sequence': 5}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Testing',
'time_cycle': 60,
'sequence': 10}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Long time assembly',
'time_cycle': 180,
'sequence': 15}),
],
'bom_line_ids': [(0, 0, {
'product_id': drawer_drawer.id,
'product_qty': 1,
@@ -1156,10 +1186,14 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
calendar wc1 : [q1][q2]
calendar wc2 : [q1][q2]"""
self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
self.planning_bom.routing_id = self.routing_2
self.planning_bom.operation_ids = False
self.planning_bom.write({
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1, 'batch': 'yes', 'batch_size': 1}),
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
],
})
# Allow second workorder to start once the first one is not ended yet
self.operation_2.batch = 'yes'
self.operation_2.batch_size = 1
self.env['mrp.workcenter'].search([]).write({'capacity': 1})
# workcenters work 24/7
self.full_availability()
@@ -1175,8 +1209,8 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
self.assertEqual(mo.workorder_ids[0].workcenter_id, self.wc_alt_2, "wrong workcenter")
self.assertEqual(mo.workorder_ids[1].workcenter_id, self.wc_alt_1, "wrong workcenter")
duration1 = self.operation_2.time_cycle * 100.0 / self.wc_alt_2.time_efficiency + self.wc_alt_2.time_start
duration2 = 2.0 * self.operation_2.time_cycle * 100.0 / self.wc_alt_1.time_efficiency + self.wc_alt_1.time_start + self.wc_alt_1.time_stop
duration1 = self.planning_bom.operation_ids[0].time_cycle * 100.0 / self.wc_alt_2.time_efficiency + self.wc_alt_2.time_start
duration2 = 2.0 * self.planning_bom.operation_ids[0].time_cycle * 100.0 / self.wc_alt_1.time_efficiency + self.wc_alt_1.time_start + self.wc_alt_1.time_stop
wo2_start = mo.workorder_ids[1].date_planned_start
wo2_stop = mo.workorder_ids[1].date_planned_finished
@@ -1262,7 +1296,6 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
""" Marking a workorder as done before the theoretical date should update
the reservation slot in the calendar the be able to reserve the next
production sooner """
self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
self.env['mrp.workcenter'].search([]).write({'tz': 'UTC'}) # compute all date in UTC
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_4
@@ -1274,7 +1307,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
mo.button_plan()
wo = mo.workorder_ids
self.assertAlmostEqual(wo.date_planned_start, datetime(2019, 5, 13, 9, 0, 0, 0), delta=timedelta(seconds=10))
self.assertAlmostEqual(wo.date_planned_finished, datetime(2019, 5, 13, 9, 0, 0, 0) + timedelta(minutes=85.58), delta=timedelta(seconds=10))
self.assertAlmostEqual(wo.date_planned_finished, datetime(2019, 5, 13, 9, 0, 0, 0) + timedelta(minutes=60), delta=timedelta(seconds=10))
wo.button_start()
wo.record_production()
# Marking workorder as done should change the finished date
@@ -1302,7 +1335,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
self.workcenter_1.time_efficiency = 100
self.workcenter_1.time_start = 0
self.workcenter_1.time_stop = 0
self.routing_1.operation_ids.time_cycle = 60
self.planning_bom.operation_ids.time_cycle = 60
self.product_4.tracking = 'serial'
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_4
@@ -1372,7 +1405,23 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
'product_tmpl_id': self.product_5.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'routing_id': self.routing_1.id,
'operation_ids': [
(0, 0, {
'workcenter_id': self.mrp_workcenter_1.id,
'name': 'Packing',
'time_cycle': 30,
'sequence': 5}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Testing',
'time_cycle': 60,
'sequence': 10}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Long time assembly',
'time_cycle': 180,
'sequence': 15}),
],
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': self.product_1.id, 'product_qty': 1}),
@@ -1403,14 +1452,32 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
def test_conflict_and_replan(self):
""" TEST Json data conflicted and the replan button of a workorder """
self.routing_1.operation_ids[0].write({'workcenter_id': self.mrp_workcenter_3.id})
dining_table = self.dining_table
bom = self.mrp_bom_desk
bom.routing_id = self.routing_1
bom.operation_ids = False
bom.write({
'operation_ids': [
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Packing',
'time_cycle': 30,
'sequence': 5}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Testing',
'time_cycle': 60,
'sequence': 10}),
(0, 0, {
'workcenter_id': self.mrp_workcenter_3.id,
'name': 'Long time assembly',
'time_cycle': 180,
'sequence': 15}),
]})
bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_table_sheet).operation_id = bom.routing_id.operation_ids[0].id
bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_table_leg).operation_id = bom.routing_id.operation_ids[1].id
bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_bolt).operation_id = bom.routing_id.operation_ids[2].id
bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_table_sheet).operation_id = bom.operation_ids[0].id
bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_table_leg).operation_id = bom.operation_ids[1].id
bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_bolt).operation_id = bom.operation_ids[2].id
production_table_form = Form(self.env['mrp.production'])
production_table_form.product_id = dining_table
@@ -1513,54 +1580,38 @@ class TestRoutingAndKits(SavepointCase):
cls.workcenter_kit1 = cls.env['mrp.workcenter'].create({
'name': 'workcenter2',
})
cls.routing_finished1 = cls.env['mrp.routing'].create({
'name': 'routing for finished1',
})
cls.operation_finished1 = cls.env['mrp.routing.workcenter'].create({
'sequence': 1,
'name': 'finished operation 1',
'workcenter_id': cls.workcenter_finished1.id,
'routing_id': cls.routing_finished1.id,
})
cls.operation_finished2 = cls.env['mrp.routing.workcenter'].create({
'sequence': 1,
'name': 'finished operation 2',
'workcenter_id': cls.workcenter_finished1.id,
'routing_id': cls.routing_finished1.id,
})
cls.routing_kit1 = cls.env['mrp.routing'].create({
'name': 'routing for kit1',
})
cls.operation_kit1 = cls.env['mrp.routing.workcenter'].create({
'name': 'Kit operation',
'workcenter_id': cls.workcenter_kit1.id,
'routing_id': cls.routing_kit1.id,
})
cls.bom_finished1 = cls.env['mrp.bom'].create({
'product_id': cls.finished1.id,
'product_tmpl_id': cls.finished1.product_tmpl_id.id,
'product_uom_id': cls.uom_unit.id,
'product_qty': 1,
'type': 'normal',
'routing_id': cls.routing_finished1.id,
'bom_line_ids': [
(0, 0, {'product_id': cls.compfinished1.id, 'product_qty': 1}),
(0, 0, {'product_id': cls.kit1.id, 'product_qty': 1}),
]})
],
'operation_ids': [
(0, 0, {'sequence': 1, 'name': 'finished operation 1', 'workcenter_id': cls.workcenter_finished1.id}),
(0, 0, {'sequence': 2, 'name': 'finished operation 2', 'workcenter_id': cls.workcenter_finished1.id}),
],
})
cls.bom_kit1 = cls.env['mrp.bom'].create({
'product_id': cls.kit1.id,
'product_tmpl_id': cls.kit1.product_tmpl_id.id,
'product_uom_id': cls.uom_unit.id,
'product_qty': 1,
'type': 'phantom',
'routing_id': cls.routing_kit1.id,
'bom_line_ids': [
(0, 0, {'product_id': cls.compkit1.id, 'product_qty': 1}),
]})
],
'operation_ids': [
(0, 0, {'name': 'Kit operation', 'workcenter_id': cls.workcenter_kit1.id})
]
})
def test_1(self):
"""Operations are set on `self.bom_kit1` but none on `self.bom_finished1`."""
self.bom_kit1.bom_line_ids.operation_id = self.operation_kit1
self.bom_kit1.bom_line_ids.operation_id = self.bom_kit1.operation_ids[0]
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.finished1
@@ -1594,9 +1645,9 @@ class TestRoutingAndKits(SavepointCase):
def test_3(self):
"""Operations are set both `self.bom_kit1` and `self.bom_finished1`."""
self.bom_kit1.bom_line_ids.operation_id = self.operation_kit1
self.bom_finished1.bom_line_ids[0].operation_id = self.operation_finished1
self.bom_finished1.bom_line_ids[1].operation_id = self.operation_finished2
self.bom_kit1.bom_line_ids.operation_id = self.bom_kit1.operation_ids
self.bom_finished1.bom_line_ids[0].operation_id = self.bom_finished1.operation_ids[0]
self.bom_finished1.bom_line_ids[1].operation_id = self.bom_finished1.operation_ids[1]
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.finished1
@@ -1653,12 +1704,12 @@ class TestRoutingAndKits(SavepointCase):
def test_5(self):
# Main bom: set the normal component to the first of the two operations of the routing.
bomline_compfinished = self.bom_finished1.bom_line_ids.filtered(lambda bl: bl.product_id == self.compfinished1)
bomline_compfinished.operation_id = self.operation_finished1
bomline_compfinished.operation_id = self.bom_finished1.operation_ids[0]
# Main bom: the kit do not have an operation set but there's one on its bom
bomline_kit1 = self.bom_finished1.bom_line_ids - bomline_compfinished
self.assertFalse(bomline_kit1.operation_id.id)
self.bom_kit1.bom_line_ids.operation_id = self.bom_kit1.routing_id.operation_ids
self.bom_kit1.bom_line_ids.operation_id = self.bom_kit1.operation_ids
# Main bom: add a kit without routing
kit2 = self.env['product.product'].create({
@@ -1693,28 +1744,10 @@ class TestRoutingAndKits(SavepointCase):
self.assertEqual(mo.workorder_ids[1].raw_workorder_line_ids.product_id, compkit2)
self.assertEqual(mo.workorder_ids[2].raw_workorder_line_ids.product_id, self.compkit1)
def test_6(self):
""" Use the same routing on `self.bom_fnished1` and `self.kit1`. The workorders should not
be duplicated.
"""
self.bom_finished1.bom_line_ids[0].operation_id = self.operation_finished1
self.bom_finished1.bom_line_ids[1].operation_id = self.operation_finished2
self.bom_kit1.routing_id = self.routing_finished1
self.bom_kit1.bom_line_ids.operation_id = self.operation_finished1
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.finished1
mo_form.bom_id = self.bom_finished1
mo_form.product_qty = 1.0
mo = mo_form.save()
mo.action_confirm()
mo.button_plan()
self.assertEqual(len(mo.workorder_ids), 2)
self.assertEqual(set(mo.workorder_ids[0].raw_workorder_line_ids.product_id.ids), set([self.compfinished1.id, self.compkit1.id]))
self.assertFalse(mo.workorder_ids[1].raw_workorder_line_ids.product_id.id)
# -------------------------------------------------------------------------
# Those 2 next tests aren't related to routing and kit but to flexible
# consumption.
# -------------------------------------------------------------------------
def test_merge_lot(self):
""" Produce 10 units of product tracked by lot on two workorder. On the
first one, produce 4 onto lot1 then 6 onto lot1 as well. The second
@@ -1768,7 +1801,7 @@ class TestRoutingAndKits(SavepointCase):
move.location_id = mo.location_src_id
move.location_dest_id = mo.production_location_id
move.product_uom_qty = 2
move.operation_id = mo.routing_id.operation_ids[0]
move.operation_id = mo.bom_id.operation_ids[0]
mo = mo_form.save()
self.assertEqual(len(mo.move_raw_ids), 3)
mo.action_confirm()
@@ -1802,7 +1835,7 @@ class TestRoutingAndKits(SavepointCase):
move.location_id = mo.location_src_id
move.location_dest_id = mo.production_location_id
move.product_uom_qty = 2
move.operation_id = mo.routing_id.operation_ids[0]
move.operation_id = mo.bom_id.operation_ids[0]
mo = mo_form.save()
new_move = mo.move_raw_ids.filtered(lambda move: move.additional)
self.assertEqual(len(mo.move_raw_ids), 3)
+20 -9
View File
@@ -14,7 +14,7 @@
<field name="arch" type="xml">
<form string="Byproduct">
<group>
<field name="routing_id" invisible="1"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="company_id"/>
<field name="product_id"/>
<label for="product_qty"/>
@@ -36,6 +36,11 @@
<form string="Bill of Material">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(action_mrp_routing_time)d" type="action" class="oe_stat_button" icon="fa-clock-o" groups="mrp.group_mrp_routings">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Routing<br/>Performance</span>
</div>
</button>
<button name="%(action_report_mrp_bom)d" type="action"
class="oe_stat_button" icon="fa-bars" string="Structure &amp; Cost"/>
</div>
@@ -51,7 +56,6 @@
<field name="product_qty"/>
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/>
</div>
<field name="routing_id" attrs="{'invisible': [('type','not in',('normal','phantom'))]}" groups="mrp.group_mrp_routings" context="{'default_company_id': company_id}"/>
</group>
<group>
<field name="code"/>
@@ -72,10 +76,9 @@
</group>
<notebook>
<page string="Components" name="components">
<field name="bom_line_ids" widget="one2many" context="{'default_parent_product_tmpl_id': product_tmpl_id, 'default_product_id': False, 'default_company_id': company_id, 'default_routing_id': routing_id}">
<field name="bom_line_ids" widget="one2many" context="{'default_parent_product_tmpl_id': product_tmpl_id, 'default_product_id': False, 'default_company_id': company_id, 'default_bom_id': id}">
<tree string="Components" editable="bottom">
<field name="company_id" invisible="1"/>
<field name="routing_id" invisible="1"/>
<field name="sequence" widget="handle"/>
<field name="product_id" context="{'default_type': 'product'}"/>
<field name="product_tmpl_id" invisible="1"/>
@@ -88,6 +91,7 @@
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/>
<field name="bom_product_template_attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="operation_id" groups="mrp.group_mrp_routings" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" options="{'no_quick_create':True,'no_create_edit':True}"/>
</tree>
</field>
@@ -96,13 +100,13 @@
name="by_products"
attrs="{'invisible': [('type','!=','normal')]}"
groups="mrp.group_mrp_byproducts">
<field name="byproduct_ids" context="{'form_view_ref' : 'mrp.mrp_bom_byproduct_form_view', 'default_company_id': company_id, 'default_routing_id': routing_id}">
<field name="byproduct_ids" context="{'form_view_ref' : 'mrp.mrp_bom_byproduct_form_view', 'default_company_id': company_id, 'default_bom_id': id}">
<tree string="By-products" editable="top">
<field name="company_id" invisible="1"/>
<field name="routing_id" invisible="1"/>
<field name="product_id" context="{'default_type': 'product'}"/>
<field name="product_qty"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="operation_id" groups="mrp.group_mrp_routings" options="{'no_quick_create':True,'no_create_edit':True}"/>
</tree>
</field>
@@ -118,6 +122,15 @@
</group>
</group>
</page>
<page string="Operations"
name="operations"
attrs="{'invisible': [('type','not in',('normal','phantom'))]}"
groups="mrp.group_mrp_routings">
<field name="operation_ids"
attrs="{'invisible': [('type','not in',('normal','phantom'))]}"
groups="mrp.group_mrp_routings"
context="{'default_company_id': company_id}"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
@@ -142,7 +155,6 @@
<field name="company_id" groups="base.group_multi_company" optional="show"/>
<field name="product_qty" optional="show"/>
<field name="product_uom_id" groups="uom.group_uom" optional="show" string="Unit of Measure"/>
<field name="routing_id" groups="mrp.group_mrp_routings" optional="show"/>
</tree>
</field>
</record>
@@ -187,7 +199,6 @@
<filter string="Product" name="product" domain="[]" context="{'group_by': 'product_tmpl_id'}"/>
<filter string='BoM Type' name="group_by_type" domain="[]" context="{'group_by' : 'type'}"/>
<filter string='Unit of Measure' name="default_unit_of_measure" domain="[]" context="{'group_by' : 'product_uom_id'}"/>
<filter string="Routing" name="routings" domain="[]" context="{'group_by': 'routing_id'}"/>
</group>
</search>
</field>
@@ -239,8 +250,8 @@
</group>
<group string="Operation">
<field name="company_id" invisible="1"/>
<field name="routing_id" invisible="1"/>
<field name="sequence" groups="base.group_no_one"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="operation_id" groups="mrp.group_mrp_routings"/>
</group>
</group>
+15 -19
View File
@@ -17,7 +17,6 @@
<field name="bom_id" readonly="1" optional="hide"/>
<field name="origin" optional="show"/>
<field name="user_id" optional="hide" widget="many2one_avatar_user"/>
<field name="routing_id" groups="mrp.group_mrp_routings" optional="show"/>
<field name="reservation_state" optional="show"/>
<field name="product_qty" sum="Total Qty" string="Quantity" readonly="1" optional="show"/>
<field name="company_id" readonly="1" groups="base.group_multi_company" optional="show"/>
@@ -45,14 +44,15 @@
<field name="arch" type="xml">
<form string="Manufacturing Orders">
<header>
<field name="bom_has_operations" invisible="1"/>
<field name="confirm_cancel" invisible="1"/>
<button name="button_mark_done" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/>
<button name="action_confirm" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('is_locked', '=', False)]}" string="Mark as Todo" type="object" class="oe_highlight"/>
<button name="action_assign" attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object" class="oe_highlight"/>
<button name="button_plan" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('routing_id', '=', False)]}" type="object" string="Plan" class="oe_highlight"/>
<button name="button_plan" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('bom_has_operations', '=', False)]}" type="object" string="Plan" class="oe_highlight"/>
<button name="button_unplan" type="object" string="Unplan" attrs="{'invisible': ['|', '|', ('state', '!=', 'planned'), ('date_planned_start', '=', False), ('date_planned_finished', '=', False)]}"/>
<button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', '!=', 'assigned'), ('routing_id', '!=', False)]}" string="Produce" type="object" class="oe_highlight"/>
<button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', 'not in', ('confirmed', 'waiting')), ('routing_id', '!=', False)]}" string="Produce" type="object"/>
<button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', '!=', 'assigned'), ('bom_has_operations', '!=', False)]}" string="Produce" type="object" class="oe_highlight"/>
<button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', 'not in', ('confirmed', 'waiting')), ('bom_has_operations', '!=', False)]}" string="Produce" type="object"/>
<button name="post_inventory" string="Post Inventory" type="object" attrs="{'invisible': [('post_visible', '=', False)]}"/>
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('cancel', 'draft')), ('is_locked', '=', False)]}"/>
<button name="button_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}"/>
@@ -129,20 +129,19 @@
</div>
<field name="bom_id"
context="{'default_product_tmpl_id': product_tmpl_id}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="routing_id" groups="mrp.group_mrp_routings"/>
</group>
<group>
<field name="date_deadline" attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}"/>
<div class="o_row o_td_label">
<label for="date_planned_start" string="Planned Date"
attrs="{'invisible': [
('routing_id', '!=', False),
('bom_has_operations', '!=', False),
('state', 'in', ['draft', 'confirmed'])
]}"/>
<label for="date_planned_start" string="Plan from"
attrs="{'invisible': [
'|',
('routing_id', '=', False),
('bom_has_operations', '=', False),
('state', 'not in', ['draft', 'confirmed'])
]}"/>
</div>
@@ -151,27 +150,27 @@
attrs="{'readonly': [
'|',
'&amp;',
('routing_id', '=', False),
('bom_has_operations', '=', False),
('state', 'in', ['done', 'cancel']),
'&amp;',
('routing_id', '!=', False),
('bom_has_operations', '!=', False),
('state', 'not in', ['draft', 'confirmed'])]}"/>
<label for="date_planned_finished" string="to" attrs="{'invisible': [
'|',
('id', '=', False),
'&amp;',
('routing_id', '!=', False),
('bom_has_operations', '!=', False),
('state', 'in', ['draft', 'confirmed'])
]}"/>
<field name="date_planned_finished" required="1"
attrs="{'readonly': [
'|',
'&amp;',
('routing_id', '=', False),
('bom_has_operations', '=', False),
('state', 'in', ['done', 'cancel']),
('routing_id', '!=', False)
('bom_has_operations', '!=', False)
], 'invisible': [
('routing_id', '!=', False),
('bom_has_operations', '!=', False),
('state', 'in', ['draft', 'confirmed'])
]}"/>
<field name="delay_alert_date" invisible="1"/>
@@ -273,8 +272,7 @@
<field eval="2" name="priority"/>
<field name="arch" type="xml">
<calendar date_start="date_planned_start" date_stop="date_planned_finished"
string="Manufacturing Orders" color="routing_id" event_limit="5" quick_add="False">
<field name="routing_id" filters="1"/>
string="Manufacturing Orders" event_limit="5" quick_add="False">
<field name="user_id" avatar_field="image_128"/>
<field name="product_id"/>
<field name="product_qty"/>
@@ -286,7 +284,7 @@
<field name="name">mrp.production.gantt</field>
<field name="model">mrp.production</field>
<field name="arch" type="xml">
<gantt date_stop="date_finished" date_start="date_start" string="Productions" default_group_by="routing_id" create="0">
<gantt date_stop="date_finished" date_start="date_start" string="Productions" create="0">
</gantt>
</field>
</record>
@@ -319,8 +317,7 @@
<field name="name" string="Manufacturing Order" filter_domain="['|', ('name', 'ilike', self), ('origin', 'ilike', self)]"/>
<field name="product_id"/>
<field name="move_raw_ids" string="Component" filter_domain="[('move_raw_ids.product_id', 'ilike', self)]"/>
<field name="name" string="Work Center" filter_domain="[('routing_id.operation_ids.workcenter_id', 'ilike', self)]"/>
<field name="routing_id" groups="mrp.group_mrp_routings"/>
<field name="name" string="Work Center" filter_domain="[('bom_id.operation_ids.workcenter_id', 'ilike', self)]"/>
<field name="origin"/>
<filter string="To Do" name="todo" domain="[('state', 'in', ('draft', 'confirmed', 'planned','progress', 'to_close'))]"
help="Manufacturing Orders which are in confirmed state."/>
@@ -352,7 +349,6 @@
domain="[('activity_exception_decoration', '!=', False)]"/>
<group expand="0" string="Group By...">
<filter string="Product" name="product" domain="[]" context="{'group_by': 'product_id'}"/>
<filter string="Routing" name="routing" domain="[]" context="{'group_by': 'routing_id'}" groups="mrp.group_mrp_routings"/>
<filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/>
<filter string="Material Availability" name="groupby_reservation_state" domain="[]" context="{'group_by': 'reservation_state'}"/>
<filter string="Scheduled Date" name="scheduled_date" domain="[]" context="{'group_by': 'date_planned_start'}" help="Scheduled Date by Month"/>
-114
View File
@@ -21,7 +21,6 @@
<field name="arch" type="xml">
<form string="Routing Work Centers">
<sheet>
<field name="routing_id" invisible="1" required="0"/>
<group>
<group name="description">
<field name="name"/>
@@ -62,118 +61,5 @@
</form>
</field>
</record>
<!-- Routings -->
<record id="mrp_routing_form_view" model="ir.ui.view">
<field name="name">mrp.routing.form</field>
<field name="model">mrp.routing</field>
<field name="arch" type="xml">
<form string="Routing">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(action_mrp_routing_time)d" type="action" class="oe_stat_button" icon="fa-clock-o">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Time<br/> Analysis</span>
</div>
</button>
</div>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_title">
<h1>
<field name="code"/>
</h1>
</div>
<group>
<group>
<field name="name"/>
<field name="active" invisible="1"/>
</group>
<group>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Work Center Operations" name="work_center_operations">
<field name="operation_ids" context="{'default_routing_id': id}"/>
</page>
<page string="Notes" name="notes">
<field name="note"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="mrp_routing_tree_view" model="ir.ui.view">
<field name="name">mrp.routing.tree</field>
<field name="model">mrp.routing</field>
<field name="arch" type="xml">
<tree string="Routing">
<field name="code"/>
<field name="name"/>
<field name="active" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="mrp_routing_kanban_view" model="ir.ui.view">
<field name="name">mrp.routing.kanban</field>
<field name="model">mrp.routing</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="code"/>
<field name="name"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings mt4">
<strong class="o_kanban_record_title"><span><t t-esc="record.name.value"/></span></strong>
</div>
<span class="badge badge-pill"><field name="code"/></span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="mrp_routing_search_view" model="ir.ui.view">
<field name="name">mrp.routing.search</field>
<field name="model">mrp.routing</field>
<field name="arch" type="xml">
<search string="Routing">
<field name="name" string="Routing" filter_domain="['|', ('name', 'ilike', self), ('code', 'ilike', self)]"/>
<filter name="inactive" string="Archived" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<record id="mrp_routing_action" model="ir.actions.act_window">
<field name="name">Routings</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">mrp.routing</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" ref="mrp_routing_tree_view"/>
<field name="search_view_id" ref="mrp_routing_search_view"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new routing
</p><p>
Routings define the successive operations that need to be
done to realize a Manufacturing Order. Each operation from
a Routing is done at a specific Work Center and has a specific duration.
</p>
</field>
</record>
<menuitem id="menu_mrp_routing_action"
action="mrp_routing_action"
parent="menu_mrp_configuration"
groups="group_mrp_routings"
sequence="50"/>
</data>
</odoo>
+1 -1
View File
@@ -6,7 +6,7 @@
<field name="name">Manufacturing Orders</field>
<field name="res_model">mrp.production</field>
<field name="view_mode">tree,kanban,form,gantt</field>
<field name="domain">[('routing_id', '!=', False),('routing_id.operation_ids.workcenter_id','=', active_id)]</field>
<field name="domain">[('bom_id', '!=', False), ('bom_id.operation_ids.workcenter_id', '=', active_id)]</field>
</record>
<record id="action_work_orders" model="ir.actions.act_window">
+1 -1
View File
@@ -32,7 +32,7 @@
<field name="view_mode">graph,pivot,tree,form,gantt,calendar</field>
<field name="context">{'search_default_done': True}</field>
<field name="search_view_id" ref="view_mrp_production_work_order_search"/>
<field name="domain">[('production_id.routing_id', '=', active_id), ('state', '=', 'done')]</field>
<field name="domain">[('operation_id.bom_id', '=', active_id), ('state', '=', 'done')]</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No data to display
@@ -20,13 +20,10 @@
<label for="group_mrp_routings" string="Work Orders"/>
<a href="https://www.odoo.com/documentation/user/13.0/manufacturing/management/manufacturing_order.html#manage-manufacturing-with-routings-and-work-centers" title="Documentation" class="o_doc_link" target="_blank"></a>
<div class="text-muted">
Process operations at specific work centers based on the routing
Process operations at specific work centers
</div>
<div class="content-group" attrs="{'invisible': [('group_mrp_routings','=',False)]}">
<div class="mt8">
<div>
<button name="%(mrp.mrp_routing_action)d" icon="fa-arrow-right" type="action" string="Routings" class="btn-link"/>
</div>
<div>
<button name="%(mrp.mrp_workcenter_action)d" icon="fa-arrow-right" type="action" string="Work Centers" class="btn-link"/>
</div>
+2 -2
View File
@@ -31,8 +31,8 @@
<span> / </span>
<span><field name="reserved_availability" nolabel="1"/></span>
</div>
<field name="operation_id" attrs="{'invisible': ['|', ('parent.routing_id', '=', False), ('id', '!=', False)]}" groups="mrp.group_mrp_routings"/>
<field name="routing_id" invisible="1"/>
<field name="operation_id" attrs="{'invisible': [('id', '!=', False)]}" groups="mrp.group_mrp_routings"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="unit_factor" invisible="1"/>
<field name="additional" invisible="1"/>
<field name="company_id" invisible="1"/>
+1 -1
View File
@@ -80,7 +80,7 @@ class ChangeProductionQty(models.TransientModel):
production._log_manufacture_exception(documents)
operation_bom_qty = {}
for bom, bom_data in boms:
for operation in bom.routing_id.operation_ids:
for operation in bom.operation_ids:
operation_bom_qty[operation.id] = bom_data['qty']
finished_moves_modification = self._update_finished_moves(production, production.product_qty - qty_produced, old_production_qty)
production._log_downside_manufactured_quantity(finished_moves_modification)
+1 -1
View File
@@ -47,7 +47,7 @@ class ProductProduct(models.Model):
if not boms_to_recompute:
boms_to_recompute = []
total = 0
for opt in bom.routing_id.operation_ids:
for opt in bom.operation_ids:
duration_expected = (
opt.workcenter_id.time_start +
opt.workcenter_id.time_stop +
+57 -33
View File
@@ -19,7 +19,7 @@ class TestBom(common.TransactionCase):
super(TestBom, self).setUp()
self.Product = self.env['product.product']
self.Bom = self.env['mrp.bom']
self.Routing = self.env['mrp.routing']
#self.Routing = self.env['mrp.routing']
self.operation = self.env['mrp.routing.workcenter']
# Products.
@@ -117,37 +117,63 @@ class TestBom(common.TransactionCase):
workcenter_from1.costs_hour = 100
workcenter_1 = workcenter_from1.save()
routing_form1 = Form(self.Routing)
routing_form1.name = 'Assembly Furniture'
routing_1 = routing_form1.save()
self.bom_1.write({
'operation_ids': [
(0, 0, {
'name': 'Cutting',
'workcenter_id': workcenter_1.id,
'time_mode': 'manual',
'time_cycle_manual': 20,
'batch': 'no',
'sequence': 1,
}),
(0, 0, {
'name': 'Drilling',
'workcenter_id': workcenter_1.id,
'time_mode': 'manual',
'time_cycle_manual': 25,
'batch': 'no',
'sequence': 2,
}),
(0, 0, {
'name': 'Fitting',
'workcenter_id': workcenter_1.id,
'time_mode': 'manual',
'time_cycle_manual': 30,
'batch': 'no',
'sequence': 3,
}),
],
}),
self.bom_2.write({
'operation_ids': [
(0, 0, {
'name': 'Cutting',
'workcenter_id': workcenter_1.id,
'time_mode': 'manual',
'time_cycle_manual': 20,
'batch': 'no',
'sequence': 1,
}),
(0, 0, {
'name': 'Drilling',
'workcenter_id': workcenter_1.id,
'time_mode': 'manual',
'time_cycle_manual': 25,
'batch': 'no',
'sequence': 2,
}),
(0, 0, {
'name': 'Fitting',
'workcenter_id': workcenter_1.id,
'time_mode': 'manual',
'time_cycle_manual': 30,
'batch': 'no',
'sequence': 3,
}),
],
}),
operation_1 = self.operation.create({
'name': 'Cutting',
'workcenter_id': workcenter_1.id,
'routing_id': routing_1.id,
'time_mode': 'manual',
'time_cycle_manual': 20,
'batch': 'no',
'sequence': 1,
})
operation_2 = self.operation.create({
'name': 'Drilling',
'workcenter_id': workcenter_1.id,
'routing_id': routing_1.id,
'time_mode': 'manual',
'time_cycle_manual': 25,
'batch': 'no',
'sequence': 2,
})
operation_3 = self.operation.create({
'name': 'Fitting',
'workcenter_id': workcenter_1.id,
'routing_id': routing_1.id,
'time_mode': 'manual',
'time_cycle_manual': 30,
'batch': 'no',
'sequence': 3,
})
# -----------------------------------------------------------------
# Dinning Table Operation Cost(1 Unit)
@@ -160,7 +186,6 @@ class TestBom(common.TransactionCase):
# Operation Cost 1 unit = 125
# -----------------------------------------------------------------
self.bom_1.routing_id = routing_1.id
# --------------------------------------------------------------------------
# Table Head Operation Cost (1 Dozen)
@@ -173,7 +198,6 @@ class TestBom(common.TransactionCase):
# Operation Cost 1 dozen (125 per dozen) and 10.42 for 1 Unit
# --------------------------------------------------------------------------
self.bom_2.routing_id = routing_1.id
self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000")
self.dining_table.button_bom_cost()
@@ -66,7 +66,6 @@ class TestMrpAccount(TestWorkOrderProcessCommon):
})
inventory.action_validate
bom = self.mrp_bom_desk.copy()
bom.routing_id = False # TODO: extend the test later with the necessary operations
production_table_form = Form(self.env['mrp.production'])
production_table_form.product_id = self.dining_table
production_table_form.bom_id = bom
@@ -37,16 +37,12 @@ class TestStockLandedCostsMrp(StockAccountTestCommon):
'type': 'product',
'categ_id': cls.categ_all.id
})
cls.routing_1 = cls.env['mrp.routing'].create({
'name': 'Simple Line',
})
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
cls.bom_refri = cls.env['mrp.bom'].create({
'product_id': cls.product_refrigerator.id,
'product_tmpl_id': cls.product_refrigerator.product_tmpl_id.id,
'product_uom_id': cls.uom_unit.id,
'product_qty': 1.0,
'routing_id': cls.routing_1.id,
'type': 'normal',
})
cls.bom_refri_line1 = cls.env['mrp.bom.line'].create({
@@ -64,14 +64,6 @@ class TestStockProductionLot(TestStockCommon):
'time_stop': 5,
'time_efficiency': 80,
})
cls.routing = cls.env['mrp.routing'].create({'name': 'COOK'})
cls.operation = cls.env['mrp.routing.workcenter'].create({
'name': 'Bake in the oven',
'workcenter_id': cls.workcenter.id,
'routing_id': cls.routing.id,
'time_cycle': 15,
'sequence': 1,
})
def test_01_product_produce(self):
""" Checks user doesn't get a confirmation wizard when they produces with
@@ -122,7 +114,14 @@ class TestStockProductionLot(TestStockCommon):
""" Checks user doesn't get a confirmation wizard when they makes a
workorder without expired components. """
# Set a routing on the BOM.
self.bom_apple_pie.routing_id = self.routing
self.bom_apple_pie.write({
'operation_ids': [(0, 0, {
'name': 'Bake in the oven',
'workcenter_id': self.workcenter.id,
'time_cycle': 15,
'sequence': 1,
})],
})
# Creates the MO, starts it and plans the Work Order.
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_apple_pie
@@ -149,7 +148,14 @@ class TestStockProductionLot(TestStockCommon):
""" Checks user doesn't get a confirmation wizard when they makes a
workorder without expired components. """
# Set a routing on the BOM.
self.bom_apple_pie.routing_id = self.routing
self.bom_apple_pie.write({
'operation_ids': [(0, 0, {
'name': 'Bake in the oven',
'workcenter_id': self.workcenter.id,
'time_cycle': 15,
'sequence': 1,
})],
})
# Creates the MO, starts it and plans the Work Order.
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_apple_pie