Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-23-shopfloor-plant-view-design.md
gsinghpal 48805b5988 docs(shopfloor): plant-view kanban redesign spec
Replaces per-step-grouped kanban with department-grouped (9 fixed columns).
One card per fp.job; recipe step count no longer drives layout width.

- 9 fixed columns in process sequence: Receiving / Masking / Blasting /
  Racking / Plating / Baking / De-Racking / Final inspection / Shipping
- new fp.work.centre.area_kind Selection + step_id.area_kind related
- 13 mutually-exclusive card states with explicit precedence list and
  matching _compute_card_state dispatcher
- Variant C card: WO header, customer/PN/qty/PO, recipe/spec, tag chips,
  current step + tank + state chip, 9-step mini-timeline, progress +
  operator pill + icon row
- /fp/landing/plant_kanban endpoint returns columns + denormalized cards
- MVP uses existing single-station pairing UX; M2M field structure is
  forward-compatible for cross-trained operators (Phase 2)
- Feature flag x_fc_shopfloor_layout for parallel rollout

Deferred to Phase 2: drag-drop, sibling grouping, bottleneck heatmap,
manager-specific KPIs, phone breakpoint, sort customization,
quick-action sheet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:22:17 -04:00

39 KiB
Raw Blame History

Shop Floor Plant View — Redesign

Date: 2026-05-23 Status: Design — approved through brainstorming, awaiting plan Replaces: the current Shop Floor kanban (per-step grouping, one card per step) Affects: fusion_plating_shopfloor (primary), fusion_plating (work centre taxonomy), fusion_plating_jobs (active-step + workflow-state computes)


1. Problem

The current Shop Floor kanban groups cards by individual fp.job.step.work_centre_id. Every ready/pending step of a job spawns a separate card in its respective column. A 14-step recipe (e.g. ENP-ALUM-BASIC on WO-30019) produces 9 cards across 9 columns for ONE job. With 17 active jobs on the floor, the board shows 100+ cards across 10+ narrow columns, most of which contain duplicates of the same WO.

Confirmed by the user via screenshots taken 2026-05-23:

"the same job is appearing in multiple places, there can be 20 steps in any job and we cannot just make 20 columns for those jobs"

Net effect:

  • Operators can't scan the board — duplicates drown the signal
  • Recipes with many steps (15+) make the board explode horizontally
  • "Where is WO-30019 right now?" is impossible to answer at a glance
  • The mode toggle (Station / All Plant) is cosmetic — both produce the same cluttered output

The redesign re-anchors the kanban on one card per job at the department level, and scales to any recipe step count.


2. Goals & non-goals

Goals

  1. Every active fp.job appears in EXACTLY ONE column at all times. No duplication.
  2. Fixed 9-column layout that doesn't grow with recipe step count.
  3. Columns always render in process sequence (Receiving → … → Shipping), regardless of card distribution. Empty columns still show.
  4. Operator paired to a station sees their work highlighted but can also see the whole plant — "Where is everything right now?" is the central operator question.
  5. Every floor state the audit + battle-test catalog exposes is visually distinguishable on the card (13 states total).
  6. Scales infinitely: a 5-step recipe and a 30-step recipe both produce single cards moving across the same 9 columns.
  7. Tablet-first — readable on a 1080p wall-mounted tablet without horizontal scroll.

Non-goals

  • Replacing the Job Workspace (the full-screen single-WO surface). The kanban is the entry point; the Workspace remains the place where work happens. Card tap opens the Workspace.
  • Replacing the Manager Dashboard (fp_manager_dashboard with workflow funnel + at-risk + heatmap). The kanban's "Manager" mode is a filter on the same board; the dedicated dashboard stays separate.
  • Drag-and-drop step advancement from the kanban. State transitions happen inside the Workspace or via Move dialogs. The kanban reflects state, doesn't drive it.
  • Per-tank columns. Tanks are surfaced as chips on the card, not as columns.

3. Decisions locked during brainstorming (2026-05-23)

# Decision
D1 Plant-wide view with mine highlighted is the operator default (over "filter to my station only"). Operators help each other and cover stations; visibility matters more than filtering.
D2 9 fixed columns by process area (Receiving, Masking, Blasting, Racking, Plating, Baking, De-Racking, Final inspection, Shipping).
D3 All wet steps roll up into Plating — Soak Clean, Electroclean, Acid Dip, Etch, Desmut, Zincate, Rinse, Water Break Test, E-Nickel Plating, Chrome, Anodize, Black Oxide, Drying. The tank chip on the card distinguishes them.
D4 De-Masking folds into De-Racking — same operator action in this shop's workflow; no separate column.
D5 Contract Review (paperwork) cards live in Receiving with a purple paperwork chip. Same for any pre-physical-work admin gate.
D6 Variant C card design — full-width vertical card with WO header, customer/PN/qty/PO line, recipe + spec, tag chips (Rush/FAIR/VIP), current step name, tank + state chip row, 9-column mini-timeline, progress bar + operator pill + icons.
D7 13 card states distinguishable by background tint, left-border color, state chip text/color, and timeline marker color. Full catalog in §6.
D8 Columns appear in sequence and never reorder — even empty columns show. The sequence is the visual mental model of the floor.

