feat(shopfloor): rich Tablet Station dashboard + full shop-floor demo data
Tablet Station rebuilt as a live dashboard (not just a QR scanner):
* KPI strip — WOs Ready/Progress, Awaiting/Missed bakes,
First-Piece pending, Quality Holds (each tinted by state)
* Active WO banner with pulsing indicator when a WO is running
* My Queue panel (left) — priority-badged operator next-up list,
clickable rows that jump to the WO/bake/gate form
* Baths tile grid (right) — last-log status chips, MTO count,
hover jump to chemistry log
* Bake Windows list — inline Start/End/Open actions, colour-coded
by state (awaiting / in-progress / missed)
* First-Piece Gates — Pass/Fail buttons for pending inspections
* Quality Holds — Review jump when any open holds exist
* Station picker + scan drawer (collapsed by default)
* 30s auto-refresh, persists picked station in localStorage
New controller endpoints: /fp/shopfloor/tablet_overview,
/fp/shopfloor/pair_station, /fp/shopfloor/mark_gate.
Demo seeder (Phase 12.5) now populates:
* 5 shop-floor stations (Plating, Bake, Inspection, Shipping, Receiving)
* +3 bake windows (awaiting / in-progress / near-due)
* 4 first-piece gates (1 pending, 1 passed+released, 1 passed-holding, 1 failed)
* 2 quality holds on active MOs (one on_hold, one under_review)
All four Shop Floor menu pages (Plant Overview, Tablet Station, Bake
Windows, First-Piece Gates) now have meaningful content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -993,6 +993,143 @@ else:
|
||||
LOG(f" Already has {bw_count} bake windows — skipping")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Phase 12.5: Shop-floor stations + first-piece gates + variety
|
||||
# Feeds the Tablet Station + First-Piece Gates + bake Kanban demos.
|
||||
# ============================================================
|
||||
LOG("Phase 12.5: Shop-floor stations + first-piece gates")
|
||||
|
||||
Station = env['fusion.plating.shopfloor.station']
|
||||
stn_count = Station.search_count([])
|
||||
if stn_count < 5:
|
||||
station_defs = [
|
||||
('Plating Room Tablet 1', 'TAB-PL-01', 'tablet', 'Plating'),
|
||||
('Bake Oven Tablet', 'TAB-BK-01', 'tablet', 'Bake Oven'),
|
||||
('Inspection Kiosk', 'TAB-QA-01', 'kiosk', 'Inspection'),
|
||||
('Shipping Desktop', 'TAB-SH-01', 'desktop', 'Shipping'),
|
||||
('Receiving Mobile', 'TAB-RC-01', 'mobile', 'Receiving'),
|
||||
]
|
||||
for sname, scode, stype, wc_name in station_defs:
|
||||
if Station.search_count([('code', '=', scode)]):
|
||||
continue
|
||||
fp_wc = env['fusion.plating.work.center'].search(
|
||||
[('name', '=', wc_name)], limit=1,
|
||||
) if wc_name else False
|
||||
Station.create({
|
||||
'name': sname,
|
||||
'code': scode,
|
||||
'station_type': stype,
|
||||
'facility_id': facility.id,
|
||||
'work_center_id': fp_wc.id if fp_wc else False,
|
||||
'last_ping': datetime.now() - timedelta(minutes=random.randint(0, 45)),
|
||||
})
|
||||
LOG(f" 5 shop-floor stations created")
|
||||
else:
|
||||
LOG(f" Already has {stn_count} stations — skipping")
|
||||
|
||||
# More bake windows — add a couple active ones for realism
|
||||
if env['fusion.plating.bake.window'].search_count([]) < 6:
|
||||
env['fusion.plating.bake.window'].create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'HW-TOR-5501', 'lot_ref': 'LOT-BW-HW-01',
|
||||
'customer_ref': 'Honeywell Toronto',
|
||||
'quantity': 120, 'window_hours': 4.0,
|
||||
'plate_exit_time': datetime.now() - timedelta(hours=2, minutes=15),
|
||||
})
|
||||
env['fusion.plating.bake.window'].create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'AP-WGL-7200', 'lot_ref': 'LOT-BW-AP-01',
|
||||
'customer_ref': 'Amphenol Canada',
|
||||
'quantity': 300, 'window_hours': 4.0,
|
||||
'plate_exit_time': datetime.now() - timedelta(hours=3, minutes=30),
|
||||
'bake_start_time': datetime.now() - timedelta(minutes=40),
|
||||
'state': 'bake_in_progress',
|
||||
})
|
||||
env['fusion.plating.bake.window'].create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'CY-STR-240', 'lot_ref': 'LOT-BW-CY-02',
|
||||
'customer_ref': 'Cyclone Manufacturing',
|
||||
'quantity': 60, 'window_hours': 4.0,
|
||||
'plate_exit_time': datetime.now() - timedelta(minutes=45),
|
||||
})
|
||||
LOG(" +3 additional bake windows (awaiting / in-progress)")
|
||||
|
||||
# First-piece inspection gates — seed 4 variants
|
||||
Gate = env['fusion.plating.first.piece.gate']
|
||||
if Gate.search_count([]) < 4:
|
||||
Gate.create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'HW-TOR-5501',
|
||||
'customer_ref': 'Honeywell Toronto',
|
||||
'routing_first_run': True,
|
||||
'first_piece_produced': datetime.now() - timedelta(minutes=35),
|
||||
'result': 'pending',
|
||||
})
|
||||
Gate.create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'AP-WGL-7200',
|
||||
'customer_ref': 'Amphenol Canada',
|
||||
'routing_first_run': False,
|
||||
'first_piece_produced': datetime.now() - timedelta(hours=2),
|
||||
'first_piece_inspected': datetime.now() - timedelta(hours=1, minutes=40),
|
||||
'inspector_id': env.user.id,
|
||||
'result': 'pass',
|
||||
'rest_of_lot_released': True,
|
||||
})
|
||||
Gate.create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'MG-WG-8801',
|
||||
'customer_ref': 'Magellan Aerospace',
|
||||
'routing_first_run': True,
|
||||
'first_piece_produced': datetime.now() - timedelta(hours=4),
|
||||
'first_piece_inspected': datetime.now() - timedelta(hours=3, minutes=30),
|
||||
'inspector_id': env.user.id,
|
||||
'result': 'pass',
|
||||
'rest_of_lot_released': False, # passed but awaiting release
|
||||
'notes': '<p>Thickness 1.95 mils — within tolerance. Lot released pending planner signoff.</p>',
|
||||
})
|
||||
Gate.create({
|
||||
'bath_id': bath_en.id,
|
||||
'part_ref': 'CY-STR-240',
|
||||
'customer_ref': 'Cyclone Manufacturing',
|
||||
'routing_first_run': True,
|
||||
'first_piece_produced': datetime.now() - timedelta(hours=6),
|
||||
'first_piece_inspected': datetime.now() - timedelta(hours=5, minutes=30),
|
||||
'inspector_id': env.user.id,
|
||||
'result': 'fail',
|
||||
'notes': '<p>Thickness 0.8 mils — below spec (min 1.2). Rework required.</p>',
|
||||
})
|
||||
LOG(" 4 first-piece gates: 1 pending / 1 passed+released / 1 passed-holding / 1 failed")
|
||||
else:
|
||||
LOG(f" Already has {Gate.search_count([])} first-piece gates — skipping")
|
||||
|
||||
# Quality holds on active MOs — gives the Shop Floor quality-holds panel content
|
||||
Hold = env['fusion.plating.quality.hold']
|
||||
if Hold.search_count([]) < 2:
|
||||
active_mos = env['mrp.production'].search(
|
||||
[('state', 'in', ('progress', 'confirmed'))], limit=3,
|
||||
)
|
||||
hold_reasons = ['out_of_spec', 'damaged', 'contamination']
|
||||
for i, mo in enumerate(active_mos[:2]):
|
||||
wo = mo.workorder_ids[:1]
|
||||
Hold.create({
|
||||
'part_ref': mo.product_id.default_code or mo.product_id.name,
|
||||
'qty_on_hold': random.randint(3, 8),
|
||||
'qty_original': int(mo.product_qty or 10),
|
||||
'hold_reason': hold_reasons[i % len(hold_reasons)],
|
||||
'description': f'Demo hold — flagged during in-process inspection on {mo.name}.',
|
||||
'production_id': mo.id,
|
||||
'workorder_id': wo.id if wo else False,
|
||||
'portal_job_id': mo.x_fc_portal_job_id.id if mo.x_fc_portal_job_id else False,
|
||||
'facility_id': facility.id,
|
||||
'operator_id': env.user.id,
|
||||
'state': 'on_hold' if i == 0 else 'under_review',
|
||||
})
|
||||
LOG(f" {Hold.search_count([])} quality holds seeded")
|
||||
else:
|
||||
LOG(f" Already has {Hold.search_count([])} quality holds — skipping")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Phase 13: Quote configurator + win/loss variety
|
||||
# ============================================================
|
||||
@@ -1236,6 +1373,9 @@ LOG(f" Payments: {env['account.payment'].search_count([('payment_type', '
|
||||
LOG(f" Bath logs: {env['fusion.plating.bath.log'].search_count([])}")
|
||||
LOG(f" Replenishments: {env['fusion.plating.bath.replenishment.suggestion'].search_count([])}")
|
||||
LOG(f" Bake windows: {env['fusion.plating.bake.window'].search_count([])}")
|
||||
LOG(f" Stations: {env['fusion.plating.shopfloor.station'].search_count([])}")
|
||||
LOG(f" First-piece: {env['fusion.plating.first.piece.gate'].search_count([])}")
|
||||
LOG(f" Quality holds: {env['fusion.plating.quality.hold'].search_count([])}")
|
||||
LOG(f" Racks: {env['fusion.plating.rack'].search_count([])}")
|
||||
LOG(f" Operator certs: {env['fp.operator.certification'].search_count([])}")
|
||||
LOG("")
|
||||
|
||||
Reference in New Issue
Block a user