diff options
| author | Arnold Moyaux <arm@odoo.com> | 2020-05-08 10:02:44 +0200 |
|---|---|---|
| committer | Simon Lejeune <sle@openerp.com> | 2020-06-15 16:59:41 +0200 |
| commit | 182f1b8e26a8c41d8fef2bedca781356d1362dfc (patch) | |
| tree | de8cfedb52ba7f86d0cc432e7a44e6de6d62f54c /addons/mrp/models/mrp_workorder.py | |
| parent | 5011ec520c48cab4117e6e11cc91c1d551bbf648 (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.py | 229 |
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. |
