[REF] mrp: workorder onchange

Create the workorders at the onchange when setting the bom and not a
"plan" anymore.

task-2241471
This commit is contained in:
Arnold Moyaux
2020-06-15 16:59:41 +02:00
committed by Simon Lejeune
parent 5011ec520c
commit 182f1b8e26
15 changed files with 412 additions and 288 deletions
+4
View File
@@ -672,6 +672,10 @@
<field name="date_start" eval="(datetime.now() - timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<function model="mrp.production" name="_create_workorder">
<value eval="[ref('mrp.mrp_production_3')]"/>
</function>
<function model="mrp.production" name="action_confirm">
<value eval="[ref('mrp.mrp_production_3')]"/>
</function>
@@ -54,8 +54,6 @@ class MrpAbstractWorkorder(models.AbstractModel):
workorder line in order to match the new quantity to consume for each
component and their reserved quantity.
"""
if self.qty_producing <= 0:
raise UserError(_('You have to produce at least one %s.') % self.product_uom_id.name)
line_values = self._update_workorder_lines()
for values in line_values['to_create']:
self.env[self._workorder_line_ids()._name].new(values)
+75 -100
View File
@@ -146,7 +146,6 @@ class MrpProduction(models.Model):
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('planned', 'Planned'),
('progress', 'In Progress'),
('to_close', 'To Close'),
('done', 'Done'),
@@ -155,7 +154,6 @@ class MrpProduction(models.Model):
store=True, tracking=True,
help=" * Draft: The MO is not confirmed yet.\n"
" * Confirmed: The MO is confirmed, the stock rules and the reordering of the components are trigerred.\n"
" * Planned: The WO are planned.\n"
" * In Progress: The production has started (on the MO or on the WO).\n"
" * To Close: The production is done, the MO has to be closed.\n"
" * Done: The MO is closed, the stock moves are posted. \n"
@@ -185,8 +183,7 @@ class MrpProduction(models.Model):
'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product"
)
workorder_ids = fields.One2many(
'mrp.workorder', 'production_id', 'Work Orders',
copy=False, readonly=True)
'mrp.workorder', 'production_id', 'Work Orders', copy=True)
workorder_count = fields.Integer('# Work Orders', compute='_compute_workorder_count')
workorder_done_count = fields.Integer('# Done Work Orders', compute='_compute_workorder_done_count')
move_dest_ids = fields.One2many('stock.move', 'created_production_id',
@@ -227,6 +224,7 @@ class MrpProduction(models.Model):
priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
readonly=True, states={'draft': [('readonly', False)]}, default='1')
is_locked = fields.Boolean('Is Locked', default=True, copy=False)
is_planned = fields.Boolean('Its Operations are Planned', compute="_compute_is_planned")
show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots')
production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production', readonly=False, related_sudo=False)
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this manufacturing order')
@@ -296,6 +294,13 @@ class MrpProduction(models.Model):
else:
production.bom_has_operations = len(production.bom_id.operation_ids) > 0
def _compute_is_planned(self):
for production in self:
if production.workorder_ids:
production.is_planned = all(wo.date_planned_start and wo.date_planned_finished for wo in production.workorder_ids)
else:
production.is_planned = False
@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')
@@ -403,14 +408,12 @@ class MrpProduction(models.Model):
production.workorder_done_count = count_data.get(production.id, 0)
@api.depends(
'move_raw_ids.state', 'move_finished_ids.state', 'workorder_ids',
'workorder_ids.date_planned_start', 'workorder_ids.date_planned_finished',
'workorder_ids.state', 'move_raw_ids.quantity_done', 'product_qty', 'qty_producing')
'move_raw_ids.state', 'move_raw_ids.quantity_done', 'move_finished_ids.state',
'workorder_ids', 'workorder_ids.state', 'product_qty', 'qty_producing')
def _compute_state(self):
""" Compute the production state. It use the same process than stock
picking. It exists 3 extra steps for production:
- planned: Workorder has been launched (workorders only)
- progress: At least one item is produced.
- progress: At least one item is produced or consumed.
- to_close: The quantity produced is greater than the quantity to
produce and all work orders has been finished.
"""
@@ -422,22 +425,16 @@ class MrpProduction(models.Model):
production.state = 'draft'
elif all(move.state == 'cancel' for move in production.move_raw_ids):
production.state = 'cancel'
elif all(move.state in ['cancel', 'done'] for move in production.move_raw_ids):
if (
production.bom_id.consumption == 'flexible'
and float_compare(production.qty_producing, production.product_qty, precision_rounding=production.product_uom_id.rounding) == -1
):
production.state = 'progress'
else:
production.state = 'done'
elif all(move.state in ('cancel', 'done') for move in production.move_raw_ids):
production.state = 'done'
elif production.qty_producing >= production.product_qty:
production.state = 'to_close'
elif production.workorder_ids and any(wo_state == 'progress' for wo_state in production.workorder_ids.mapped('state')):
elif any(wo_state in ('progress', 'done') for wo_state in production.workorder_ids.mapped('state')):
production.state = 'progress'
elif not float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding) and production.qty_producing < production.product_qty:
elif not float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding):
production.state = 'progress'
elif any(not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding) for move in production.move_raw_ids) :
production.state = 'progress'
elif production.workorder_ids:
production.state = 'planned'
else:
production.state = 'confirmed'
@@ -522,6 +519,17 @@ class MrpProduction(models.Model):
self.bom_id = False
self.product_uom_id = self.product_id.uom_id.id
@api.onchange('product_qty', 'product_uom_id')
def _onchange_product_qty(self):
product_qty = self.product_uom_id._compute_quantity(self.product_qty, self.product_uom_id)
for workorder in self.workorder_ids:
if not workorder.product_uom_id:
workorder.product_uom_id = self.product_uom_id
workorder.qty_producing = self.product_id.tracking == 'serial' and 1.0 or product_qty
workorder.duration_expected = workorder._get_duration_expected()
if workorder.date_planned_start and workorder.duration_expected:
workorder.date_planned_finished = workorder.date_planned_start + relativedelta(minutes=workorder.duration_expected)
@api.onchange('bom_id')
def _onchange_bom_id(self):
if not self.product_id and self.bom_id:
@@ -533,7 +541,7 @@ class MrpProduction(models.Model):
@api.onchange('date_planned_start')
def _onchange_date_planned_start(self):
if not self.bom_id.operation_ids:
if not self.workorder_ids:
self.date_planned_finished = self.date_planned_start + datetime.timedelta(hours=1)
@api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id')
@@ -595,6 +603,11 @@ class MrpProduction(models.Model):
for move_line, res in vals['to_write']:
move_line.update(res)
@api.onchange('bom_id')
def _onchange_workorder_ids(self):
if self.bom_id:
self._create_workorder()
def write(self, vals):
res = super(MrpProduction, self).write(vals)
@@ -602,7 +615,7 @@ class MrpProduction(models.Model):
if 'date_planned_start' in vals:
if production.state in ['done', 'cancel']:
raise UserError(_('You cannot move a manufacturing order once it is cancelled or done.'))
if production.workorder_ids and not self.env.context.get('force_date', False):
if production.state not in ('draft', 'confirmed') and production.workorder_ids and not self.env.context.get('force_date', False):
raise UserError(_('You cannot move a planned manufacturing order.'))
if ('move_raw_ids' in vals or 'move_finished_ids' in vals) and production.state != 'draft':
production._autoconfirm_production()
@@ -652,6 +665,34 @@ class MrpProduction(models.Model):
self.is_locked = not self.is_locked
return True
def _create_workorder(self):
for production in self:
if not production.bom_id:
continue
workorders_values = []
product_qty = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id)
exploded_boms, dummy = production.bom_id.explode(production.product_id, product_qty / production.bom_id.product_qty, picking_type=production.bom_id.picking_type_id)
for bom, bom_data in exploded_boms:
# If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders.
if not (bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids)):
continue
for operation in bom.operation_ids:
workorders_values += [{
'name': operation.name,
'production_id': production.id,
'workcenter_id': operation.workcenter_id.id,
'product_uom_id': production.product_id.uom_id.id,
'operation_id': operation.id,
'state': 'pending',
'qty_producing': production.product_id.tracking == 'serial' and 1.0 or product_qty,
'consumption': production.consumption,
}]
production.workorder_ids = [(5, 0)] + [(0, 0, value) for value in workorders_values]
for workorder in production.workorder_ids:
workorder.duration_expected = workorder._get_duration_expected()
def _get_finished_move_value(self, product_id, product_uom_qty, product_uom, operation_id=False, byproduct_id=False):
group_orders = self.procurement_group_id.mrp_production_ids
move_dest_ids = self.move_dest_ids
@@ -823,6 +864,8 @@ class MrpProduction(models.Model):
if moves_to_confirm:
moves_to_confirm._action_confirm()
self.workorder_ids.filtered(lambda w: w.state not in ['done', 'cancel'])._action_confirm()
def action_view_mrp_production_childs(self):
self.ensure_one()
mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids
@@ -911,17 +954,17 @@ class MrpProduction(models.Model):
production._generate_finished_moves()
production.move_raw_ids._adjust_procure_method()
(production.move_raw_ids | production.move_finished_ids)._action_confirm()
production.workorder_ids._action_confirm()
return True
def action_assign(self):
for production in self:
production.move_raw_ids._action_assign()
production.workorder_ids._refresh_wo_lines()
return True
def button_plan(self):
""" Create work orders. And probably do stuff, like things. """
orders_to_plan = self.filtered(lambda order: order.bom_id.operation_ids and order.state == 'confirmed')
orders_to_plan = self.filtered(lambda order: not order.is_planned)
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
@@ -929,9 +972,6 @@ class MrpProduction(models.Model):
# have their leaves booked in the workcenter calendar. We thus disable the
# automatic rescheduling in this scenario.
order.move_raw_ids.write({'propagate_date': False})
quantity = order.product_uom_id._compute_quantity(order.product_qty, order.bom_id.product_uom_id) / order.bom_id.product_qty
boms, lines = order.bom_id.explode(order.product_id, quantity, picking_type=order.bom_id.picking_type_id)
order._generate_workorders(boms)
order._plan_workorders()
return True
@@ -963,9 +1003,10 @@ class MrpProduction(models.Model):
vals = {}
for workcenter in workcenters:
# compute theoretical duration
time_cycle = workorder.operation_id.time_cycle
cycle_number = float_round(qty_to_produce / workcenter.capacity, precision_digits=0, rounding_method='UP')
duration_expected = workcenter.time_start + workcenter.time_stop + cycle_number * time_cycle * 100.0 / workcenter.time_efficiency
if workorder.workcenter_id == workcenter:
duration_expected = workorder.duration_expected
else:
duration_expected = workorder._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
@@ -1015,73 +1056,7 @@ class MrpProduction(models.Model):
raise UserError(_("Some work orders are already done, you cannot unplan this manufacturing order."))
elif any(wo.state == 'progress' for wo in self.workorder_ids):
raise UserError(_("Some work orders have already started, you cannot unplan this manufacturing order."))
self.workorder_ids.unlink()
def _generate_workorders(self, exploded_boms):
workorders = self.env['mrp.workorder']
original_one = False
for bom, bom_data in exploded_boms:
# 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"
if original_one:
temp_workorders[-1].next_work_order_id = original_one
original_one = temp_workorders[0]
return workorders
def _workorders_create(self, bom, bom_data):
"""
:param bom: in case of recursive boms: we could create work orders for child
BoMs
"""
workorders = self.env['mrp.workorder']
# Initial qty producing
quantity = max(self.product_qty - sum(self.move_finished_ids.filtered(lambda move: move.product_id == self.product_id).mapped('quantity_done')), 0)
quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom_id)
if self.product_id.tracking == 'serial':
quantity = 1.0
for operation in bom.operation_ids:
workorder = workorders.create({
'name': operation.name,
'production_id': self.id,
'workcenter_id': operation.workcenter_id.id,
'product_uom_id': self.product_id.uom_id.id,
'operation_id': operation.id,
'state': len(workorders) == 0 and 'ready' or 'pending',
'qty_producing': quantity,
'consumption': self.consumption,
})
if workorders:
workorders[-1].next_work_order_id = workorder.id
workorders[-1]._start_nextworkorder()
workorders += workorder
# 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:
moves_raw |= move
moves_finished = self.move_finished_ids.filtered(lambda move: move.operation_id == operation)
# - Raw moves from a BoM where a routing was set but no operation was precised should
# 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.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)
moves_raw.mapped('move_line_ids').write({'workorder_id': workorder.id})
(moves_finished | moves_raw).write({'workorder_id': workorder.id})
workorder._generate_wo_lines()
return workorders
self.workorder_ids.leave_id.unlink()
def _get_consumption_issues(self):
"""Compare the quantity consumed of the components, the expected quantity
@@ -1286,7 +1261,7 @@ class MrpProduction(models.Model):
production.name = self._get_name_backorder(production.name, production.backorder_sequence)
for wo in backorder_mo.workorder_ids:
wo.duration_expected = wo._get_duration_expected(wo.workcenter_id)
wo.duration_expected = wo._get_duration_expected()
backorders.action_confirm()
# Remove the serial move line without reserved quantity. Post inventory will assigned all the non done moves
@@ -1340,7 +1315,7 @@ class MrpProduction(models.Model):
production.write({'date_finished': fields.Datetime.now(), 'product_qty': production.qty_produced})
for workorder in self.workorder_ids.filtered(lambda w: w.state not in ('done', 'cancel')):
workorder.duration_expected = workorder._get_duration_expected(workorder.workcenter_id)
workorder.duration_expected = workorder._get_duration_expected()
if not backorders:
return True
+3 -2
View File
@@ -17,11 +17,12 @@ class MrpRoutingWorkcenter(models.Model):
help="Gives the sequence order when displaying a list of routing Work Centers.")
bom_id = fields.Many2one(
'mrp.bom', 'Bill of Material',
index=True, ondelete='cascade', required=True,
index=True, ondelete='cascade',
help="The Bill of Material this operation is linked to")
company_id = fields.Many2one(
'res.company', 'Company',
readonly=True, related='bom_id.company_id', store=True)
readonly=True, related='bom_id.company_id', store=True,
default=lambda self: self.env.company)
worksheet_type = fields.Selection([
('pdf', 'PDF'), ('google_slide', 'Google Slide'), ('text', 'Text')],
string="Work Sheet", default="pdf",
+176 -53
View File
@@ -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.
+1
View File
@@ -55,6 +55,7 @@ class StockRule(models.Model):
# create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(productions_values)
self.env['stock.move'].sudo().create(productions._get_moves_raw_values())
productions._create_workorder()
productions.filtered(lambda p: p.move_raw_ids).action_confirm()
for production in productions:
@@ -24,6 +24,9 @@ var MrpWorkorderPopover = PopoverWidget.extend({
_render: function () {
this._super.apply(this, arguments);
if (! this.$popover) {
return;
}
var self = this;
this.$popover.find('.action_replan_button').click(function (e) {
self._onReplanClick(e);
+2 -7
View File
@@ -347,19 +347,14 @@ class TestMrpWorkorderBackorder(SavepointCase):
self.assertEqual(workorder1.raw_workorder_line_ids.product_id, self.compfinished1)
self.assertEqual(workorder1.raw_workorder_line_ids.qty_to_consume, 2)
#mrp_production_workorder_form_view_inherit
workorder1.button_start()
with Form(workorder1) as w:
w.qty_producing = 1
workorder1.record_production()
workorder1.button_finish()
self.assertEqual(workorder2.raw_workorder_line_ids.product_id, self.compfinished2)
self.assertEqual(workorder2.raw_workorder_line_ids.qty_to_consume, 2)
workorder2.button_start()
with Form(workorder2) as w:
w.qty_producing = 1
workorder2.record_production()
workorder2.button_finish()
action = mo.with_context(debug=True).button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+24 -8
View File
@@ -82,13 +82,32 @@ class TestMrpCancelMO(TestMrpCommon):
done and the WO must be cancelled.
"""
# Create MO
mo_data = self.generate_mo(consumption='strict')
manufacturing_order = mo_data[0]
bom = mo_data[1]
bom.write({
product_to_build = self.env['product.product'].create({
'name': 'Young Tom',
'type': 'product',
})
product_to_use_1 = self.env['product.product'].create({
'name': 'Botox',
'type': 'product',
})
product_to_use_2 = self.env['product.product'].create({
'name': 'Old Tom',
'type': 'product',
})
bom = self.env['mrp.bom'].create({
'product_id': product_to_build.id,
'product_tmpl_id': product_to_build.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'consumption': 'strict',
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_to_use_2.id, 'product_qty': 4}),
(0, 0, {'product_id': product_to_use_1.id, 'product_qty': 1})
],
'operation_ids': [
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
],
]
})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_to_build
@@ -96,9 +115,6 @@ class TestMrpCancelMO(TestMrpCommon):
mo_form.product_qty = 5.0
manufacturing_order = mo_form.save()
manufacturing_order.action_confirm()
mo_form = Form(manufacturing_order)
mo_form.qty_producing = 2
manufacturing_order = mo_form.save()
manufacturing_order.button_plan()
workorder = manufacturing_order.workorder_ids
+48 -1
View File
@@ -995,6 +995,53 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
self.assertEqual(production_table.workorder_ids[0].qty_producing, 2, "Quantity to produce not updated")
def test_expected_duration_workorder(self):
""" Test that the expected_duration is editable and change when other
workcenter is set or product quantity to produce is increased
Also this expected duration need to be recalculate correctly during the planning
if it used a alternative workcenter
"""
planned_date = datetime(2023, 5, 15, 9, 0)
self.mrp_workcenter_3.write({'capacity': 1, 'time_start': 0, 'time_stop': 2, 'time_efficiency': 50})
self.workcenter_1.write({'capacity': 2, 'time_start': 2, 'time_stop': 1, 'time_efficiency': 80, 'alternative_workcenter_ids': [(6, 0, [self.mrp_workcenter_3.id])]})
self.planning_bom.operation_ids[0].write({'time_cycle_manual': 10})
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.planning_bom
mo_form.date_planned_start = planned_date
mo = mo_form.save()
# TEST Default/onchange duration_expected
self.assertEqual(len(mo.workorder_ids), 1)
self.assertEqual(mo.workorder_ids.workcenter_id, self.workcenter_1, "wrong workcenter")
# time_cycle = 10, product_qty = 4.0, capacity = 2, time_start = 2, time_stop = 1, time_efficiency = 80%
# => 2 + 1 + (4 / 2 * 10) * (100 / 80) = 28.0
self.assertEqual(mo.workorder_ids.duration_expected, 28.0, "wrong duration_expected")
mo_form = Form(mo)
with mo_form.workorder_ids.edit(0) as wo_form:
wo_form.workcenter_id = self.mrp_workcenter_3
mo = mo_form.save()
# time_cycle = 10, product_qty = 4.0, capacity = 1, time_start = 0, time_stop = 2, time_efficiency = 50%
# => 0 + 2 + (4 / 1 * 10) * (100 / 50) = 82.0
self.assertEqual(mo.workorder_ids.duration_expected, 82.0, "wrong duration_expected")
# TEST Manual change of duration_expected
with mo_form.workorder_ids.edit(0) as wo_form:
wo_form.workcenter_id = self.workcenter_1
self.assertEqual(wo_form.duration_expected, 28.0, "wrong duration_expected")
wo_form.duration_expected = 40.0
mo = mo_form.save()
self.assertEqual(mo.workorder_ids.duration_expected, 40.0, "wrong duration_expected")
mo = mo_form.save()
mo.action_confirm()
mo.button_plan()
self.assertEqual(mo.workorder_ids.duration_expected, 40.0, "duration_expected doesn't reset")
self.assertAlmostEqual(mo.date_planned_start, mo.workorder_ids.date_planned_start, delta=timedelta(seconds=10))
self.assertAlmostEqual(mo.date_planned_finished, mo.workorder_ids.date_planned_finished, delta=timedelta(seconds=10))
self.assertAlmostEqual(mo.date_planned_start, planned_date, delta=timedelta(seconds=10))
self.assertAlmostEqual(mo.date_planned_finished, planned_date + timedelta(minutes=40), delta=timedelta(seconds=10))
def test_planning_0(self):
""" Test alternative conditions
1. alternative relation is directionnal
@@ -1253,7 +1300,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
self.assertEqual(bool(leave.exists()), True)
# Unplans the MO and checks the workorder and its leave no more exist.
mo.button_unplan()
self.assertEqual(bool(mo.workorder_ids.exists()), False)
self.assertEqual(bool(mo.workorder_ids.exists()), True)
self.assertEqual(bool(leave.exists()), False)
# Plans (again) the MO and checks the date is still the same.
mo.button_plan()
+9 -9
View File
@@ -96,6 +96,15 @@
</tree>
</field>
</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>
<page string="By-products"
name="by_products"
attrs="{'invisible': [('type','!=','normal')]}"
@@ -122,15 +131,6 @@
</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">
+41 -8
View File
@@ -63,8 +63,8 @@
<field name="confirm_cancel" invisible="1"/>
<button name="button_mark_done" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('qty_producing', '=', 0)]}" string="Backorder" type="object" class="oe_highlight"/>
<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')]}" string="Confirm" type="object" class="oe_highlight"/>
<button name="action_assign" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object" class="oe_highlight"/>
<button name="action_confirm" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('is_locked', '=', False)]}" string="Confirm" 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', 'not in', ('confirmed', 'progress')), ('workorder_ids', '=', []), ('is_planned', '=', True)]}" 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="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('cancel', 'draft')), ('is_locked', '=', False)]}"/>
@@ -86,14 +86,9 @@
<field name="unreserve_visible" invisible="1"/>
<field name="reserve_visible" invisible="1"/>
<field name="consumption" invisible="1"/>
<field name="is_planned" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button name="%(stock.action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="action" states="done" groups="stock.group_production_lot"/>
<button name="%(action_mrp_workorder_production_specific)d" type="action" attrs="{'invisible': [('workorder_count', '=', 0)]}" class="oe_stat_button" icon="fa-play-circle-o">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="workorder_done_count" widget="statinfo" nolabel="1"/> / <field name="workorder_count" widget="statinfo" nolabel="1"/></span>
<span class="o_stat_text">Work Orders</span>
</div>
</button>
<button name="%(action_mrp_production_moves)d" type="action" string="Product Moves" class="oe_stat_button" icon="fa-exchange" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/>
<button type="object" name="action_view_mo_delivery" class="oe_stat_button" icon="fa-truck" groups="base.group_user" attrs="{'invisible': [('delivery_count', '=', 0)]}">
<field name="delivery_count" widget="statinfo" string="Transfers"/>
@@ -321,6 +316,44 @@
</tree>
</field>
</page>
<page string="Operations" name="operations" groups="mrp.group_mrp_routings">
<field name="workorder_ids" attrs="{'readonly': [('state', 'in', ['cancel', 'done'])]}" context="{'default_production_id': id, 'default_qty_producing': product_qty, 'default_product_uom_id': product_uom_id, 'default_consumption': consumption, 'default_company_id': company_id}">
<tree editable="bottom">
<field name="consumption" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="is_produced" invisible="1"/>
<field name="is_user_working" invisible="1"/>
<field name="name" invisible="1"/>
<field name="product_uom_id" invisible="1" readonly="0"/>
<field name="qty_producing" invisible="1"/>
<field name="time_ids" invisible="1"/>
<field name="working_state" invisible="1"/>
<field name="operation_id" invisible="1" domain="['|', ('bom_id', '=', parent.bom_id), ('bom_id', '=', False)]" context="{'default_workcenter_id': workcenter_id, 'default_company_id': company_id}"/>
<field name="name" string="Operation"/>
<field name="workcenter_id"/>
<field name="date_planned_start" optional="show"/>
<field name="date_planned_finished" optional="hide"/>
<field name="date_start" optional="hide" readonly="1"/>
<field name="date_finished" optional="hide" readonly="1"/>
<field name="duration_expected" widget="float_time" sum="Total excepted duration"/>
<field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."
attrs="{'invisible': ['|', ('parent.state','=', 'draft'), ('time_ids', '=', [])]}"/>
<button name="action_open_wizard" type="object" icon="fa-external-link" width="0" class="oe_edit_only"
context="{'default_workcenter_id': workcenter_id}"/>
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state not in ('done', 'cancel')"/>
<button name="button_start" type="object" string="Start" width="0" class="btn-success"
attrs="{'invisible': ['|', '|', '|', ('parent.state','in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('state', '=', 'done'), ('is_user_working', '!=', False)]}"/>
<button name="button_pending" type="object" string="Pause" width="0" class="btn-warning"
attrs="{'invisible': ['|', '|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
<button name="button_finish" type="object" string="Done" width="0" class="btn-success"
attrs="{'invisible': ['|', '|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
<button name="%(mrp.act_mrp_block_workcenter_wo)d" type="action" string="Block" context="{'default_workcenter_id': workcenter_id}" width="0" class="btn-danger"
attrs="{'invisible': ['|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked')]}"/>
<button name="button_unblock" type="object" string="Unblock" context="{'default_workcenter_id': workcenter_id}" width="0" class="btn-danger"
attrs="{'invisible': ['|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '!=', 'blocked')]}"/>
</tree>
</field>
</page>
<page string="Miscellaneous" name="miscellaneous" groups="stock.group_stock_multi_locations">
<group>
<group>
+1 -1
View File
@@ -10,7 +10,7 @@
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="workcenter_id"/>
<field name="time_cycle" widget="float_time" string="Duration (minutes)" sum="Total Duration"/>
<field name="time_cycle" widget="float_time" string="Duration (minutes)" sum="Total Duration" width="1.5"/>
</tree>
</field>
</record>
@@ -431,14 +431,6 @@
parent="menu_mrp_configuration"
sequence="90"/>
<menuitem id="menu_mrp_dashboard"
name="Overview"
action="mrp_workcenter_kanban_action"
groups="group_mrp_routings"
parent="menu_mrp_root"
sequence="5"/>
<record id="oee_loss_form_view" model="ir.ui.view">
<field name="name">mrp.workcenter.productivity.loss.form</field>
<field name="model">mrp.workcenter.productivity.loss</field>
+25 -89
View File
@@ -92,15 +92,6 @@
<field name="allowed_lots_domain" invisible="1"/>
<field name="is_finished_lines_editable" invisible="1"/>
<header>
<button name="button_finish" type="object" string="Finish Order" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('is_produced', '=', False)]}" class="btn-info"/>
<button name="button_start" type="object" string="Start Working" attrs="{'invisible': ['|', ('working_state', '=', 'blocked'), ('state', '!=', 'pending')]}"/>
<button name="button_start" type="object" string="Start Working" attrs="{'invisible': ['|', ('working_state', '=', 'blocked'), ('state', '!=', 'ready')]}" class="btn-success"/>
<button name="record_production" type="object" string="Done" class="btn-success" attrs="{'invisible': ['|', '|', '|', ('is_produced', '=', True), ('working_state', '=', 'blocked'), ('state', '!=', 'progress'), ('is_user_working', '=', False)]}"/>
<button name="button_pending" type="object" string="Pause" class="btn-warning" attrs="{'invisible': ['|', '|', ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'pending', 'ready', 'cancel')), ('is_user_working', '=', False)]}"/>
<button name="%(act_mrp_block_workcenter_wo)d" type="action" context="{'default_workcenter_id': workcenter_id}" string="Block" class="btn-danger" attrs="{'invisible': ['|', '|', ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'pending', 'ready', 'cancel')), ('is_user_working', '=', False)]}"/>
<button name="button_unblock" type="object" string="Unblock" class="btn-danger" attrs="{'invisible': [('working_state', '!=', 'blocked')]}"/>
<button name="button_start" type="object" string="Continue Production" attrs="{'invisible': ['|', '|', '|', ('production_state', '=', 'done'), ('working_state', '=', 'blocked'), ('is_user_working', '=', True), ('state', '!=', 'progress')]}"/>
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}"/>
<field name="state" widget="statusbar" statusbar_visible="pending,ready,progress,done"/>
</header>
<sheet>
@@ -112,50 +103,24 @@
</div>
</button>
</div>
<group>
<group>
<field name="product_id" string="To Produce"/>
<label for="qty_produced" string="Quantity Produced"/>
<div class="o_row">
<field name="qty_produced"/> /
<field name="qty_production"/>
<field name="product_uom_id"/>
<field name="production_availability" nolabel="1" widget="bullet_state" options="{'classes': {'assigned': 'success', 'waiting': 'danger'}}" attrs="{'invisible': [('state', '=', 'done')]}"/>
</div>
<field name="is_produced" invisible="1"/>
</group>
<group>
<field name="workcenter_id"/>
<field name="production_id" readonly="1"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<field name="workcenter_id" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="product_tracking" invisible="1"/>
<field name="production_id" invisible="1"/>
<field name="finished_lot_id" invisible="1"/>
<field name="qty_producing" invisible="1"/>
<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"/>
<strong class="mr8 oe_inline">to</strong>
<field name="date_planned_finished" class="oe_inline" required="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>
</group>
<notebook>
<page string="Time Tracking" name="time_tracking" groups="mrp.group_mrp_manager">
<group>
<label for="date_planned_start" string="Planned Date"/>
<div class="oe_inline">
<field name="date_planned_start" class="mr8 oe_inline" required="True"/>
<strong class="mr8 oe_inline">to</strong>
<field name="date_planned_finished" class="oe_inline" required="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>
<label for="date_start" string="Effective Date" attrs="{'invisible': [('date_start', '=', False)]}"/>
<div class="o_row" attrs="{'invisible': [('date_start', '=', False)]}">
<field name="date_start" readonly="1"/>
<div attrs="{'invisible': [('date_finished', '=', False)]}">
<strong class="mr8">to</strong>
<field name="date_finished" readonly="1"/>
</div>
</div>
<label for="duration_expected"/>
<div>
<field name="duration_expected" widget="float_time" class="oe_inline"/>
minutes
</div>
<field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."/>
</group>
<group>
<field name="time_ids" nolabel="1" context="{'default_workcenter_id': workcenter_id, 'default_workorder_id': id}">
<tree>
@@ -165,7 +130,7 @@
<field name="user_id"/>
<field name="workcenter_id" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="loss_id" string="Productivity"/>
<field name="loss_id" string="Productivity" optional="hide"/>
</tree>
<form>
<group>
@@ -186,44 +151,6 @@
</field>
</group>
</page>
<page string="Current Production" name="current_production">
<group>
<group>
<field name="qty_producing" string="Quantity in Production" attrs="{'readonly': ['|', ('product_tracking', '=', 'serial'), ('state', 'in', ('done', 'cancel'))]}"/>
<field name="company_id" invisible="1"/>
<field name="finished_lot_id" context="{'default_product_id': product_id, 'default_company_id': company_id}" attrs="{'invisible': [('product_tracking', '=', 'none')]}" groups="stock.group_production_lot"/>
<field name="product_tracking" invisible="1"/>
<field name="consumption" invisible="1"/>
</group>
</group>
<h4 attrs="{'invisible': ['&amp;', ('consumption', '=', 'strict'), ('raw_workorder_line_ids', '=', [])]}">Components</h4>
<field name="raw_workorder_line_ids" attrs="{'invisible': ['&amp;', ('consumption', '=', 'strict'), ('raw_workorder_line_ids', '=', [])], 'readonly': [('state', 'in', ('cancel', 'done'))]}" options="{'create': [('consumption', '=', 'flexible')]}">
<tree editable="bottom" create="1" delete="0">
<field name="product_id"/>
<field name="product_tracking" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_company_id': company_id, 'default_product_id': product_id, 'active_mo_id': parent.production_id}"/>
<field name="qty_to_consume" attrs="{'readonly': [('parent.consumption', '=', 'strict')]}" force_save="1"/>
<field name="qty_reserved" readonly="1"/>
<field name="qty_done" attrs="{'column_invisible': [('parent.state', 'not in', ('progress', 'done'))]}"/>
<field name="product_uom_id" invisible="1"/>
<field name="move_id" invisible="1"/>
</tree>
</field>
<h4>Finished Products</h4>
<field name="finished_workorder_line_ids" attrs="{'readonly': [('is_finished_lines_editable', '=', False)]}">
<tree editable="bottom" create="1" delete="0">
<field name="product_id"/>
<field name="product_tracking" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_company_id': company_id, 'default_product_id': product_id}"/>
<field name="qty_to_consume" readonly="1" string="To Produce" force_save="1"/>
<field name="qty_done" string="Produced"/>
<field name="product_uom_id" invisible="1"/>
<field name="move_id" invisible="1"/>
</tree>
</field>
</page>
<page string="Work Instruction" name="workorder_page_work_instruction" attrs="{'invisible': [('worksheet', '=', False), ('worksheet_google_slide', '=', False), ('operation_note', '=', False)]}">
<field name="worksheet_type" invisible="1"/>
<field name="worksheet" widget="pdf_viewer" attrs="{'invisible': [('worksheet_type', '!=', 'pdf')]}"/>
@@ -485,6 +412,15 @@
</field>
</record>
<record model="ir.actions.act_window" id="mrp_workorder_mrp_production_form">
<field name="name">Work Orders</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">mrp.workorder</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="view_id" ref="mrp_production_workorder_form_view_inherit"/>
</record>
<record model="ir.actions.act_window" id="mrp_workorder_todo">
<field name="name">Work Orders</field>
<field name="type">ir.actions.act_window</field>