changes
This commit is contained in:
@@ -1021,87 +1021,286 @@ class FpShopfloorController(http.Controller):
|
||||
def process_tree(self, production_id):
|
||||
"""Return routing tree for a manufacturing order.
|
||||
|
||||
Each node is an operation/work-order step. Children represent
|
||||
sub-states (ready vs active) within that step.
|
||||
"""
|
||||
MrpWO = request.env.get('mrp.workorder')
|
||||
if MrpWO is None:
|
||||
return {
|
||||
'production_name': '',
|
||||
'product_name': '',
|
||||
'state': '',
|
||||
'nodes': [],
|
||||
}
|
||||
Walks the MO's recipe tree (fusion.plating.process.node) and returns
|
||||
a recursive nested structure:
|
||||
recipe → sub_process → operation → step
|
||||
For each `operation` node we look up the matching mrp.workorder by
|
||||
name within this MO, then attach the WO state, qty progress, kind,
|
||||
equipment, and a synthetic state-child ("Ready for X" or "In X")
|
||||
so the operator sees the live position in the flow.
|
||||
|
||||
MrpProduction = request.env['mrp.production']
|
||||
If the MO has no recipe assigned we fall back to a flat list of
|
||||
WOs as a single tier of operation nodes under a synthetic root.
|
||||
"""
|
||||
env = request.env
|
||||
MrpWO = env.get('mrp.workorder')
|
||||
MrpProduction = env['mrp.production']
|
||||
production = MrpProduction.browse(int(production_id))
|
||||
if not production.exists():
|
||||
raise UserError(f"Manufacturing order {production_id} not found")
|
||||
|
||||
work_orders = MrpWO.search(
|
||||
[('production_id', '=', production.id)],
|
||||
order='sequence, id',
|
||||
# Customer
|
||||
customer = ''
|
||||
so_name = production.origin or ''
|
||||
if production.x_fc_portal_job_id and production.x_fc_portal_job_id.partner_id:
|
||||
customer = production.x_fc_portal_job_id.partner_id.name or ''
|
||||
elif so_name:
|
||||
so = env['sale.order'].search([('name', '=', so_name)], limit=1)
|
||||
if so:
|
||||
customer = so.partner_id.name or ''
|
||||
|
||||
product_qty = int(production.product_qty or 0)
|
||||
recipe = production.x_fc_recipe_id
|
||||
|
||||
# Build a lookup so each operation node finds its matching WO by name.
|
||||
# The bridge's _generate_workorders_from_recipe() copies node.name →
|
||||
# wo.name, so this is a stable join key within one MO.
|
||||
wos_by_name = {}
|
||||
all_wos = MrpWO.browse([]) if MrpWO is not None else []
|
||||
if MrpWO is not None:
|
||||
all_wos = MrpWO.search(
|
||||
[('production_id', '=', production.id)],
|
||||
order='sequence, id',
|
||||
)
|
||||
for wo in all_wos:
|
||||
key = (wo.name or '').strip()
|
||||
if key and key not in wos_by_name:
|
||||
wos_by_name[key] = wo
|
||||
|
||||
wo_kind_selection = (
|
||||
dict(MrpWO._fields['x_fc_wo_kind'].selection)
|
||||
if MrpWO is not None and 'x_fc_wo_kind' in MrpWO._fields else {}
|
||||
)
|
||||
masking_selection = (
|
||||
dict(MrpWO._fields['x_fc_masking_material'].selection)
|
||||
if MrpWO is not None and 'x_fc_masking_material' in MrpWO._fields else {}
|
||||
)
|
||||
|
||||
nodes = []
|
||||
for wo in work_orders:
|
||||
def _f(wo, name):
|
||||
return wo[name] if wo and name in wo._fields else False
|
||||
|
||||
def _dur_disp(mins):
|
||||
if mins >= 60:
|
||||
return f'{mins / 60:.1f}h'
|
||||
if mins > 0:
|
||||
return f'{int(mins)}m'
|
||||
return ''
|
||||
|
||||
def _wo_payload(wo):
|
||||
"""Manager-Desk style fields for one WO."""
|
||||
qty_done = int(wo.qty_produced or 0)
|
||||
qty_total = int(wo.qty_production or production.product_qty or 0)
|
||||
|
||||
# Duration display
|
||||
duration_mins = wo.duration or 0
|
||||
if duration_mins >= 60:
|
||||
duration_display = f'{duration_mins / 60:.1f}h'
|
||||
elif duration_mins > 0:
|
||||
duration_display = f'{int(duration_mins)}m'
|
||||
else:
|
||||
duration_display = ''
|
||||
|
||||
# Build children — sub-state nodes
|
||||
children = []
|
||||
if wo.state in ('ready', 'waiting'):
|
||||
children.append({
|
||||
'id': f'{wo.id}_ready',
|
||||
'name': f'Ready for {wo.workcenter_id.name or wo.name}',
|
||||
'state': 'ready',
|
||||
'qty_done': 0,
|
||||
'qty_total': qty_total,
|
||||
})
|
||||
elif wo.state == 'progress':
|
||||
children.append({
|
||||
'id': f'{wo.id}_active',
|
||||
'name': f'{wo.workcenter_id.name or wo.name}-ing',
|
||||
'state': 'progress',
|
||||
'qty_done': qty_done,
|
||||
'qty_total': qty_total,
|
||||
})
|
||||
# Also show "remaining" child if partial
|
||||
remaining = qty_total - qty_done
|
||||
if remaining > 0:
|
||||
children.append({
|
||||
'id': f'{wo.id}_remaining',
|
||||
'name': f'Ready for {wo.workcenter_id.name or wo.name}',
|
||||
'state': 'ready',
|
||||
'qty_done': 0,
|
||||
'qty_total': remaining,
|
||||
})
|
||||
|
||||
nodes.append({
|
||||
'id': wo.id,
|
||||
qty_total = int(wo.qty_production or product_qty or 0)
|
||||
wo_kind = _f(wo, 'x_fc_wo_kind') or 'other'
|
||||
assigned = _f(wo, 'x_fc_assigned_user_id')
|
||||
bath = _f(wo, 'x_fc_bath_id')
|
||||
tank = _f(wo, 'x_fc_tank_id')
|
||||
oven = _f(wo, 'x_fc_oven_id')
|
||||
rack = _f(wo, 'x_fc_rack_id')
|
||||
masking = _f(wo, 'x_fc_masking_material')
|
||||
return {
|
||||
'workorder_id': wo.id,
|
||||
'sequence': wo.sequence or 0,
|
||||
'name': wo.display_name or wo.name,
|
||||
'work_center_name': wo.workcenter_id.name if wo.workcenter_id else '',
|
||||
'state': wo.state or '',
|
||||
'wo_state': wo.state or '',
|
||||
'qty_done': qty_done,
|
||||
'qty_total': qty_total,
|
||||
'duration_display': duration_display,
|
||||
'children': children,
|
||||
})
|
||||
'wo_kind': wo_kind,
|
||||
'wo_kind_label': wo_kind_selection.get(wo_kind, ''),
|
||||
'assigned_user_name': assigned.name if assigned else '',
|
||||
'bath': bath.name if bath else '',
|
||||
'tank': tank.name if tank else '',
|
||||
'oven': oven.name if oven else '',
|
||||
'rack': rack.name if rack else '',
|
||||
'masking_material': (
|
||||
masking_selection.get(masking, '') if masking else ''
|
||||
),
|
||||
'duration_display': _dur_disp(wo.duration or 0),
|
||||
'duration_expected_display': _dur_disp(wo.duration_expected or 0),
|
||||
'missing_for_release': _f(wo, 'x_fc_missing_for_release') or '',
|
||||
}
|
||||
|
||||
def _step_state_for(step_node, wo):
|
||||
"""Map a recipe step's state from the parent operation's WO.
|
||||
|
||||
The step nodes are templates ("Ready For Blast", "Blast",
|
||||
"Bake", etc.). We push the operation's WO state down so the
|
||||
step that represents the live position renders highlighted.
|
||||
|
||||
Convention: a step whose name contains "ready" represents the
|
||||
queued/waiting phase; the other step represents the action
|
||||
phase.
|
||||
"""
|
||||
if not wo:
|
||||
return ''
|
||||
step_name = (step_node.name or '').lower()
|
||||
is_ready_step = 'ready' in step_name
|
||||
wo_state = wo.state or ''
|
||||
if wo_state == 'done':
|
||||
return 'done'
|
||||
if wo_state in ('ready', 'waiting'):
|
||||
return 'ready' if is_ready_step else 'pending'
|
||||
if wo_state == 'progress':
|
||||
return 'progress' if not is_ready_step else 'done'
|
||||
return ''
|
||||
|
||||
def _step_qty_for(step_node, wo):
|
||||
"""Live qty for a step — fed from the parent WO."""
|
||||
if not wo or not wo.qty_production:
|
||||
return (0, 0)
|
||||
qty_done = int(wo.qty_produced or 0)
|
||||
qty_total = int(wo.qty_production or 0)
|
||||
step_name = (step_node.name or '').lower()
|
||||
wo_state = wo.state or ''
|
||||
if wo_state == 'done':
|
||||
return (qty_total, qty_total)
|
||||
if wo_state in ('ready', 'waiting'):
|
||||
# Everything is queued
|
||||
return (qty_total, qty_total) if 'ready' in step_name else (0, 0)
|
||||
if wo_state == 'progress':
|
||||
if 'ready' in step_name:
|
||||
remaining = qty_total - qty_done
|
||||
return (remaining, remaining) if remaining > 0 else (0, 0)
|
||||
return (qty_done, qty_total)
|
||||
return (0, 0)
|
||||
|
||||
# Track which WOs were attached to a recipe node — leftovers get
|
||||
# pushed under the recipe root as orphan operations.
|
||||
attached_wo_ids = set()
|
||||
|
||||
def _walk(node, parent_wo=None):
|
||||
wo = wos_by_name.get((node.name or '').strip())
|
||||
wo_data = {}
|
||||
if node.node_type == 'operation' and wo:
|
||||
attached_wo_ids.add(wo.id)
|
||||
wo_data = _wo_payload(wo)
|
||||
# If this node is a `step` whose parent operation has a WO,
|
||||
# mirror the WO's state onto the step so the live phase
|
||||
# ("Ready for X" or "X") renders highlighted.
|
||||
step_state = ''
|
||||
step_qty_done, step_qty_total = 0, 0
|
||||
if node.node_type == 'step' and parent_wo:
|
||||
step_state = _step_state_for(node, parent_wo)
|
||||
step_qty_done, step_qty_total = _step_qty_for(node, parent_wo)
|
||||
|
||||
# Recurse — pass this operation's WO down so step children inherit
|
||||
inherited_wo = wo if (node.node_type == 'operation' and wo) else parent_wo
|
||||
children_payload = []
|
||||
for child in node.child_ids.sorted('sequence'):
|
||||
children_payload.append(_walk(child, inherited_wo))
|
||||
|
||||
return {
|
||||
'id': f'n_{node.id}',
|
||||
'name': node.name or '',
|
||||
'node_type': node.node_type,
|
||||
'icon': node.icon or '',
|
||||
'sequence': node.sequence or 0,
|
||||
'workorder_id': wo_data.get('workorder_id'),
|
||||
'wo_state': wo_data.get('wo_state', ''),
|
||||
'state': wo_data.get('wo_state') or step_state or '',
|
||||
'qty_done': wo_data.get('qty_done') or step_qty_done or 0,
|
||||
'qty_total': wo_data.get('qty_total') or step_qty_total or 0,
|
||||
'wo_kind': wo_data.get('wo_kind', ''),
|
||||
'wo_kind_label': wo_data.get('wo_kind_label', ''),
|
||||
'assigned_user_name': wo_data.get('assigned_user_name', ''),
|
||||
'bath': wo_data.get('bath', ''),
|
||||
'tank': wo_data.get('tank', ''),
|
||||
'oven': wo_data.get('oven', ''),
|
||||
'rack': wo_data.get('rack', ''),
|
||||
'masking_material': wo_data.get('masking_material', ''),
|
||||
'duration_display': wo_data.get('duration_display', ''),
|
||||
'duration_expected_display': wo_data.get(
|
||||
'duration_expected_display', ''),
|
||||
'missing_for_release': wo_data.get('missing_for_release', ''),
|
||||
'children': children_payload,
|
||||
}
|
||||
|
||||
if recipe:
|
||||
root = _walk(recipe)
|
||||
# Append orphan WOs (those not matched to any recipe node by name)
|
||||
# so we don't lose them — these usually appear when the user
|
||||
# adds ad-hoc WOs after generation.
|
||||
for wo in all_wos:
|
||||
if wo.id in attached_wo_ids:
|
||||
continue
|
||||
wo_data = _wo_payload(wo)
|
||||
orphan = {
|
||||
'id': f'wo_{wo.id}',
|
||||
'name': wo.name or '',
|
||||
'node_type': 'operation',
|
||||
'icon': '',
|
||||
'sequence': wo.sequence or 0,
|
||||
'workorder_id': wo.id,
|
||||
'wo_state': wo.state or '',
|
||||
'state': wo.state or '',
|
||||
'qty_done': wo_data['qty_done'],
|
||||
'qty_total': wo_data['qty_total'],
|
||||
'wo_kind': wo_data['wo_kind'],
|
||||
'wo_kind_label': wo_data['wo_kind_label'],
|
||||
'assigned_user_name': wo_data['assigned_user_name'],
|
||||
'bath': wo_data['bath'],
|
||||
'tank': wo_data['tank'],
|
||||
'oven': wo_data['oven'],
|
||||
'rack': wo_data['rack'],
|
||||
'masking_material': wo_data['masking_material'],
|
||||
'duration_display': wo_data['duration_display'],
|
||||
'duration_expected_display': wo_data['duration_expected_display'],
|
||||
'missing_for_release': wo_data['missing_for_release'],
|
||||
'children': [],
|
||||
}
|
||||
root['children'].append(orphan)
|
||||
else:
|
||||
# No recipe — synth a root with WOs as direct operation children.
|
||||
child_nodes = []
|
||||
for wo in all_wos:
|
||||
wo_data = _wo_payload(wo)
|
||||
child_nodes.append({
|
||||
'id': f'wo_{wo.id}',
|
||||
'name': wo.name or '',
|
||||
'node_type': 'operation',
|
||||
'icon': '',
|
||||
'sequence': wo.sequence or 0,
|
||||
'workorder_id': wo.id,
|
||||
'wo_state': wo.state or '',
|
||||
'state': wo.state or '',
|
||||
'qty_done': wo_data['qty_done'],
|
||||
'qty_total': wo_data['qty_total'],
|
||||
'wo_kind': wo_data['wo_kind'],
|
||||
'wo_kind_label': wo_data['wo_kind_label'],
|
||||
'assigned_user_name': wo_data['assigned_user_name'],
|
||||
'bath': wo_data['bath'],
|
||||
'tank': wo_data['tank'],
|
||||
'oven': wo_data['oven'],
|
||||
'rack': wo_data['rack'],
|
||||
'masking_material': wo_data['masking_material'],
|
||||
'duration_display': wo_data['duration_display'],
|
||||
'duration_expected_display': wo_data['duration_expected_display'],
|
||||
'missing_for_release': wo_data['missing_for_release'],
|
||||
'children': [],
|
||||
})
|
||||
root = {
|
||||
'id': 'root',
|
||||
'name': production.product_id.display_name if production.product_id
|
||||
else (production.name or 'Process'),
|
||||
'node_type': 'recipe',
|
||||
'icon': 'fa-sitemap',
|
||||
'sequence': 0,
|
||||
'children': child_nodes,
|
||||
'workorder_id': None,
|
||||
'state': production.state or '',
|
||||
'wo_state': '',
|
||||
'qty_done': 0, 'qty_total': 0,
|
||||
'wo_kind': '', 'wo_kind_label': '',
|
||||
'assigned_user_name': '', 'bath': '', 'tank': '', 'oven': '',
|
||||
'rack': '', 'masking_material': '',
|
||||
'duration_display': '', 'duration_expected_display': '',
|
||||
'missing_for_release': '',
|
||||
}
|
||||
|
||||
return {
|
||||
'production_name': production.name or '',
|
||||
'product_name': production.product_id.display_name if production.product_id else '',
|
||||
'state': production.state or '',
|
||||
'nodes': nodes,
|
||||
'customer': customer,
|
||||
'so_name': so_name,
|
||||
'product_qty': product_qty,
|
||||
'recipe': recipe.name if recipe else '',
|
||||
'root': root,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user