4. Column layout

4.1 Fixed column sequence

The board always renders these 9 columns in this exact order, left-to-right:

1. Receiving       2. Masking       3. Blasting       4. Racking       5. Plating
6. Baking          7. De-Racking    8. Final inspection                 9. Shipping

Columns are first-class entities, not derived from data. If no jobs are in Blasting, the column still appears with a "0" badge — it's a placeholder reminding the operator where Blasting sits in the flow.

4.2 Step-kind → column mapping

Each fp.job.step routes to exactly one column based on its recipe_node_id.default_kind. The mapping table:

Column Step kinds routed here
Receiving incoming_inspection, contract_review, gating, ready_for_processing, any step where state = 'pending' and the job's first physical step hasn't started
Masking masking
Blasting blasting, bead_blast, media_blast
Racking racking
Plating soak_clean, electroclean, acid_dip, etch, desmut, zincate, rinse, water_break_test, e_nickel_plate, chrome, anodize, black_oxide, drying, activation, any step whose work_centre.kind = 'wet_line'
Baking bake, oven_bake, post_bake_relief
De-Racking de_rack, de_mask, unrack
Final inspection post_plate_inspection, final_inspection, thickness_qc, fair, dimensional_check, any step whose work_centre.kind = 'inspect'
Shipping shipping, pack_ship

4.3 Implementation — area_kind field

Add a new Selection field on fp.work.centre:

area_kind = fields.Selection([
    ('receiving',  'Receiving'),
    ('masking',    'Masking'),
    ('blasting',   'Blasting'),
    ('racking',    'Racking'),
    ('plating',    'Plating'),
    ('baking',     'Baking'),
    ('de_racking', 'De-Racking'),
    ('inspection', 'Final inspection'),
    ('shipping',   'Shipping'),
], string='Floor Column', help='Which Shop Floor column this work centre belongs to. Drives the plant-view kanban.')

fp.job.step already carries a recipe_node_id and (optionally) a work_centre_id. The kanban grouping resolves a step's column via:

step.area_kind = step.work_centre_id.area_kind
                 or _DEFAULT_KIND_BY_RECIPE_KIND.get(step.recipe_node_id.default_kind)
                 or 'plating'  # safe catch-all for unmapped wet steps

A post_init_hook backfills area_kind on existing fp.work.centre records by matching their kind (wet_line/bake/mask/rack/inspect) against the new taxonomy. Unmapped centres get flagged for manual review.

4.4 Column visibility rules

  • Always show all 9 columns in order.
  • Show the column-header count even when zero (0 in grey, less prominent).
  • The operator's paired-station column gets a yellow tint + "📍 You're here" badge — see §7.

5. Card design — Variant C

5.1 Anatomy

┌─────────────────────────────────────────────┐
│ WO-30049 ⭐                Due May 16 · 3d  │  ← WO + due
│ ABC Manufacturing                            │  ← customer
│ PN 9876699373 Rev A · Qty 5 · PO 4501882    │  ← part/qty/PO
│ Recipe: ENP-ALUM-BASIC · AMS-2404 Type II   │  ← recipe + spec
│ [RUSH] [FAIR]                                │  ← tag chips
│ Racking                                      │  ← current step name
│ [Rack Station 1]  [● Ready]                  │  ← tank + state chips
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓              │  ← mini-timeline
│ Rec Mask Blast [Rack] Plat Bake D-R Insp Ship│  ← timeline labels
├─────────────────────────────────────────────┤
│ Step 4/14 ▓▓░░░░░░░░░          [GS] 🔏      │  ← progress + operator + icons
└─────────────────────────────────────────────┘

5.2 Field-by-field

