This commit is contained in:
gsinghpal
2026-04-20 01:16:12 -04:00
parent 8217bb0ff6
commit 54e56ed0e6
39 changed files with 5600 additions and 1131 deletions

View File

@@ -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,
}