[IMP] mrp,mrp_workorder: workorder scheduling revamp

Currently MRP only allows sequential scheduling of work orders.

This commit allows to specify the dependencies for work orders to be
scheduled in parallel or in a complex diagram.

By enabling the feature in the settings and at the bom level,
you can specify dependencies between operations:
on the operation form, select the operations on which the
current one depends.

The dependencies will be reflected in work orders during planning.

The workorders Gantt view has been adapted for you to visualize
and manipulate the dependencies.

Please note:
- work orders without predecessors will be started first
- they can all be done in parallel
- when all work orders of this step are finished, their successors
  will start
- and so on, step by step

Also, with the feature activated at the bom level and no dependency,
the parallel scheme will be applied (all operations at first step).

Finally, without activation at the bom level, the dependencies will
be created in order to satisfy the current sequential model.

Task: 2691328
Part-of: odoo/odoo#87131
This commit is contained in:
JF Aubert
2022-06-03 16:40:08 +02:00
parent fa111d449d
commit dbd09f16a2
12 changed files with 227 additions and 150 deletions
+19 -6
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError, ValidationError
from odoo.osv.expression import AND, NEGATIVE_TERM_OPERATORS
from odoo.tools import float_round
@@ -80,6 +80,9 @@ class MrpBom(models.Model):
possible_product_template_attribute_value_ids = fields.Many2many(
'product.template.attribute.value',
compute='_compute_possible_product_template_attribute_value_ids')
allow_operation_dependencies = fields.Boolean('Operation Dependencies',
help="Create operation level dependencies that will influence both planning and the status of work orders upon MO confirmation. If this feature is ticked, and nothing is specified, Odoo will assume that all operations can be started simultaneously."
)
_sql_constraints = [
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
@@ -177,11 +180,21 @@ class MrpBom(models.Model):
def copy(self, default=None):
res = super().copy(default)
for bom_line in res.bom_line_ids:
if bom_line.operation_id:
operation = res.operation_ids.filtered(lambda op: op._get_comparison_values() == bom_line.operation_id._get_comparison_values())
# Two operations could have the same values so we take the first one
bom_line.operation_id = operation[:1]
if self.operation_ids:
operations_mapping = {}
for original, copied in zip(self.operation_ids, res.operation_ids.sorted()):
operations_mapping[original] = copied
for bom_line in res.bom_line_ids:
if bom_line.operation_id:
bom_line.operation_id = operations_mapping[bom_line.operation_id]
for operation in self.operation_ids:
if operation.blocked_by_operation_ids:
copied_operation = operations_mapping[operation]
dependencies = []
for dependency in operation.blocked_by_operation_ids:
dependencies.append(Command.link(operations_mapping[dependency].id))
copied_operation.blocked_by_operation_ids = dependencies
return res
@api.model
+55 -61
View File
@@ -240,6 +240,7 @@ class MrpProduction(models.Model):
show_allocation = fields.Boolean(
compute='_compute_show_allocation',
help='Technical Field used to decide whether the button "Allocation" should be displayed.')
allow_workorder_dependencies = fields.Boolean('Allow Work Order Dependencies')
_sql_constraints = [
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
@@ -775,7 +776,7 @@ class MrpProduction(models.Model):
if any(field in ['move_raw_ids', 'move_finished_ids', 'workorder_ids'] for field in vals) and production.state != 'draft':
production._autoconfirm_production()
if production in production_to_replan:
production._plan_workorders(replan=True)
production._plan_workorders()
if production.state == 'done' and ('lot_producing_id' in vals or 'qty_producing' in vals):
finished_move_lines = production.move_finished_ids.filtered(
lambda move: move.product_id == production.product_id and move.state == 'done').mapped('move_line_ids')
@@ -836,6 +837,21 @@ class MrpProduction(models.Model):
default['move_raw_ids'] = [(0, 0, move.copy_data()[0]) for move in self.move_raw_ids.filtered(lambda m: m.product_qty != 0.0)]
return super(MrpProduction, self).copy_data(default=default)
def copy(self, default=None):
res = super().copy(default)
if self.workorder_ids.blocked_by_workorder_ids:
workorders_mapping = {}
for original, copied in zip(self.workorder_ids, res.workorder_ids.sorted()):
workorders_mapping[original] = copied
for workorder in self.workorder_ids:
if workorder.blocked_by_workorder_ids:
copied_workorder = workorders_mapping[workorder]
dependencies = []
for dependency in workorder.blocked_by_workorder_ids:
dependencies.append(Command.link(workorders_mapping[dependency].id))
copied_workorder.blocked_by_workorder_ids = dependencies
return res
def action_view_mo_delivery(self):
""" Returns an action that display picking related to manufacturing order.
It can either be a list view or in a form view (if there is only one picking to show).
@@ -1212,6 +1228,37 @@ class MrpProduction(models.Model):
self.filtered(lambda mo: mo.state == 'draft').state = 'confirmed'
return True
def _link_workorders_and_moves(self):
self.ensure_one()
if not self.workorder_ids:
return
workorder_per_operation = {workorder.operation_id: workorder for workorder in self.workorder_ids}
workorder_boms = self.workorder_ids.operation_id.bom_id
last_workorder_per_bom = defaultdict(lambda: self.env['mrp.workorder'])
self.allow_workorder_dependencies = self.bom_id.allow_operation_dependencies
if self.allow_workorder_dependencies:
for workorder in self.workorder_ids:
workorder.blocked_by_workorder_ids = [Command.link(workorder_per_operation[operation_id].id) for operation_id in workorder.operation_id.blocked_by_operation_ids]
if not workorder.needed_by_workorder_ids:
last_workorder_per_bom[workorder.operation_id.bom_id] = workorder
else:
previous_workorder = False
for workorder in self.workorder_ids:
if previous_workorder and previous_workorder.operation_id.bom_id == workorder.operation_id.bom_id:
workorder.blocked_by_workorder_ids = [Command.link(previous_workorder.id)]
previous_workorder = workorder
last_workorder_per_bom[workorder.operation_id.bom_id] = workorder
for move in (self.move_raw_ids | self.move_finished_ids):
if move.operation_id:
move.write({
'workorder_id': workorder_per_operation[move.operation_id].id
})
else:
bom = move.bom_line_id.bom_id if (move.bom_line_id and move.bom_line_id.bom_id in workorder_boms) else self.bom_id
move.write({
'workorder_id': last_workorder_per_bom[bom].id
})
def action_assign(self):
for production in self:
production.move_raw_ids._action_assign()
@@ -1237,65 +1284,17 @@ class MrpProduction(models.Model):
if not self.workorder_ids:
return
# Schedule all work orders (new ones and those already created)
qty_to_produce = max(self.product_qty - self.qty_produced, 0)
qty_to_produce = self.product_uom_id._compute_quantity(qty_to_produce, self.product_id.uom_id)
start_date = max(self.date_planned_start, datetime.datetime.now())
if replan:
workorder_ids = self.workorder_ids.filtered(lambda wo: wo.state in ('pending', 'waiting', 'ready'))
# We plan the manufacturing order according to its `date_planned_start`, but if
# `date_planned_start` is in the past, we plan it as soon as possible.
workorder_ids.leave_id.unlink()
else:
workorder_ids = self.workorder_ids.filtered(lambda wo: not wo.date_planned_start)
for workorder in workorder_ids:
workcenters = workorder.workcenter_id | workorder.workcenter_id.alternative_workcenter_ids
best_finished_date = datetime.datetime.max
vals = {}
for workcenter in workcenters:
# compute theoretical duration
if workorder.workcenter_id == workcenter:
duration_expected = workorder.duration_expected
else:
duration_expected = workorder._get_duration_expected(alternative_workcenter=workcenter)
self._link_workorders_and_moves()
from_date, to_date = workcenter._get_first_available_slot(start_date, duration_expected)
# If the workcenter is unavailable, try planning on the next one
if not from_date:
continue
# Check if this workcenter is better than the previous ones
if to_date and to_date < best_finished_date:
best_start_date = from_date
best_finished_date = to_date
best_workcenter = workcenter
vals = {
'workcenter_id': workcenter.id,
'duration_expected': duration_expected,
}
# Plan workorders starting from final ones (those with no dependent workorders)
final_workorders = self.workorder_ids.filtered(lambda wo: not wo.needed_by_workorder_ids)
for workorder in final_workorders:
workorder._plan_workorder(replan)
# If none of the workcenter are available, raise
if best_finished_date == datetime.datetime.max:
raise UserError(_('Impossible to plan the workorder. Please check the workcenter availabilities.'))
# Instantiate start_date for the next workorder planning
if workorder.next_work_order_id:
start_date = best_finished_date
# Create leave on chosen workcenter calendar
leave = self.env['resource.calendar.leaves'].create({
'name': workorder.display_name,
'calendar_id': best_workcenter.resource_calendar_id.id,
'date_from': best_start_date,
'date_to': best_finished_date,
'resource_id': best_workcenter.resource_id.id,
'time_type': 'other'
})
vals['leave_id'] = leave.id
workorder.write(vals)
self.with_context(force_date=True).write({
'date_planned_start': self.workorder_ids[0].date_planned_start,
'date_planned_finished': self.workorder_ids[-1].date_planned_finished
'date_planned_start': min([workorder.leave_id.date_from for workorder in self.workorder_ids]),
'date_planned_finished': max([workorder.leave_id.date_to for workorder in self.workorder_ids])
})
def button_unplan(self):
@@ -1697,7 +1696,6 @@ class MrpProduction(models.Model):
# backordered workorders. To do that, we use the original `duration_expected` and the
# ratio of the quantity produced and the quantity to produce.
workorders_to_cancel = self.env['mrp.workorder']
workorders_to_update = self.env['mrp.workorder']
for production in self:
initial_qty = initial_qty_by_production[production]
initial_workorder_remaining_qty = []
@@ -1717,13 +1715,9 @@ class MrpProduction(models.Model):
workorder.qty_reported_from_previous_wo = max(workorder.qty_production - remaining_qty, 0)
if remaining_qty:
initial_workorder_remaining_qty[index % workorders_len] = max(remaining_qty - workorder.qty_produced, 0)
if workorders_to_update[-1:].production_id != workorder.production_id:
workorders_to_update += workorder
else:
workorders_to_cancel += workorder
workorders_to_cancel.action_cancel()
for workorder in workorders_to_update:
workorder.state = 'ready' if workorder.next_work_order_id.production_availability == 'assigned' else 'waiting'
backorders.workorder_ids._action_confirm()
return self.env['mrp.production'].browse(production_ids)
+17
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _, tools
from odoo.exceptions import ValidationError
class MrpRoutingWorkcenter(models.Model):
@@ -48,6 +49,17 @@ class MrpRoutingWorkcenter(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.")
allow_operation_dependencies = fields.Boolean(related='bom_id.allow_operation_dependencies')
blocked_by_operation_ids = fields.Many2many('mrp.routing.workcenter', relation="mrp_routing_workcenter_dependencies_rel",
column1="operation_id", column2="blocked_by_id",
string="Blocked By", help="Operations that need to be completed before this operation can start.",
domain="[('allow_operation_dependencies', '=', True), ('id', '!=', id), ('bom_id', '=', bom_id)]",
copy=False)
needed_by_operation_ids = fields.Many2many('mrp.routing.workcenter', relation="mrp_routing_workcenter_dependencies_rel",
column1="blocked_by_id", column2="operation_id",
string="Blocks", help="Operations that cannot start before this operation is completed.",
domain="[('allow_operation_dependencies', '=', True), ('id', '!=', id), ('bom_id', '=', bom_id)]",
copy=False)
@api.depends('time_mode', 'time_mode_batch')
def _compute_time_computed_on(self):
@@ -89,6 +101,11 @@ class MrpRoutingWorkcenter(models.Model):
for operation in self:
operation.workorder_count = count_data.get(operation.id, 0)
@api.constrains('blocked_by_operation_ids')
def _check_no_cyclic_dependencies(self):
if not self._check_m2m_recursion('blocked_by_operation_ids'):
raise ValidationError(_("You cannot create cyclic dependency."))
def copy_to_bom(self):
if 'bom_id' in self.env.context:
bom_id = self.env.context.get('bom_id')
+84 -76
View File
@@ -7,7 +7,7 @@ from collections import defaultdict
import json
from odoo import api, fields, models, _, SUPERUSER_ID
from odoo.exceptions import UserError
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare, float_round, format_datetime
@@ -66,7 +66,7 @@ class MrpWorkorder(models.Model):
('done', 'Finished'),
('cancel', 'Cancelled')], string='Status',
compute='_compute_state', store=True,
default='pending', copy=False, readonly=True)
default='pending', copy=False, readonly=True, recursive=True)
leave_id = fields.Many2one(
'resource.calendar.leaves',
help='Slot into workcenter calendar once planned',
@@ -140,7 +140,6 @@ class MrpWorkorder(models.Model):
help='Technical field to store the hourly cost of workcenter at time of work order completion (i.e. to keep a consistent cost).',
default=0.0, group_operator="avg")
next_work_order_id = fields.Many2one('mrp.workorder', "Next Work Order", check_company=True)
scrap_ids = fields.One2many('stock.scrap', 'workorder_id')
scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move')
production_date = fields.Datetime('Production Date', related='production_id.date_planned_start', store=True)
@@ -149,15 +148,32 @@ class MrpWorkorder(models.Model):
consumption = fields.Selection(related='production_id.consumption')
qty_reported_from_previous_wo = fields.Float('Carried Quantity', digits='Product Unit of Measure', copy=False,
help="The quantity already produced awaiting allocation in the backorders chain.")
is_planned = fields.Boolean(related='production_id.is_planned')
allow_workorder_dependencies = fields.Boolean(related='production_id.allow_workorder_dependencies')
blocked_by_workorder_ids = fields.Many2many('mrp.workorder', relation="mrp_workorder_dependencies_rel",
column1="workorder_id", column2="blocked_by_id", string="Blocked By",
domain="[('allow_workorder_dependencies', '=', True), ('id', '!=', id), ('production_id', '=', production_id)]",
copy=False)
needed_by_workorder_ids = fields.Many2many('mrp.workorder', relation="mrp_workorder_dependencies_rel",
column1="blocked_by_id", column2="workorder_id", string="Blocks",
domain="[('allow_workorder_dependencies', '=', True), ('id', '!=', id), ('production_id', '=', production_id)]",
copy=False)
@api.depends('production_availability')
@api.depends('production_availability', 'blocked_by_workorder_ids', 'blocked_by_workorder_ids.state')
def _compute_state(self):
# Force the flush of the production_availability, the wo state is modify in the _compute_reservation_state
# It is a trick to force that the state of workorder is computed as the end of the
# cyclic depends with the mo.state, mo.reservation_state and wo.state
for workorder in self:
if workorder.state == 'pending':
if all([wo.state in ('done', 'cancel') for wo in workorder.blocked_by_workorder_ids]):
workorder.state = 'ready' if workorder.production_id.reservation_state == 'assigned' else 'waiting'
continue
if workorder.state not in ('waiting', 'ready'):
continue
if not all([wo.state in ('done', 'cancel') for wo in workorder.blocked_by_workorder_ids]):
workorder.state = 'pending'
continue
if workorder.production_id.reservation_state not in ('waiting', 'confirmed', 'assigned'):
continue
if workorder.production_id.reservation_state == 'assigned' and workorder.state == 'waiting':
@@ -167,15 +183,6 @@ class MrpWorkorder(models.Model):
@api.depends('production_state', 'date_planned_start', 'date_planned_finished')
def _compute_json_popover(self):
previous_wo_data = self.env['mrp.workorder'].read_group(
[('next_work_order_id', 'in', self.ids)],
['ids:array_agg(id)', 'date_planned_start:max', 'date_planned_finished:max'],
['next_work_order_id'])
previous_wo_dict = dict([(x['next_work_order_id'][0], {
'id': x['ids'][0],
'date_planned_start': x['date_planned_start'],
'date_planned_finished': x['date_planned_finished']})
for x in previous_wo_data])
if self.ids:
conflicted_dict = self._get_conflicted_workorder_ids()
for wo in self:
@@ -185,9 +192,9 @@ class MrpWorkorder(models.Model):
wo.json_popover = False
continue
if wo.state in ('pending', 'waiting', 'ready'):
previous_wo = previous_wo_dict.get(wo.id)
prev_start = previous_wo and previous_wo['date_planned_start'] or False
prev_finished = previous_wo and previous_wo['date_planned_finished'] or False
previous_wos = wo.blocked_by_workorder_ids
prev_start = min([workorder.date_planned_start for workorder in previous_wos]) if previous_wos else False
prev_finished = max([workorder.date_planned_finished for workorder in previous_wos]) if previous_wos else False
if wo.state == 'pending' and prev_start and not (prev_start > wo.date_planned_start):
infos.append({
'color': 'text-primary',
@@ -264,6 +271,11 @@ class MrpWorkorder(models.Model):
'date_to': date_to,
})
@api.constrains('blocked_by_workorder_ids')
def _check_no_cyclic_dependencies(self):
if not self._check_m2m_recursion('blocked_by_workorder_ids'):
raise ValidationError(_("You cannot create cyclic dependency."))
def name_get(self):
res = []
for wo in self:
@@ -279,13 +291,8 @@ class MrpWorkorder(models.Model):
self.mapped('leave_id').unlink()
mo_dirty = self.production_id.filtered(lambda mo: mo.state in ("confirmed", "progress", "to_close"))
previous_wos = self.env['mrp.workorder'].search([
('next_work_order_id', 'in', self.ids),
('id', 'not in', self.ids)
])
for pw in previous_wos:
while pw.next_work_order_id and pw.next_work_order_id in self:
pw.next_work_order_id = pw.next_work_order_id.next_work_order_id
for workorder in self:
workorder.blocked_by_workorder_ids.needed_by_workorder_ids = workorder.needed_by_workorder_ids
res = super().unlink()
# We need to go through `_action_confirm` for all workorders of the current productions to
# make sure the links between them are correct (`next_work_order_id` could be obsolete now).
@@ -456,61 +463,64 @@ class MrpWorkorder(models.Model):
return res
def _action_confirm(self):
workorders_by_production = defaultdict(lambda: self.env['mrp.workorder'])
for workorder in self:
workorders_by_production[workorder.production_id] |= workorder
for production, workorders in workorders_by_production.items():
workorders_by_bom = defaultdict(lambda: self.env['mrp.workorder'])
bom = self.env['mrp.bom']
moves = production.move_raw_ids | production.move_finished_ids
for workorder in workorders:
bom = workorder.operation_id.bom_id or workorder.production_id.bom_id
previous_workorder = workorders_by_bom[bom][-1:]
previous_workorder.next_work_order_id = workorder.id
workorders_by_bom[bom] |= workorder
moves.filtered(lambda m: m.operation_id == workorder.operation_id).write({
'workorder_id': workorder.id
})
exploded_boms, dummy = production.bom_id.explode(production.product_id, 1, picking_type=production.bom_id.picking_type_id)
exploded_boms = {b[0]: b[1] for b in exploded_boms}
for move in moves:
if move.workorder_id:
continue
bom = move.bom_line_id.bom_id
while bom and bom not in workorders_by_bom:
bom_data = exploded_boms.get(bom, {})
bom = bom_data.get('parent_line') and bom_data['parent_line'].bom_id or False
if bom in workorders_by_bom:
move.write({
'workorder_id': workorders_by_bom[bom][-1:].id
})
else:
move.write({
'workorder_id': workorders_by_bom[production.bom_id][-1:].id
})
for workorders in workorders_by_bom.values():
if not workorders:
continue
if workorders[0].state == 'pending':
workorders[0].state = 'ready' if workorders[0].production_availability == 'assigned' else 'waiting'
for workorder in workorders:
workorder._start_nextworkorder()
for production in self.mapped("production_id"):
production._link_workorders_and_moves()
def _get_byproduct_move_to_update(self):
return self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
def _start_nextworkorder(self):
if self.state == 'done':
next_order = self.next_work_order_id
while next_order and next_order.state == 'cancel':
next_order = next_order.next_work_order_id
if next_order.state == 'pending':
next_order.state = 'ready' if next_order.production_availability == 'assigned' else 'waiting'
def _plan_workorder(self, replan=False):
self.ensure_one()
# Plan workorder after its predecessors
start_date = max(self.production_id.date_planned_start, datetime.now())
for workorder in self.blocked_by_workorder_ids:
workorder._plan_workorder(replan)
start_date = max(start_date, workorder.date_planned_finished)
# Plan only suitable workorders
if self.state not in ['pending', 'waiting', 'ready']:
return
if self.date_planned_start:
if replan:
self.leave_id.unlink()
else:
return
# Consider workcenter and alternatives
workcenters = self.workcenter_id | self.workcenter_id.alternative_workcenter_ids
best_finished_date = datetime.max
vals = {}
for workcenter in workcenters:
# Compute theoretical duration
if self.workcenter_id == workcenter:
duration_expected = self.duration_expected
else:
duration_expected = self._get_duration_expected(alternative_workcenter=workcenter)
from_date, to_date = workcenter._get_first_available_slot(start_date, duration_expected)
# If the workcenter is unavailable, try planning on the next one
if not from_date:
continue
# Check if this workcenter is better than the previous ones
if to_date and to_date < best_finished_date:
best_start_date = from_date
best_finished_date = to_date
best_workcenter = workcenter
vals = {
'workcenter_id': workcenter.id,
'duration_expected': duration_expected,
}
# If none of the workcenter are available, raise
if best_finished_date == datetime.max:
raise UserError(_('Impossible to plan the workorder. Please check the workcenter availabilities.'))
# Create leave on chosen workcenter calendar
leave = self.env['resource.calendar.leaves'].create({
'name': self.display_name,
'calendar_id': best_workcenter.resource_calendar_id.id,
'date_from': best_start_date,
'date_to': best_finished_date,
'resource_id': best_workcenter.resource_id.id,
'time_type': 'other'
})
vals['leave_id'] = leave.id
self.write(vals)
@api.model
def gantt_unavailability(self, start_date, end_date, scale, group_bys=None, rows=None):
@@ -611,8 +621,6 @@ class MrpWorkorder(models.Model):
if not workorder.date_planned_start or end_date < workorder.date_planned_start:
vals['date_planned_start'] = end_date
workorder.write(vals)
workorder._start_nextworkorder()
return True
def end_previous(self, doall=False):
+7
View File
@@ -21,6 +21,13 @@ class ResConfigSettings(models.TransientModel):
implied_group='mrp.group_mrp_routings')
group_unlocked_by_default = fields.Boolean("Unlock Manufacturing Orders", implied_group='mrp.group_unlocked_by_default')
group_mrp_reception_report = fields.Boolean("Allocation Report for Manufacturing Orders", implied_group='mrp.group_mrp_reception_report')
group_mrp_workorder_dependencies = fields.Boolean("Work Order Dependencies", implied_group="mrp.group_mrp_workorder_dependencies")
def set_values(self):
super().set_values()
if not self.group_mrp_workorder_dependencies:
# Disabling this option should not interfere with currently planned productions
self.env['mrp.bom'].search([('allow_operation_dependencies', '=', True)]).allow_operation_dependencies = False
@api.onchange('use_manufacturing_lead')
def _onchange_use_manufacturing_lead(self):
+5
View File
@@ -44,6 +44,11 @@
<field name="implied_ids" eval="[(4, ref('mrp.group_mrp_manager'))]"/>
</record>
<record id="group_mrp_workorder_dependencies" model="res.groups">
<field name="name">Use Operation Dependencies</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</data>
<data noupdate="1">
<record id="base.default_user" model="res.users">
+1 -1
View File
@@ -2141,7 +2141,7 @@ class TestMrpOrder(TestMrpCommon):
mo.bom_id = bom
mo = mo.save()
self.assertEqual(list(mo.workorder_ids.mapped("state")), ["pending", "pending"])
self.assertEqual(list(mo.workorder_ids.mapped("state")), ["waiting", "waiting"])
mo.action_confirm()
mo.action_assign()
+1
View File
@@ -135,6 +135,7 @@
<group>
<field name="ready_to_produce" attrs="{'invisible': [('type','=','phantom')]}" string="Manufacturing Readiness" widget="radio" groups="mrp.group_mrp_routings"/>
<field name="consumption" attrs="{'invisible': [('type','=','phantom')]}" widget="radio"/>
<field name="allow_operation_dependencies" groups="mrp.group_mrp_workorder_dependencies"/>
</group>
<group>
<field name="picking_type_id" attrs="{'invisible': [('type','=','phantom')]}" string="Operation" groups="stock.group_adv_location"/>
+9 -2
View File
@@ -16,6 +16,7 @@
<field name="company_id" optional="show" groups="base.group_multi_company"/>
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
<field name="bom_product_template_attribute_value_ids" optional="hide" widget="many2many_tags" options="{'no_create': True}" groups="product.group_product_variant"/>
<field name="blocked_by_operation_ids" widget="many2many_tags" optional="hide" groups="mrp.group_mrp_workorder_dependencies"/>
</tree>
</field>
</record>
@@ -43,6 +44,11 @@
<xpath expr="//field[@name='bom_product_template_attribute_value_ids']" position="after">
<button name="action_archive" class="btn-link" type="object" string="Archive Operation" icon="fa-times"/>
</xpath>
<xpath expr="//field[@name='blocked_by_operation_ids']" position="replace">
</xpath>
<xpath expr="//field[@name='name']" position="after">
<field name="blocked_by_operation_ids" widget="many2many_tags" optional="hide" attrs="{'column_invisible': [('parent.allow_operation_dependencies', '=', False)]}"/>
</xpath>
</field>
</record>
@@ -77,11 +83,12 @@
<group name="description">
<field name="active" invisible="1"/>
<field name="name"/>
<field name="sequence" groups="base.group_no_one"/>
<field name="bom_id" invisible="context.get('bom_id_invisible', False)" domain="[]"/>
<field name="bom_id" invisible="context.get('bom_id_invisible', False)" domain="[]" readonly="context.get('default_bom_id', False)"/>
<field name="workcenter_id" context="{'default_company_id': company_id}"/>
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
<field name="bom_product_template_attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" groups="product.group_product_variant"/>
<field name="allow_operation_dependencies" invisible="1"/>
<field name="blocked_by_operation_ids" widget="many2many_tags" context="{'default_bom_id':bom_id}" attrs="{'invisible': [('allow_operation_dependencies', '=', False)]}"/>
</group><group name="workorder">
<field name="workorder_count" invisible="1"/>
<field name="time_mode" widget="radio"/>
+20 -2
View File
@@ -157,9 +157,10 @@
<group attrs="{'invisible': [('date_planned_start', '=', False)]}">
<label for="date_planned_start" string="Planned Date"/>
<div class="oe_inline">
<field name="date_planned_start" class="mr8 oe_inline" required="True"/>
<field name="is_planned" invisible="1"/>
<field name="date_planned_start" class="mr8 oe_inline" attrs="{'required': [('is_planned', '=', True)]}"/>
<strong class="mr8 oe_inline">to</strong>
<field name="date_planned_finished" class="oe_inline" required="True"/>
<field name="date_planned_finished" class="oe_inline" attrs="{'required': [('is_planned', '=', True)]}"/>
<field name="show_json_popover" invisible="1"/>
<field name="json_popover" widget="mrp_workorder_popover" class="oe_inline mx-2" attrs="{'invisible': [('show_json_popover', '=', False)]}"/>
</div>
@@ -225,6 +226,23 @@
<field name="worksheet_google_slide" widget="embed_viewer" attrs="{'invisible': [('worksheet_type', '!=', 'google_slide')]}"/>
<field name="operation_note" attrs="{'invisible': [('worksheet_type', '!=', 'text')]}"/>
</page>
<field name="allow_workorder_dependencies" invisible="1"/>
<page string="Blocked By" name="dependencies" attrs="{'invisible': [('allow_workorder_dependencies', '=', False)]}">
<field name="blocked_by_workorder_ids" nolabel="1">
<tree editable="bottom">
<field name="name" string="Operation"/>
<field name="company_id" optional="hide" groups="base.group_multi_company"/>
<field name="workcenter_id"/>
<field name="date_planned_start" readonly="1"/>
<field name="date_planned_finished" readonly="1"/>
<field name="duration_expected" widget="float_time" sum="expected duration"/>
<field name="production_state" invisible="1"/>
<field name="state" widget="badge" decoration-warning="state == 'progress'" decoration-success="state == 'done'" decoration-info="state not in ('progress', 'done', 'cancel')"
attrs="{'invisible': [('production_state', '=', 'draft')], 'column_invisible': [('parent.state', '=', 'draft')]}"/>
<button class="oe_link float-right" string="View WorkOrder" name="action_open_wizard" type="object"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
@@ -29,6 +29,15 @@
</div>
</div>
</div>
<div class="row mt-2" attrs="{'invisible': [('group_mrp_routings','=',False)]}">
<field name="group_mrp_workorder_dependencies" class="col-lg-1 ml16 mr0"/>
<div class="col pl-0">
<label for="group_mrp_workorder_dependencies"/>
<div class="text-muted">
Set the order that work orders should be processed in. Activate the feature within each BoM's Miscellaneous tab
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6 col-12 o_setting_box">
@@ -99,8 +99,6 @@ class ChangeProductionQty(models.TransientModel):
wo.state = 'progress'
if wo.qty_produced == wo.qty_production and wo.state == 'progress':
wo.state = 'done'
if wo.next_work_order_id.state == 'pending':
wo.next_work_order_id.state = 'ready'
# assign moves; last operation receive all unassigned moves
# TODO: following could be put in a function as it is similar as code in _workorders_create
# TODO: only needed when creating new moves