Element Source Notes
WO # fp.job.display_wo_name Big, bold. suffix appears when card is at operator's paired station. Tappable — opens Job Workspace.
Due date fp.job.commitment_date Format "Due May 16 · 3d" (relative). Turns red + when overdue.
Customer fp.job.partner_id.name Single line; truncate with ellipsis if too long.
PN / Qty / PO fp.job.part_catalog_id.part_number + .revision · fp.job.qty · fp.job.sale_order_id.x_fc_po_number One line, comma-separated.
Recipe + spec fp.job.recipe_id.name · fp.job.customer_spec_id.code Muted small text.
Tag chips derived Multi: Rush (partner flag) / FAIR (customer_spec.x_fc_requires_first_article) / VIP (partner flag) / AS9100 (job aerospace flag). Only renders when applicable.
Current step name fp.job.active_step_id.name or first ready step Operator-facing label of the step the job is at.
Tank chip fp.job.active_step_id.work_centre_id.code or .tank_id.code Blue chip. Specific tank/station.
State chip computed (§6) One of 13 states. Color matches state.
Mini-timeline derived (§8) 9-step bar showing the journey across columns.
Step X / Y fp.job.active_step_id.sequence / count(fp.job.step_ids) Recipe progress, not the same as the 9-col timeline.
Progress bar computed Filled to active_step.sequence / total_steps. Color matches state.
Operator pill fp.job.active_step_id.assigned_user_id Initials avatar. Hidden when ready (no operator engaged yet).
Icon row derived Compact status flags (see §5.3).

5.3 Icon row catalog

Icon Meaning Trigger
🔏 Sign-off required step.requires_signoff AND not yet signed
Bake window approaching upstream wet step done, bake_required_by - now < 1h
🔥 Bake compliance gate active step kind = bake
💬 Recent chatter activity job.message_post in last 24h
🔒 Predecessor locked step.requires_predecessor_done AND upstream not done
📋 Required inputs unrecorded step._fp_missing_required_step_inputs() returns non-empty
📷 Photo required but missing step has a photo input prompt unrecorded
🚚 Inbound shipment tracking state = no_parts AND x_fc_receiving.x_fc_carrier_tracking present
📜 Cert ready / issued state = done AND fp.certificate.state in ('issued','sent')
Jump to blocker tappable; navigates to the predecessor step in the Workspace

Icons only render when their condition is true. Max 3-4 visible per card; overflow into a tooltip.


6. Card states — exhaustive catalog

13 mutually-exclusive states, computed server-side per job. Each card carries exactly one state; precedence rules below resolve conflicts.

6.1 State definitions

# State Background Left border State chip Timeline marker Triggered when
1 ready_mine #fffaeb (yellow) #f0a500 (yellow, 4px) "● Ready to start" (teal) current (yellow) active_step.state = 'ready' AND active_step.work_centre_id IN operator_paired_stations
2 running_mine #fffaeb (yellow) #f0a500 (yellow, 4px) "▶ Running 8m" (yellow) current (yellow) active_step.state = 'in_progress' AND active_step.work_centre_id IN operator_paired_stations
3 ready #ffffff (white) none "● Ready" (teal) current (yellow) active_step.state = 'ready' AND NOT mine
4 running #ffffff (white) none "▶ Running 3m" (yellow) current (yellow) active_step.state = 'in_progress' AND NOT mine
5 on_hold #fff5f5 (red) #dc3545 (red, 4px) "🔴 Quality Hold" (red) current.hold (red) fusion.plating.quality.hold exists on the job with state = 'open'
6 predecessor_locked #f8f9fa (grey) none "🔒 Waiting on Blasting" (grey) current.locked (grey) step._fp_should_block_predecessors() returns True AND any earlier-sequence step not done/skipped/cancelled
7 bake_due #fff8e1 (orange) #ff9800 (orange, 4px) " Bake window in 23m" (orange) current.bake (orange) fusion.plating.bake.window for this job has bake_required_by - now < 1h AND state = 'awaiting_bake'
8 awaiting_signoff #f5f0ff (purple) #6f42c1 (purple, 4px) "🔏 Awaiting QA sign-off" (purple) current.signoff (purple) step.requires_signoff AND step.state = 'done' AND step.signoff_user_id IS NULL (S22 gate)
9 idle_warning #fef9e7 (amber) #e6a800 (amber, 4px) "⏸ Idle 14h · Carlos" (amber) current.idle (amber) step.state = 'in_progress' AND now - step.last_activity_at > 8h (S16 cron)
10 awaiting_qc #e7f5fc (cyan) #17a2b8 (cyan, 4px) "🔬 QC pending · 2/6 items" (cyan) current.qc (cyan) fusion.plating.quality.check exists with state IN ('draft','in_progress') AND no other higher-precedence state
11 no_parts #f5f5f5 (grey, dashed) #6c757d (grey, 4px, dashed) "📦 Parts in transit · 2d" (grey) current.no_parts (grey) fp.job.state = 'confirmed' AND inbound fp.receiving.state = 'draft' AND no step has started yet
12 contract_review #ffffff (white) none "📋 QA-005 Awaiting QA Manager" (purple) current.paperwork (purple) active_step.recipe_node_id.default_kind = 'contract_review' AND not complete
13 done #f0f9f4 (green) #28a745 (green, 4px) "✓ Ready for pickup" (green) current.done (green) active step is in Shipping column AND fp.job.state = 'done'

