diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py
index 5e6f5730..f59d6540 100644
--- a/fusion_plating/fusion_plating_jobs/models/fp_job.py
+++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py
@@ -287,6 +287,13 @@ class FpJob(models.Model):
if not job.active_step_id:
if job.state == 'done':
job.card_state = 'done'
+ elif job.state == 'awaiting_cert':
+ # Spec 2026-05-25 — state drives card_state for
+ # post-shop jobs (active_step_id is False because
+ # every step is terminal).
+ job.card_state = 'awaiting_cert'
+ elif job.state == 'awaiting_ship':
+ job.card_state = 'awaiting_ship'
elif (job.state == 'confirmed'
and job._fp_inbound_not_received()):
job.card_state = 'no_parts'
@@ -327,6 +334,17 @@ class FpJob(models.Model):
and step._fp_is_idle(threshold_hours=8)):
job.card_state = 'idle_warning'
continue
+ # Rule 7.5 — awaiting_cert + awaiting_ship (spec 2026-05-25)
+ # State drives card_state regardless of step state. Inserted
+ # BEFORE the done rule because state='done' jobs are filtered
+ # off the board upstream so the done rule is unreachable
+ # from cards we'd actually render.
+ if job.state == 'awaiting_cert':
+ job.card_state = 'awaiting_cert'
+ continue
+ if job.state == 'awaiting_ship':
+ job.card_state = 'awaiting_ship'
+ continue
# Rule 8 — done
if step.area_kind == 'shipping' and job.state == 'done':
job.card_state = 'done'
@@ -356,10 +374,50 @@ class FpJob(models.Model):
'step_ids.area_kind',
'active_step_id',
'card_state',
+ 'state',
)
def _compute_mini_timeline_json(self):
- """9-element JSON array, one per Shop Floor column."""
+ """9-element JSON array, one per Shop Floor column.
+
+ For awaiting_cert / awaiting_ship (spec 2026-05-25): the
+ Final-inspection or Shipping dot renders as 'current' with the
+ state-named variant; all earlier dots render 'done'. Lets the
+ QM see at a glance "this card has cleared the line, just
+ waiting on paperwork/shipping".
+ """
for job in self:
+ # Post-shop state override (spec 2026-05-25): visually walk
+ # the card across the two right-most columns even though
+ # the recipe may not have steps with those area_kinds.
+ if job.state == 'awaiting_cert':
+ timeline = []
+ for area in _COLUMN_SEQUENCE:
+ if area == 'inspection':
+ timeline.append({
+ 'area': area,
+ 'state': 'current',
+ 'variant': 'awaiting_cert',
+ })
+ elif area == 'shipping':
+ timeline.append({'area': area, 'state': 'upcoming'})
+ else:
+ timeline.append({'area': area, 'state': 'done'})
+ job.mini_timeline_json = json.dumps(timeline)
+ continue
+ if job.state == 'awaiting_ship':
+ timeline = []
+ for area in _COLUMN_SEQUENCE:
+ if area == 'shipping':
+ timeline.append({
+ 'area': area,
+ 'state': 'current',
+ 'variant': 'awaiting_ship',
+ })
+ else:
+ timeline.append({'area': area, 'state': 'done'})
+ job.mini_timeline_json = json.dumps(timeline)
+ continue
+ # Standard path — pre-existing logic.
active_area = (job.active_step_id.area_kind
if job.active_step_id else None)
timeline = []
diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py
index 8e0ce0f3..f3e17f1b 100644
--- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py
+++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py
@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Shop Floor',
- 'version': '19.0.33.2.0',
+ 'version': '19.0.34.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
'description': """
diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py b/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py
index 52a12863..c1f9b70d 100644
--- a/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py
+++ b/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py
@@ -38,11 +38,13 @@ _SORT_PRIORITY = {
'no_parts': 1,
'bake_due': 2,
'awaiting_signoff': 3,
+ 'awaiting_cert': 3.5, # spec 2026-05-25 — after awaiting_signoff
'awaiting_qc': 4,
'ready_mine': 5,
'running_mine': 6,
'ready': 7,
'running': 8,
+ 'awaiting_ship': 8.5, # spec 2026-05-25 — after running
'idle_warning': 9,
'predecessor_locked': 10,
'contract_review': 11,
@@ -65,13 +67,18 @@ class PlantKanbanController(http.Controller):
else env['fp.work.centre'])
paired_area = paired.area_kind if paired else None
- # Base domain — only in-flight jobs.
+ # Base domain — in-flight jobs.
# 2026-05-24 (spec 2026-05-24-shopfloor-live-step-fix-design.md
# Defect 4 / Change 3): done + cancelled jobs drop off the live
# board. They stay reachable via smart buttons, the Plating Jobs
# backend list, and history reports.
+ # 2026-05-25 (spec post-shop-cert-shipping-job-states): awaiting_cert
+ # + awaiting_ship are included so completed-but-uncertified /
+ # ready-to-ship jobs stay visible in the Final inspection /
+ # Shipping columns.
domain = [
- ('state', 'in', ('confirmed', 'in_progress')),
+ ('state', 'in', ('confirmed', 'in_progress',
+ 'awaiting_cert', 'awaiting_ship')),
]
filters = filters or {}
if filters.get('overdue'):
@@ -88,6 +95,11 @@ class PlantKanbanController(http.Controller):
)))
if filters.get('mine'):
domain.append(('card_state', 'in', ('ready_mine', 'running_mine')))
+ # Spec 2026-05-25 — post-shop state filter chips
+ if filters.get('awaiting_cert'):
+ domain.append(('state', '=', 'awaiting_cert'))
+ if filters.get('awaiting_ship'):
+ domain.append(('state', '=', 'awaiting_ship'))
if filters.get('fair'):
# Match either part-catalog or partner level requires_first_article
domain.append('|')
@@ -133,6 +145,13 @@ class PlantKanbanController(http.Controller):
'on_hold': sum(
1 for j in jobs if j.card_state == 'on_hold'
),
+ # Spec 2026-05-25 — post-shop state KPIs
+ 'awaiting_cert': sum(
+ 1 for j in jobs if j.state == 'awaiting_cert'
+ ),
+ 'awaiting_ship': sum(
+ 1 for j in jobs if j.state == 'awaiting_ship'
+ ),
'overdue': sum(
1 for j in jobs
if j.date_deadline and j.date_deadline.date() < date.today()
@@ -181,6 +200,14 @@ def _resolve_card_area(job):
# active step is — the receiver is who acts.
if job.card_state == 'no_parts':
return 'receiving'
+ # 2026-05-25 (spec post-shop-cert-shipping-job-states): state drives
+ # column for the two post-shop states. The recipe may not even have
+ # a step with inspection / shipping area_kind, but the card belongs
+ # in those columns once the job has cleared all shop steps.
+ if job.state == 'awaiting_cert':
+ return 'inspection'
+ if job.state == 'awaiting_ship':
+ return 'shipping'
if job.active_step_id and job.active_step_id.area_kind:
return job.active_step_id.area_kind
# Orphan fallback — represents a data integrity issue, not a
@@ -319,6 +346,11 @@ def _state_chip(card_state, step):
return {'label': _('📦 Parts in transit'), 'kind': 'no_parts'}
if card_state == 'contract_review':
return {'label': _('📋 QA-005 review'), 'kind': 'paperwork'}
+ # Spec 2026-05-25 — post-shop states
+ if card_state == 'awaiting_cert':
+ return {'label': _('🏷️ Awaiting CoC'), 'kind': 'awaiting_cert'}
+ if card_state == 'awaiting_ship':
+ return {'label': _('📦 Ready to ship'), 'kind': 'awaiting_ship'}
if card_state == 'done':
return {'label': _('✓ Ready for pickup'), 'kind': 'done'}
return {'label': '', 'kind': ''}
diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/_plant_tokens.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/_plant_tokens.scss
index 4f19f42d..b7942e57 100644
--- a/fusion_plating/fusion_plating_shopfloor/static/src/scss/_plant_tokens.scss
+++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/_plant_tokens.scss
@@ -34,6 +34,12 @@ $_plant-noparts-border-hex: #6c757d;
$_plant-done-bg-hex: #f0f9f4;
$_plant-done-border-hex: #28a745;
+// Spec 2026-05-25 — post-shop states
+$_plant-awaiting-cert-bg-hex: #fff3cd;
+$_plant-awaiting-cert-border-hex: #ff9800;
+$_plant-awaiting-ship-bg-hex: #d1f1d4;
+$_plant-awaiting-ship-border-hex: #2e7d32;
+
// === Dark-mode overrides (compile-time branch per project rule) ===
@if $o-webclient-color-scheme == dark {
$_plant-bg-hex: #1a1d21 !global;
@@ -51,6 +57,12 @@ $_plant-done-border-hex: #28a745;
$_plant-locked-bg-hex: #2d3138 !global;
$_plant-noparts-bg-hex: #2d3138 !global;
$_plant-done-bg-hex: #14281a !global;
+
+ // Spec 2026-05-25 — post-shop states (dark)
+ $_plant-awaiting-cert-bg-hex: #3a2f15 !global;
+ $_plant-awaiting-cert-border-hex: #ffb74d !global;
+ $_plant-awaiting-ship-bg-hex: #1a2d1f !global;
+ $_plant-awaiting-ship-border-hex: #66bb6a !global;
}
// === CSS-custom-property wrappers so future themes can override ===
@@ -78,3 +90,9 @@ $plant-noparts-bg: var(--fp-plant-noparts-bg, $_plant-noparts-bg-hex);
$plant-noparts-border: var(--fp-plant-noparts-border, $_plant-noparts-border-hex);
$plant-done-bg: var(--fp-plant-done-bg, $_plant-done-bg-hex);
$plant-done-border: var(--fp-plant-done-border, $_plant-done-border-hex);
+
+// Spec 2026-05-25 — post-shop states
+$plant-awaiting-cert-bg: var(--fp-plant-awaiting-cert-bg, $_plant-awaiting-cert-bg-hex);
+$plant-awaiting-cert-border: var(--fp-plant-awaiting-cert-border, $_plant-awaiting-cert-border-hex);
+$plant-awaiting-ship-bg: var(--fp-plant-awaiting-ship-bg, $_plant-awaiting-ship-bg-hex);
+$plant-awaiting-ship-border: var(--fp-plant-awaiting-ship-border, $_plant-awaiting-ship-border-hex);
diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_plant_card.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_plant_card.scss
index 4f8cc585..f6fe3c1d 100644
--- a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_plant_card.scss
+++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_plant_card.scss
@@ -66,6 +66,17 @@
border-left: 4px solid $plant-done-border;
padding-left: 7px;
}
+ // Spec 2026-05-25 — post-shop states
+ &.state-awaiting_cert {
+ background: $plant-awaiting-cert-bg;
+ border-left: 4px solid $plant-awaiting-cert-border;
+ padding-left: 7px;
+ }
+ &.state-awaiting_ship {
+ background: $plant-awaiting-ship-bg;
+ border-left: 4px solid $plant-awaiting-ship-border;
+ padding-left: 7px;
+ }
&.overdue:not(.mine):not(.state-on_hold):not(.state-bake_due) {
border-left: 4px solid $plant-hold-border;
padding-left: 7px;
diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_kanban.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_kanban.xml
index e279ec32..501f67ee 100644
--- a/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_kanban.xml
+++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/plant_kanban.xml
@@ -47,6 +47,17 @@
kind="'urgent'"
active="!!state.filters.on_hold"
onClick="() => this.toggleFilter('on_hold')"/>
+
+
+
+
+
+