summaryrefslogtreecommitdiff
path: root/addons/mrp/models/mrp_workorder.py
diff options
context:
space:
mode:
authorArnold Moyaux <arm@odoo.com>2020-05-08 10:02:44 +0200
committerSimon Lejeune <sle@openerp.com>2020-06-15 16:59:41 +0200
commit182f1b8e26a8c41d8fef2bedca781356d1362dfc (patch)
treede8cfedb52ba7f86d0cc432e7a44e6de6d62f54c /addons/mrp/models/mrp_workorder.py
parent5011ec520c48cab4117e6e11cc91c1d551bbf648 (diff)
[REF] mrp: workorder onchange
Create the workorders at the onchange when setting the bom and not a "plan" anymore. task-2241471
Diffstat (limited to 'addons/mrp/models/mrp_workorder.py')
-rw-r--r--addons/mrp/models/mrp_workorder.py229
1 files changed, 176 insertions, 53 deletions
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 444976ebaed..edb6345f662 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -31,7 +31,7 @@ class MrpWorkorder(models.Model):
required=True, index=True, readonly=True)
workcenter_id = fields.Many2one(
'mrp.workcenter', 'Work Center', required=True,
- states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
+ states={'done': [('readonly', True)], 'cancel': [('readonly', True)], 'progress': [('readonly', True)]},
group_expand='_read_group_workcenter_id', check_company=True)
working_state = fields.Selection(
string='Workcenter Status', related='workcenter_id.working_state', readonly=False,
@@ -59,34 +59,32 @@ class MrpWorkorder(models.Model):
('progress', 'In Progress'),
('done', 'Finished'),
('cancel', 'Cancelled')], string='Status',
- default='pending')
+ default='pending', copy=False, readonly=True)
leave_id = fields.Many2one(
'resource.calendar.leaves',
help='Slot into workcenter calendar once planned',
- check_company=True)
+ check_company=True, copy=False)
date_planned_start = fields.Datetime(
- 'Scheduled Date Start',
+ 'Scheduled Start Date',
compute='_compute_dates_planned',
inverse='_set_dates_planned',
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
- store=True,
- tracking=True)
+ store=True, tracking=True, copy=False)
date_planned_finished = fields.Datetime(
- 'Scheduled Date Finished',
+ 'Scheduled End Date',
compute='_compute_dates_planned',
inverse='_set_dates_planned',
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
- store=True,
- tracking=True)
+ store=True, tracking=True, copy=False)
date_start = fields.Datetime(
- 'Effective Start Date',
+ 'Start Date', copy=False,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
date_finished = fields.Datetime(
- 'Effective End Date',
+ 'End Date', copy=False,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
duration_expected = fields.Float(
- 'Expected Duration', digits=(16, 2),
+ 'Expected Duration', digits=(16, 2), default=60.0,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help="Expected duration (in minutes)")
duration = fields.Float(
@@ -101,8 +99,7 @@ class MrpWorkorder(models.Model):
progress = fields.Float('Progress Done (%)', digits=(16, 2), compute='_compute_progress')
operation_id = fields.Many2one(
- 'mrp.routing.workcenter', 'Operation',
- check_company=True)
+ 'mrp.routing.workcenter', 'Operation', check_company=True)
# Should be used differently as BoM can change in the meantime
worksheet = fields.Binary(
'Worksheet', related='operation_id.worksheet', readonly=True)
@@ -124,7 +121,7 @@ class MrpWorkorder(models.Model):
'stock.production.lot', 'Lot/Serial Number', domain="[('id', 'in', allowed_lots_domain)]",
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, check_company=True)
time_ids = fields.One2many(
- 'mrp.workcenter.productivity', 'workorder_id')
+ 'mrp.workcenter.productivity', 'workorder_id', copy=False)
is_user_working = fields.Boolean(
'Is the Current User Working', compute='_compute_working_users',
help="Technical field indicating whether the current user is working. ")
@@ -144,6 +141,7 @@ class MrpWorkorder(models.Model):
json_popover = fields.Char('Popover Data JSON', compute='_compute_json_popover')
show_json_popover = fields.Boolean('Show Popover?', compute='_compute_json_popover')
+ @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)],
@@ -154,9 +152,14 @@ class MrpWorkorder(models.Model):
'date_planned_start': x['date_planned_start'],
'date_planned_finished': x['date_planned_finished']})
for x in previous_wo_data])
- conflicted_dict = self._get_conflicted_workorder_ids()
+ if self.ids:
+ conflicted_dict = self._get_conflicted_workorder_ids()
for wo in self:
infos = []
+ if not wo.date_planned_start or not wo.date_planned_finished or not wo.ids:
+ wo.show_json_popover = False
+ wo.json_popover = False
+ continue
if wo.state in ['pending', 'ready']:
previous_wo = previous_wo_dict.get(wo.id)
prev_start = previous_wo and previous_wo['date_planned_start'] or False
@@ -222,24 +225,6 @@ class MrpWorkorder(models.Model):
else:
workorder.is_finished_lines_editable = False
- @api.onchange('finished_lot_id')
- def _onchange_finished_lot_id(self):
- """When the user changes the lot being currently produced, suggest
- a quantity to produce consistent with the previous workorders. """
- previous_wo = self.env['mrp.workorder'].search([
- ('next_work_order_id', '=', self.id)
- ])
- if previous_wo:
- line = previous_wo.finished_workorder_line_ids.filtered(lambda line: line.product_id == self.product_id and line.lot_id == self.finished_lot_id)
- if line:
- self.qty_producing = line.qty_done
-
- @api.onchange('date_planned_finished')
- def _onchange_date_planned_finished(self):
- if self.date_planned_start and self.date_planned_finished:
- diff = self.date_planned_finished - self.date_planned_start
- self.duration_expected = diff.total_seconds() / 60
-
@api.depends('production_id.workorder_ids.finished_workorder_line_ids',
'production_id.workorder_ids.finished_workorder_line_ids.qty_done',
'production_id.workorder_ids.finished_workorder_line_ids.lot_id')
@@ -251,6 +236,8 @@ class MrpWorkorder(models.Model):
productions = self.mapped('production_id')
treated = self.browse()
for production in productions:
+ if production.state == 'draft':
+ continue
if production.product_id.tracking == 'none':
continue
@@ -297,12 +284,17 @@ class MrpWorkorder(models.Model):
# Removes references to workorder to avoid Validation Error
(self.mapped('move_raw_ids') | self.mapped('move_finished_ids')).write({'workorder_id': False})
self.mapped('leave_id').unlink()
- return super(MrpWorkorder, self).unlink()
+ mo_dirty = self.production_id.filtered(lambda mo: mo.state in ("confirmed", "progress", "to_close"))
+ 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).
+ mo_dirty.workorder_ids._action_confirm()
+ return res
- @api.depends('production_id.product_qty', 'qty_produced')
+ @api.depends('production_id.product_qty', 'qty_produced', 'production_id.product_uom_id')
def _compute_is_produced(self):
self.is_produced = False
- for order in self.filtered(lambda p: p.production_id):
+ for order in self.filtered(lambda p: p.production_id and p.production_id.product_uom_id):
rounding = order.production_id.product_uom_id.rounding
order.is_produced = float_compare(order.qty_produced, order.production_id.product_qty, precision_rounding=rounding) >= 0
@@ -347,11 +339,39 @@ class MrpWorkorder(models.Model):
for workorder in self:
workorder.scrap_count = count_data.get(workorder.id, 0)
+ @api.onchange('finished_lot_id')
+ def _onchange_finished_lot_id(self):
+ """When the user changes the lot being currently produced, suggest
+ a quantity to produce consistent with the previous workorders. """
+ previous_wo = self.env['mrp.workorder'].search([
+ ('next_work_order_id', '=', self.id)
+ ])
+ if previous_wo:
+ line = previous_wo.finished_workorder_line_ids.filtered(lambda line: line.product_id == self.product_id and line.lot_id == self.finished_lot_id)
+ if line:
+ self.qty_producing = line.qty_done
+
+ @api.onchange('date_planned_finished')
+ def _onchange_date_planned_finished(self):
+ if self.date_planned_start and self.date_planned_finished:
+ diff = self.date_planned_finished - self.date_planned_start
+ self.duration_expected = diff.total_seconds() / 60
+
+ @api.onchange('operation_id')
+ def _onchange_operation_id(self):
+ if self.operation_id:
+ self.name = self.operation_id.name
+ self.workcenter_id = self.operation_id.workcenter_id.id
+
@api.onchange('date_planned_start', 'duration_expected')
def _onchange_date_planned_start(self):
if self.date_planned_start and self.duration_expected:
self.date_planned_finished = self.date_planned_start + relativedelta(minutes=self.duration_expected)
+ @api.onchange('operation_id', 'workcenter_id', 'qty_production')
+ def _onchange_expected_duration(self):
+ self.duration_expected = self._get_duration_expected()
+
def write(self, values):
if 'production_id' in values:
raise UserError(_('You cannot link this work order to another manufacturing order.'))
@@ -361,7 +381,7 @@ class MrpWorkorder(models.Model):
if workorder.state in ('progress', 'done', 'cancel'):
raise UserError(_('You cannot change the workcenter of a work order that is in progress or done.'))
workorder.leave_id.resource_id = self.env['mrp.workcenter'].browse(values['workcenter_id']).resource_id
- if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self):
+ if any(k not in ['time_ids', 'duration_expected', 'next_work_order_id'] for k in values.keys()) and any(workorder.state == 'done' for workorder in self):
raise UserError(_('You can not change the finished work order.'))
if 'date_planned_start' in values or 'date_planned_finished' in values:
for workorder in self:
@@ -381,6 +401,67 @@ class MrpWorkorder(models.Model):
})
return super(MrpWorkorder, self).write(values)
+ @api.model_create_multi
+ def create(self, values):
+ res = super().create(values)
+ # Auto-confirm manually added workorders.
+ # We need to go through `_action_confirm` for all workorders of the current productions to
+ # make sure the links between them are correct.
+ to_confirm = res.filtered(lambda wo: wo.production_id.state in ("confirmed", "progress", "to_close"))
+ to_confirm = to_confirm.production_id.workorder_ids
+ to_confirm._action_confirm()
+ 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 self:
+ if workorder.operation_id.bom_id:
+ bom = workorder.operation_id.bom_id
+ if not bom:
+ bom = 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 workorder in workorders:
+ workorder._apply_update_workorder_lines()
+
+ for workorders in workorders_by_bom.values():
+ if workorders[0].state == 'pending':
+ workorders[0].state = 'ready'
+ for workorder in workorders:
+ workorder._start_nextworkorder()
+
def _generate_wo_lines(self):
""" Generate workorder line """
self.ensure_one()
@@ -570,6 +651,8 @@ class MrpWorkorder(models.Model):
(self.operation_id.batch == 'yes' and
float_compare(self.operation_id.batch_size, self.qty_produced, precision_rounding=rounding) <= 0)):
self.next_work_order_id.state = 'ready'
+ if self.state == 'done' and self.next_work_order_id.state == 'pending':
+ self.next_work_order_id.state = 'ready'
@api.model
def gantt_unavailability(self, start_date, end_date, scale, group_bys=None, rows=None):
@@ -617,7 +700,7 @@ class MrpWorkorder(models.Model):
# Need a loss in case of the real time exceeding the expected
timeline = self.env['mrp.workcenter.productivity']
- if self.duration < self.duration_expected:
+ if not self.duration_expected or self.duration < self.duration_expected:
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1)
if not len(loss_id):
raise UserError(_("You need to define at least one productivity loss in the category 'Productivity'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
@@ -632,7 +715,7 @@ class MrpWorkorder(models.Model):
timeline.create({
'workorder_id': self.id,
'workcenter_id': self.workcenter_id.id,
- 'description': _('Time Tracking: ')+self.env.user.name,
+ 'description': _('Time Tracking: ') + self.env.user.name,
'loss_id': loss_id[0].id,
'date_start': datetime.now(),
'user_id': self.env.user.id, # FIXME sle: can be inconsistent with company_id
@@ -640,26 +723,45 @@ class MrpWorkorder(models.Model):
})
if self.state == 'progress':
return True
+ start_date = datetime.now()
+ vals = {
+ 'state': 'progress',
+ 'date_start': start_date,
+ }
+ if not self.leave_id:
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': self.production_id.name + ' - ' + self.name,
+ 'calendar_id': self.workcenter_id.resource_calendar_id.id,
+ 'date_from': start_date,
+ 'date_to': start_date + relativedelta(minutes=self.duration_expected),
+ 'resource_id': self.workcenter_id.resource_id.id,
+ 'time_type': 'other'
+ })
+ vals['leave_id'] = leave.id
+ return self.write(vals)
else:
- start_date = datetime.now()
- vals = {
- 'state': 'progress',
- 'date_start': start_date,
- 'date_planned_start': start_date,
- }
+ vals['date_planned_start'] = start_date
if self.date_planned_finished < start_date:
vals['date_planned_finished'] = start_date
return self.write(vals)
def button_finish(self):
- self.ensure_one()
- self.end_all()
end_date = datetime.now()
- return self.write({
- 'state': 'done',
- 'date_finished': end_date,
- 'date_planned_finished': end_date
- })
+ for workorder in self:
+ if workorder.state in ('done', 'cancel'):
+ continue
+ workorder.end_all()
+ vals = {
+ 'state': 'done',
+ 'date_finished': end_date,
+ 'date_planned_finished': end_date
+ }
+ if not workorder.date_start:
+ vals['date_start'] = end_date
+ if not workorder.date_planned_start or end_date < workorder.date_planned_start:
+ vals['date_planned_start'] = end_date
+ workorder.write(vals)
+ return True
def end_previous(self, doall=False):
"""
@@ -751,11 +853,32 @@ class MrpWorkorder(models.Model):
action['domain'] = [('workorder_id', '=', self.id)]
return action
+ def action_open_wizard(self):
+ self.ensure_one()
+ action = self.env.ref('mrp.mrp_workorder_mrp_production_form').read()[0]
+ action['res_id'] = self.id
+ return action
+
@api.depends('qty_production', 'qty_produced')
def _compute_qty_remaining(self):
for wo in self:
wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding)
+ def _get_duration_expected(self, alternative_workcenter=False):
+ self.ensure_one()
+ if not self.workcenter_id:
+ return False
+ qty_production = self.production_id.product_uom_id._compute_quantity(self.qty_production, self.production_id.product_id.uom_id)
+ cycle_number = float_round(qty_production / self.workcenter_id.capacity, precision_digits=0, rounding_method='UP')
+ if alternative_workcenter:
+ # TODO : find a better alternative : the settings of workcenter can change
+ duration_expected_working = (self.duration_expected - self.workcenter_id.time_start - self.workcenter_id.time_stop) * self.workcenter_id.time_efficiency / (100.0 * cycle_number)
+ if duration_expected_working < 0:
+ duration_expected_working = 0
+ return alternative_workcenter.time_start + alternative_workcenter.time_stop + cycle_number * duration_expected_working * 100.0 / alternative_workcenter.time_efficiency
+ time_cycle = self.operation_id and self.operation_id.time_cycle or 60.0
+ return self.workcenter_id.time_start + self.workcenter_id.time_stop + cycle_number * time_cycle * 100.0 / self.workcenter_id.time_efficiency
+
def _get_conflicted_workorder_ids(self):
"""Get conlicted workorder(s) with self.