6.2 Precedence rules

When multiple state triggers fire simultaneously, the resolver iterates through this explicit precedence list and takes the first match:

1. no_parts              (can't co-occur with anything else; checked first)
2. on_hold               (compliance bomb — always wins over operational states)
3. awaiting_signoff      (S22 gate — blocks advancement even when step.state='done')
4. awaiting_qc           (quality gate — sticky until QC closes)
5. bake_due              (time-sensitive compliance window)
6. predecessor_locked    (soft block on a step that's data-ready but workflow-locked)
7. idle_warning          (long-running supersedes plain running)
8. done                  (terminal state — only reached if none of the above apply)
9. contract_review       (paperwork — used at job entry before physical work)
10. running_mine         (more specific than running)
11. ready_mine           (more specific than ready)
12. running              (operational default for active work)
13. ready                (operational default for next-up work)

The numeric ordering here is the dispatch order in _fp_resolve_card_state, not a severity ranking. Examples:

  • Job both on-hold AND awaiting-signoff → on_hold (rule 2 fires before rule 3)
  • Job both bake-due AND running_mine → bake_due (rule 5 fires before rule 10)
  • Job in_progress for 14h at the operator's station → idle_warning (rule 7 fires before rule 10)

Implementation in §9.3 mirrors this list exactly — keep them synchronized.

6.3 Mine resolution

A card is "mine" when any of the following is true:

  1. active_step.work_centre_id.id IN operator.paired_work_centre_ids (operator paired to that specific station)
  2. active_step.assigned_user_id == operator.id (job is personally assigned to operator)
  3. active_step.area_kind == operator.preferred_area_kind (operator's profile lists this department, for cross-trained operators)

For MVP, use rule 1 only. Rules 2-3 are post-MVP enhancements.

The res.users.paired_work_centre_ids is a Many2many on the data model (so it's forward-compatible with cross-trained operators), but the MVP pairing UX keeps the existing single-station dropdown (fp_shopfloor_tech_store.currentStationId). On unlock, the M2M holds exactly one record — the selected station. A Phase 2 enhancement adds a multi-select picker so cross-trained operators can pair to 2-4 stations at once; the resolver above already supports that without further code change.


7. Sticky header

The header pins to the top of the kanban and remains visible during scroll.

7.1 Layout

┌────────────────────────────────────────────────────────────────────────────┐
│ 🏭 Shop Floor    [📍 Racking — Garry Singh ▾]  [Station|All Plant|Manager]│
│                  [📷 Scan QR] [🔓 Hand Off] [⚙]                            │
├────────────────────────────────────────────────────────────────────────────┤
│ [17 Active]  [3 At My Station]  [2 Bakes Due ≤2h]  [1 On Hold]  [2 Overdue]│
├────────────────────────────────────────────────────────────────────────────┤
│ [🔎 Search WO #, customer, part #, PO…]                                    │
│ [All] [My Station] [Running] [Blocked] [Overdue] [FAIR]                    │
└────────────────────────────────────────────────────────────────────────────┘

7.2 KPI strip — 5 tiles, clickable filters

Tile Source Click behavior
Active Jobs count(fp.job WHERE state IN ('confirmed','in_progress')) Filter chip "All" → shows everything
At My Station count of cards with state IN ('ready_mine','running_mine') Filter chip "My Station" → only mine
Bakes Due ≤2h count of cards with state = 'bake_due' AND bake_required_by - now < 2h Highlights orange cards
On Hold count of cards with state = 'on_hold' Filter to red cards; clicking opens Quality Holds list
Overdue count of cards where commitment_date < today AND state != 'done' Filter to overdue

Each tile is a button. Active tile shows a darker border + filled chip indicator.

7.3 Filter chips

Below KPIs, a row of toggleable filter chips. Multiple can be active (intersected with AND):

  • All (default; clears others)
  • My Station (cards where state IN ('ready_mine','running_mine'))
  • Running (active_step.state = 'in_progress')
  • Blocked (state IN ('on_hold','predecessor_locked','awaiting_signoff','awaiting_qc','no_parts'))
  • Overdue (commitment_date < today AND state != 'done')
  • FAIR (partner or spec requires FAIR; flagged via tag)

Chip state persists per operator per browser session (localStorage), so an operator who always filters to "My Station" doesn't have to re-set it each shift.

7.4 Station picker

The [📍 Racking — Garry Singh ▾] button:

  • Shows the operator's current paired station + their name
  • Dropdown lets them switch to a different station they're certified on (from their paired_work_centre_ids)
  • "All stations" option clears pairing
  • Disabled when the operator hasn't signed in (lock screen takes precedence)

7.5 Mode toggle

Three modes:

Mode Behavior
Station Cards at the paired station's column get the yellow mine treatment. Column header shows "📍 You're here". Other columns visible but neutral.
All Plant No "mine" highlight anywhere. Pure plant overview. Use case: supervisor walking the floor without paired station.
Manager Same as All Plant + adds bottleneck heatmap row at top (fp.work.centre.bottleneck_score driven). KPI strip swaps to manager-specific tiles (Late Risk, Avg Wait, etc.).

Manager mode is gated by fusion_plating.group_fusion_plating_manager.


8. Mini-timeline derivation

The 9-step bar on each card is not the recipe step count — it's a fixed 9-element array keyed by the 9 columns. Logic:

def _compute_mini_timeline(self):
    """Returns list of 9 dicts, one per column, with state in {'done','current','upcoming','hold','locked','bake','signoff','idle','qc','no_parts','done','paperwork'}."""
    timeline = []
    job_steps = self.step_ids.sorted('sequence')
    active = self.active_step_id
    active_area = active.area_kind if active else None
    for area in COLUMN_SEQUENCE:  # ['receiving', 'masking', 'blasting', ...]
        steps_in_area = job_steps.filtered(lambda s: s.area_kind == area)
        if not steps_in_area:
            # area not used by this recipe — still show as 'upcoming' to keep alignment
            timeline.append({'area': area, 'state': 'upcoming'})
            continue
        if all(s.state in ('done', 'skipped') for s in steps_in_area):
            timeline.append({'area': area, 'state': 'done'})
        elif area == active_area:
            # The card's state determines the current marker color
            timeline.append({'area': area, 'state': 'current', 'variant': self.card_state})
        else:
            timeline.append({'area': area, 'state': 'upcoming'})
    return timeline

Notes:

  • Recipes that skip a column (e.g. a job that doesn't need Masking) still render that column slot as "upcoming" grey — visual alignment matters more than perfect accuracy.
  • The variant field on the current marker tells the renderer which color to use (matches the card-state color: yellow / red / orange / purple / etc.).

9. Backend changes

9.1 New / modified fields

Model Field Type Purpose
fp.work.centre area_kind Selection (9 values) Routes each work centre to one of the 9 columns
fp.job.step area_kind Char, computed, stored, indexed Related from work_centre_id.area_kind with fallback to recipe_node_id.default_kind lookup
fp.job card_state Char, computed, stored, indexed The 13-state classifier; computed via _compute_card_state with the precedence rules in §6.2
fp.job mini_timeline_json Text, computed JSON-serialized output of _compute_mini_timeline
fp.job.step last_activity_at Datetime, indexed Updated on any state transition / move / chatter post; drives idle-warning detection (S16)
res.users paired_work_centre_ids M2M fp.work.centre Operator's certified stations; resolved on PIN unlock

area_kind Selection values (used by both fp.work.centre and fp.job.step):

COLUMN_SEQUENCE = [
    ('receiving',  'Receiving'),
    ('masking',    'Masking'),
    ('blasting',   'Blasting'),
    ('racking',    'Racking'),
    ('plating',    'Plating'),
    ('baking',     'Baking'),
    ('de_racking', 'De-Racking'),
    ('inspection', 'Final inspection'),
    ('shipping',   'Shipping'),
]

9.2 New endpoint — /fp/landing/plant_kanban

Replaces the existing /fp/landing/kanban. Returns:

{
  "ok": true,
  "mode": "station",
  "paired_station": {"id": 12, "name": "Rack Station 1", "area_kind": "racking"},
  "kpis": {
    "active_jobs": 17,
    "at_my_station": 3,
    "bakes_due_soon": 2,
    "on_hold": 1,
    "overdue": 2
  },
  "columns": [
    {
      "area_kind": "receiving",
      "label": "Receiving",
      "is_mine": false,
      "card_ids": [2885, 2886, 2887]
    },
    {
      "area_kind": "masking",
      "label": "Masking",
      "is_mine": false,
      "card_ids": [2884]
    },
    ...
  ],
  "cards": {
    "2885": {
      "wo_name": "WO-30049",
      "is_mine": true,
      "card_state": "ready_mine",
      "due_date": "2026-05-16",
      "due_label": "Due May 16 · 3d",
      "is_overdue": false,
      "customer": "ABC Manufacturing",
      "part_number": "9876699373",
      "part_revision": "A",
      "qty": 5,
      "po_number": "4501882",
      "recipe_name": "ENP-ALUM-BASIC",
      "spec_code": "AMS-2404 Type II",
      "tags": ["rush", "fair"],
      "step_name": "Racking",
      "step_seq": 4,
      "step_total": 14,
      "tank_label": "Rack Station 1",
      "state_chip": {"label": "● Ready to start", "kind": "ready"},
      "operator": {"id": null, "name": null, "initials": null},
      "duration_label": null,
      "icons": ["signoff_required"],
      "mini_timeline": [
        {"area": "receiving", "state": "done"},
        {"area": "masking", "state": "done"},
        {"area": "blasting", "state": "done"},
        {"area": "racking", "state": "current", "variant": "ready_mine"},
        ...
      ]
    },
    ...
  }
}

Design choices:

  • Two-tier structure (columns + cards) keeps payload small when 2 cards happen to be at the same step — no per-column-per-card duplication.
  • card_state is server-computed — frontend just maps state → CSS class.
  • mini_timeline is server-computed — frontend renders the 9 dots without knowing the recipe shape.
  • Operator info is denormalized — initials, name, color hash all in the payload so the frontend doesn't fan out RPCs.

9.3 State computation — _compute_card_state

Matches the precedence list in §6.2 exactly. Both must stay in sync.

def _compute_card_state(self):
    for job in self:
        # Edge: job has no active step (all pending or all done)
        if not job.active_step_id:
            # rule 1
            if job.state == 'confirmed' and job._fp_inbound_not_received():
                job.card_state = 'no_parts'
            else:
                # Fallback to first pending step's kind; otherwise contract_review
                job.card_state = 'contract_review'
            continue

        step = job.active_step_id

        # rule 1 — no_parts (even with an active step, if inbound is still draft)
        if job._fp_inbound_not_received():
            job.card_state = 'no_parts'
            continue
        # rule 2 — on_hold
        if job._fp_has_open_hold():
            job.card_state = 'on_hold'
            continue
        # rule 3 — awaiting_signoff (S22)
        if (step.requires_signoff and step.state == 'done'
                and not step.signoff_user_id):
            job.card_state = 'awaiting_signoff'
            continue
        # rule 4 — awaiting_qc
        if job._fp_has_pending_qc():
            job.card_state = 'awaiting_qc'
            continue
        # rule 5 — bake_due
        if job._fp_bake_window_due_soon():
            job.card_state = 'bake_due'
            continue
        # rule 6 — predecessor_locked
        if (step._fp_should_block_predecessors()
                and step._fp_has_unfinished_predecessors()):
            job.card_state = 'predecessor_locked'
            continue
        # rule 7 — idle_warning (S16)
        if step.state == 'in_progress' and step._fp_is_idle(threshold_hours=8):
            job.card_state = 'idle_warning'
            continue
        # rule 8 — done (terminal, only reached when nothing above fires)
        if step.area_kind == 'shipping' and job.state == 'done':
            job.card_state = 'done'
            continue
        # rule 9 — contract_review
        if step.recipe_node_id.default_kind == 'contract_review':
            job.card_state = 'contract_review'
            continue
        # rules 10/12 — running (mine vs not)
        if step.state == 'in_progress':
            job.card_state = ('running_mine' if job._fp_is_mine()
                              else 'running')
            continue
        # rules 11/13 — ready (mine vs not)
        if step.state == 'ready':
            job.card_state = ('ready_mine' if job._fp_is_mine()
                              else 'ready')
            continue
        # Safe default
        job.card_state = 'ready'

Each _fp_* helper is a small method on fp.job (or fp.job.step) that encapsulates one precedence check. Centralizing them this way means future audits can extend the catalog without touching the dispatch.

9.4 Helpers

Helper Returns Source data
_fp_inbound_not_received() bool fp.receiving linked via SO; state = 'draft'
_fp_has_open_hold() bool fusion.plating.quality.hold with state = 'open' linked via job_id
_fp_has_pending_qc() bool fusion.plating.quality.check with state IN ('draft','in_progress') linked via job_id
_fp_bake_window_due_soon() bool fusion.plating.bake.window linked, bake_required_by - now < 1h, state = 'awaiting_bake'
step._fp_is_idle(threshold_hours=8) bool now - last_activity_at > threshold
_fp_is_mine() bool active_step.work_centre_id IN env.user.paired_work_centre_ids

10. Frontend changes

10.1 OWL component structure

New / modified files in fusion_plating_shopfloor/static/src/:

js/
  plant_kanban.js                         (new — replaces shopfloor_landing.js)
  components/
    plant_card.js                         (new — Variant C card component)
    mini_timeline.js                      (new — 9-step horizontal bar)
    column_header.js                      (new — column header with "📍 You're here" badge)
    kpi_tile.js                           (new — clickable KPI button)
    filter_chip.js                        (new — toggleable filter chip)
xml/
  plant_kanban.xml                        (new)
  components/
    plant_card.xml                        (new)
    mini_timeline.xml                     (new)
    column_header.xml                     (new)
    kpi_tile.xml                          (new)
    filter_chip.xml                       (new)
scss/
  plant_kanban.scss                       (new — board layout + sticky header)
  components/
    _plant_card.scss                      (new — 13 card-state styles)
    _mini_timeline.scss                   (new — timeline dots)
    _column_header.scss                   (new)
    _kpi_tile.scss                        (new)
    _filter_chip.scss                     (new)

10.2 Component tree

FpPlantKanban (top-level client action)
├── FpTabletLock (existing wrapper for PIN gate)
└── (when unlocked)
    ├── PlantHeader
    │   ├── StationPicker
    │   ├── ModeToggle
    │   ├── ToolbarButtons (Scan / Hand Off / Settings)
    │   ├── KpiStrip (5 × KpiTile)
    │   └── FilterRow (search input + 6 × FilterChip)
    └── Board
        └── 9 × Column
            ├── ColumnHeader
            └── PlantCard[]
                ├── CardHeader (WO, due)
                ├── CardBody (customer, PN, recipe, tags)
                ├── CardStep (step name + chips)
                ├── MiniTimeline
                └── CardFooter (progress + operator + icons)

10.3 Card state CSS

All 13 states share the base .plant-card class with state-specific modifier classes:

.plant-card {
  background: $card-bg;
  border: 1px solid $border-color;
  border-radius: 8px;
  // ... base layout

  &.state-ready_mine, &.state-running_mine {
    background: #fffaeb;
    border-left: 4px solid #f0a500;
    padding-left: 9px;
  }
  &.state-on_hold {
    background: #fff5f5;
    border-left: 4px solid #dc3545;
    padding-left: 9px;
  }
  &.state-bake_due {
    background: #fff8e1;
    border-left: 4px solid #ff9800;
    padding-left: 9px;
  }
  &.state-awaiting_signoff {
    background: #f5f0ff;
    border-left: 4px solid #6f42c1;
    padding-left: 9px;
  }
  &.state-idle_warning {
    background: #fef9e7;
    border-left: 4px solid #e6a800;
    padding-left: 9px;
  }
  &.state-awaiting_qc {
    background: #e7f5fc;
    border-left: 4px solid #17a2b8;
    padding-left: 9px;
  }
  &.state-predecessor_locked {
    background: #f8f9fa;
  }
  &.state-no_parts {
    background: #f5f5f5;
    border: 1px dashed #999;
    border-left: 4px solid #6c757d;
    padding-left: 9px;
  }
  &.state-done {
    background: #f0f9f4;
    border-left: 4px solid #28a745;
    padding-left: 9px;
  }
  // state-ready, state-running, state-contract_review: default neutral white
}

Dark-mode SCSS branch follows the project pattern ($o-webclient-color-scheme == dark block) with adjusted hex values.

10.4 Auto-refresh

Polling every 10s via setInterval. On each tick:

  1. Fetch /fp/landing/plant_kanban with current mode + filter state in the request payload.
  2. Diff against current state.
  3. Apply changes to OWL reactive state — cards that moved columns animate the transition (fade-out from old column, fade-in at new column over 200ms).

Hand-Off, mode toggle, station-picker, and filter chip changes trigger an immediate refresh.

10.5 Card tap behavior

Single tap on a card → opens Job Workspace (fp_job_workspace client action) with the WO pre-loaded. No quick-action sheet on tablet (would compete with the Workspace's own action rail).

Card has a small "" icon in the top-right that opens a quick-info popover (for supervisor walk-bys who want details without leaving the kanban). Post-MVP.


11. Migration & rollout

11.1 Database migration

# fusion_plating/migrations/19.0.21.0.0/post-migrate.py

def migrate(cr, version):
    """Backfill fp.work.centre.area_kind from existing kind values."""
    cr.execute("""
        UPDATE fp_work_centre
           SET area_kind = CASE kind
               WHEN 'wet_line' THEN 'plating'
               WHEN 'bake'     THEN 'baking'
               WHEN 'mask'     THEN 'masking'
               WHEN 'rack'     THEN 'racking'
               WHEN 'inspect'  THEN 'inspection'
               ELSE 'plating'
           END
         WHERE area_kind IS NULL
    """)
    # Log unmapped centres for manual review
    cr.execute("""
        SELECT id, name FROM fp_work_centre WHERE area_kind IS NULL
    """)
    for row in cr.fetchall():
        _logger.warning("Work centre %s (%s) has no area_kind — defaulted to 'plating'", row[0], row[1])

11.2 Feature flag

New config setting x_fc_shopfloor_layout on res.config.settings:

  • legacy (default during rollout) — existing landing
  • v2 — new plant view

Once validated on entech, default flips to v2 and legacy code can be removed in a follow-up cleanup.

The client action fp_shopfloor_landing resolver chooses which OWL component to mount based on this setting.

11.3 Rollout sequence

  1. Ship migration + backend (area_kind, card_state, mini_timeline_json, helpers, endpoint) under the v2 flag.
  2. Ship OWL components under the v2 flag. Both screens coexist.
  3. QA on entech: flip x_fc_shopfloor_layout = 'v2', validate end-to-end.
  4. Run battle-test scenarios (S1-S23) against the new view to confirm no regression.
  5. Flip default to v2 site-wide.
  6. After 2 weeks of stable v2, remove legacy code.

12. Testing strategy

12.1 Unit tests

  • test_card_state_computation — for each of the 13 states, construct an fp.job in that exact data shape, assert card_state resolves correctly
  • test_card_state_precedence — overlay multiple triggers (e.g. on-hold + bake-due), assert precedence rules produce the documented winner
  • test_area_kind_routing — for each step kind in the mapping table, assert it routes to the correct column
  • test_mini_timeline — for a 14-step recipe at various points, assert the 9-element output matches expectations (including skipped columns rendered as upcoming)
  • test_one_card_per_job_invariant — across a realistic 17-job board, assert no two entries in cards{} share the same fp.job.id

12.2 Persona walks

Re-run the battle-test scenarios that drove this redesign:

  • S20 walk — operator persona traversal of the tablet. Confirm: card density readable, "mine" highlight obvious, can find a specific WO in <5s via search.
  • S22 / S23 simulations — finish a step that needs sign-off / transition form, confirm the card transitions to awaiting_signoff / awaiting_qc state correctly.
  • 20-step-recipe regression — load a synthetic job with 25+ recipe steps, confirm it occupies one and only one card on the board.

12.3 Visual snapshot tests

Per state, a Playwright/headless-chromium snapshot of a single card at fixed viewport. Diff against checked-in golden images on every PR. Catches accidental CSS regressions.


13. Open questions (deferred)

These don't block MVP but should be tracked for the follow-up plan.

# Question Suggested resolution
Q1 Drag-and-drop card between columns? No for MVP. State transitions happen via the Workspace action rail or Move dialogs. The kanban reflects state, doesn't drive it.
Q2 Empty-column auto-collapse? No. Column position = mental model. Collapsing breaks the sequence.
Q3 Sort within column? MVP: most urgent first — overdue → bake-due → ready → running → idle → locked → done. Post-MVP: operator-toggleable.
Q4 Card tap → quick-action sheet vs. open Workspace? MVP: open Workspace. Quick-action sheet is a post-MVP enhancement.
Q5 Manager mode KPI tile swap? Phase 2. MVP ships with the same 5 KPI tiles in all modes. Phase 2 adds manager-specific tiles (late-risk %, avg wait per station, bottleneck score).
Q6 Sibling jobs (WO-30029-01 / -02) visual grouping? No special treatment for MVP. Each is its own card. If siblings clutter, post-MVP adds a "group siblings" toggle.
Q7 Bottleneck heatmap row in manager mode? Phase 2. Reuses existing fp.work.centre.bottleneck_score.
Q8 Mobile (phone) breakpoint? Phase 2. MVP optimized for 1080p tablet. Phone view = collapse to single-column scroll.

14. Summary

Question Answer
Layout 9 fixed columns in sequence (Receiving → … → Shipping)
Card model One card per fp.job, always in the column matching the active step's area_kind
Card density Variant C — full info with mini-timeline
State catalog 13 mutually-exclusive states with precedence rules
Operator focus Plant-wide view, paired-station column + "mine" cards highlighted
Backend touch New area_kind Selection, new card_state compute, new /fp/landing/plant_kanban endpoint
Frontend touch New OWL component tree under fp_plant_kanban client action
Rollout Feature flag x_fc_shopfloor_layout, parallel deployment, flip default after entech validation
Recipe-step scaling Doesn't matter — 5-step or 50-step recipes both produce one card moving across 9 fixed columns

The redesign solves the "one job in N columns" problem by re-anchoring grouping at the department level and decoupling the kanban from recipe step count. Every floor scenario in the audit + battle-test catalog (S1-S23) maps to one of the 13 documented states.

Implementation plan to follow.