feat(plant_kanban): post-shop states visible on board (Tasks 9-13)
Controller (plant_kanban.py):
- Widen domain: state IN (confirmed, in_progress, awaiting_cert,
awaiting_ship). Done jobs still drop off.
- _resolve_card_area: state=awaiting_cert → 'inspection' column,
state=awaiting_ship → 'shipping' column. State drives column
regardless of recipe shape.
- _state_chip: 🏷️ Awaiting CoC (amber) + 📦 Ready to ship (green).
- _SORT_PRIORITY: awaiting_cert=3.5, awaiting_ship=8.5.
- KPI dict: awaiting_cert + awaiting_ship counts.
- Filter clauses for the two new chips.
Model (fp_job.py):
- _compute_card_state handles new states in BOTH branches: the
no-active-step early return (where awaiting_cert/ship cards
land — all steps terminal) AND the per-step branch (defensive).
- _compute_mini_timeline_json: awaiting_cert paints inspection
dot 'current'; awaiting_ship paints shipping dot 'current'.
All earlier dots show 'done'.
SCSS (_plant_tokens.scss + _plant_card.scss):
- New tokens for amber (cert) + green (ship), light + dark variants
via the existing $o-webclient-color-scheme compile-time branch.
- .state-awaiting_cert / .state-awaiting_ship modifier classes
match the existing border-left pattern.
XML (plant_kanban.xml):
- Two new KPI tiles + two new filter chips wired to the state
filter clauses.
Manifest: fusion_plating_shopfloor 19.0.33.2.0 → 19.0.34.0.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -287,6 +287,13 @@ class FpJob(models.Model):
|
|||||||
if not job.active_step_id:
|
if not job.active_step_id:
|
||||||
if job.state == 'done':
|
if job.state == 'done':
|
||||||
job.card_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'
|
elif (job.state == 'confirmed'
|
||||||
and job._fp_inbound_not_received()):
|
and job._fp_inbound_not_received()):
|
||||||
job.card_state = 'no_parts'
|
job.card_state = 'no_parts'
|
||||||
@@ -327,6 +334,17 @@ class FpJob(models.Model):
|
|||||||
and step._fp_is_idle(threshold_hours=8)):
|
and step._fp_is_idle(threshold_hours=8)):
|
||||||
job.card_state = 'idle_warning'
|
job.card_state = 'idle_warning'
|
||||||
continue
|
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
|
# Rule 8 — done
|
||||||
if step.area_kind == 'shipping' and job.state == 'done':
|
if step.area_kind == 'shipping' and job.state == 'done':
|
||||||
job.card_state = 'done'
|
job.card_state = 'done'
|
||||||
@@ -356,10 +374,50 @@ class FpJob(models.Model):
|
|||||||
'step_ids.area_kind',
|
'step_ids.area_kind',
|
||||||
'active_step_id',
|
'active_step_id',
|
||||||
'card_state',
|
'card_state',
|
||||||
|
'state',
|
||||||
)
|
)
|
||||||
def _compute_mini_timeline_json(self):
|
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:
|
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
|
active_area = (job.active_step_id.area_kind
|
||||||
if job.active_step_id else None)
|
if job.active_step_id else None)
|
||||||
timeline = []
|
timeline = []
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Shop Floor',
|
'name': 'Fusion Plating — Shop Floor',
|
||||||
'version': '19.0.33.2.0',
|
'version': '19.0.34.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -38,11 +38,13 @@ _SORT_PRIORITY = {
|
|||||||
'no_parts': 1,
|
'no_parts': 1,
|
||||||
'bake_due': 2,
|
'bake_due': 2,
|
||||||
'awaiting_signoff': 3,
|
'awaiting_signoff': 3,
|
||||||
|
'awaiting_cert': 3.5, # spec 2026-05-25 — after awaiting_signoff
|
||||||
'awaiting_qc': 4,
|
'awaiting_qc': 4,
|
||||||
'ready_mine': 5,
|
'ready_mine': 5,
|
||||||
'running_mine': 6,
|
'running_mine': 6,
|
||||||
'ready': 7,
|
'ready': 7,
|
||||||
'running': 8,
|
'running': 8,
|
||||||
|
'awaiting_ship': 8.5, # spec 2026-05-25 — after running
|
||||||
'idle_warning': 9,
|
'idle_warning': 9,
|
||||||
'predecessor_locked': 10,
|
'predecessor_locked': 10,
|
||||||
'contract_review': 11,
|
'contract_review': 11,
|
||||||
@@ -65,13 +67,18 @@ class PlantKanbanController(http.Controller):
|
|||||||
else env['fp.work.centre'])
|
else env['fp.work.centre'])
|
||||||
paired_area = paired.area_kind if paired else None
|
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
|
# 2026-05-24 (spec 2026-05-24-shopfloor-live-step-fix-design.md
|
||||||
# Defect 4 / Change 3): done + cancelled jobs drop off the live
|
# Defect 4 / Change 3): done + cancelled jobs drop off the live
|
||||||
# board. They stay reachable via smart buttons, the Plating Jobs
|
# board. They stay reachable via smart buttons, the Plating Jobs
|
||||||
# backend list, and history reports.
|
# 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 = [
|
domain = [
|
||||||
('state', 'in', ('confirmed', 'in_progress')),
|
('state', 'in', ('confirmed', 'in_progress',
|
||||||
|
'awaiting_cert', 'awaiting_ship')),
|
||||||
]
|
]
|
||||||
filters = filters or {}
|
filters = filters or {}
|
||||||
if filters.get('overdue'):
|
if filters.get('overdue'):
|
||||||
@@ -88,6 +95,11 @@ class PlantKanbanController(http.Controller):
|
|||||||
)))
|
)))
|
||||||
if filters.get('mine'):
|
if filters.get('mine'):
|
||||||
domain.append(('card_state', 'in', ('ready_mine', 'running_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'):
|
if filters.get('fair'):
|
||||||
# Match either part-catalog or partner level requires_first_article
|
# Match either part-catalog or partner level requires_first_article
|
||||||
domain.append('|')
|
domain.append('|')
|
||||||
@@ -133,6 +145,13 @@ class PlantKanbanController(http.Controller):
|
|||||||
'on_hold': sum(
|
'on_hold': sum(
|
||||||
1 for j in jobs if j.card_state == 'on_hold'
|
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(
|
'overdue': sum(
|
||||||
1 for j in jobs
|
1 for j in jobs
|
||||||
if j.date_deadline and j.date_deadline.date() < date.today()
|
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.
|
# active step is — the receiver is who acts.
|
||||||
if job.card_state == 'no_parts':
|
if job.card_state == 'no_parts':
|
||||||
return 'receiving'
|
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:
|
if job.active_step_id and job.active_step_id.area_kind:
|
||||||
return job.active_step_id.area_kind
|
return job.active_step_id.area_kind
|
||||||
# Orphan fallback — represents a data integrity issue, not a
|
# 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'}
|
return {'label': _('📦 Parts in transit'), 'kind': 'no_parts'}
|
||||||
if card_state == 'contract_review':
|
if card_state == 'contract_review':
|
||||||
return {'label': _('📋 QA-005 review'), 'kind': 'paperwork'}
|
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':
|
if card_state == 'done':
|
||||||
return {'label': _('✓ Ready for pickup'), 'kind': 'done'}
|
return {'label': _('✓ Ready for pickup'), 'kind': 'done'}
|
||||||
return {'label': '', 'kind': ''}
|
return {'label': '', 'kind': ''}
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ $_plant-noparts-border-hex: #6c757d;
|
|||||||
$_plant-done-bg-hex: #f0f9f4;
|
$_plant-done-bg-hex: #f0f9f4;
|
||||||
$_plant-done-border-hex: #28a745;
|
$_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) ===
|
// === Dark-mode overrides (compile-time branch per project rule) ===
|
||||||
@if $o-webclient-color-scheme == dark {
|
@if $o-webclient-color-scheme == dark {
|
||||||
$_plant-bg-hex: #1a1d21 !global;
|
$_plant-bg-hex: #1a1d21 !global;
|
||||||
@@ -51,6 +57,12 @@ $_plant-done-border-hex: #28a745;
|
|||||||
$_plant-locked-bg-hex: #2d3138 !global;
|
$_plant-locked-bg-hex: #2d3138 !global;
|
||||||
$_plant-noparts-bg-hex: #2d3138 !global;
|
$_plant-noparts-bg-hex: #2d3138 !global;
|
||||||
$_plant-done-bg-hex: #14281a !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 ===
|
// === 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-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-bg: var(--fp-plant-done-bg, $_plant-done-bg-hex);
|
||||||
$plant-done-border: var(--fp-plant-done-border, $_plant-done-border-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);
|
||||||
|
|||||||
@@ -66,6 +66,17 @@
|
|||||||
border-left: 4px solid $plant-done-border;
|
border-left: 4px solid $plant-done-border;
|
||||||
padding-left: 7px;
|
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) {
|
&.overdue:not(.mine):not(.state-on_hold):not(.state-bake_due) {
|
||||||
border-left: 4px solid $plant-hold-border;
|
border-left: 4px solid $plant-hold-border;
|
||||||
padding-left: 7px;
|
padding-left: 7px;
|
||||||
|
|||||||
@@ -47,6 +47,17 @@
|
|||||||
kind="'urgent'"
|
kind="'urgent'"
|
||||||
active="!!state.filters.on_hold"
|
active="!!state.filters.on_hold"
|
||||||
onClick="() => this.toggleFilter('on_hold')"/>
|
onClick="() => this.toggleFilter('on_hold')"/>
|
||||||
|
<!-- Spec 2026-05-25 — post-shop state tiles -->
|
||||||
|
<FpKpiTile value="state.data.kpis.awaiting_cert"
|
||||||
|
label="'Awaiting CoC'"
|
||||||
|
kind="'warn'"
|
||||||
|
active="!!state.filters.awaiting_cert"
|
||||||
|
onClick="() => this.toggleFilter('awaiting_cert')"/>
|
||||||
|
<FpKpiTile value="state.data.kpis.awaiting_ship"
|
||||||
|
label="'Ready to Ship'"
|
||||||
|
kind="'good'"
|
||||||
|
active="!!state.filters.awaiting_ship"
|
||||||
|
onClick="() => this.toggleFilter('awaiting_ship')"/>
|
||||||
<FpKpiTile value="state.data.kpis.overdue"
|
<FpKpiTile value="state.data.kpis.overdue"
|
||||||
label="'Overdue'"
|
label="'Overdue'"
|
||||||
kind="'urgent'"
|
kind="'urgent'"
|
||||||
@@ -78,6 +89,13 @@
|
|||||||
<FpFilterChip label="'FAIR'"
|
<FpFilterChip label="'FAIR'"
|
||||||
active="!!state.filters.fair"
|
active="!!state.filters.fair"
|
||||||
onToggle="() => this.toggleFilter('fair')"/>
|
onToggle="() => this.toggleFilter('fair')"/>
|
||||||
|
<!-- Spec 2026-05-25 — post-shop state chips -->
|
||||||
|
<FpFilterChip label="'Awaiting CoC'"
|
||||||
|
active="!!state.filters.awaiting_cert"
|
||||||
|
onToggle="() => this.toggleFilter('awaiting_cert')"/>
|
||||||
|
<FpFilterChip label="'Ready to Ship'"
|
||||||
|
active="!!state.filters.awaiting_ship"
|
||||||
|
onToggle="() => this.toggleFilter('awaiting_ship')"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user