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:
gsinghpal
2026-04-17 07:43:10 -04:00
parent 3b5b5cbf7c
commit e07002d550
5 changed files with 1079 additions and 353 deletions

View File

@@ -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("")