docs(brainstorm): post-shop cert + shipping job states spec
Trigger: WO-30058 (SO-30058) finished all recipe steps on entech and disappeared from the Shop Floor kanban with a draft CoC and no QM notification. Operators reported jobs feeling lost; risk that a job could leave the building without paperwork. Design (Approach A, approved): - Two new fp.job.state values between in_progress and done: awaiting_cert + awaiting_ship - Auto-advance on last step finish; auto-advance on cert issue - Plant kanban widens domain, renders the two states in the existing Final inspection / Shipping columns - 6th tab 'Certificates' on Quality Dashboard with kanban + filters - ACL gate on fp.certificate.action_issue restricted to Manager / QM / Owner (transitive via all_group_ids) - Email + mail.activity notification to QM authority group - Migration script backfills mid-flight jobs Shipping label printing, BoL, carrier dispatch are explicitly out of scope; awaiting_ship is a parking column with a manual Mark Shipped button. Self-review pass found and fixed: - round-robin field ambiguity (last_activity_at vs login_date) - unstated behavior for button_mark_done gates (now in step.finish) - placeholder version inlined (19.0.11.0.0) - dead reference replaced with inline body Spec: docs/superpowers/specs/2026-05-25-post-shop-cert-shipping-job-states-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,610 @@
|
||||
# Post-Shop Cert + Shipping Job States
|
||||
|
||||
**Date:** 2026-05-25
|
||||
**Status:** Approved for implementation (brainstorming gate)
|
||||
**Author:** Brainstorming session (gsinghpal)
|
||||
**Triggering incident:** Job WO-30058 (SO-30058) finished all recipe steps on entech and **disappeared from the Shop Floor kanban**. CoC was auto-spawned in `draft` but nobody was notified, no surface listed pending certs for the Quality Manager, and there was no kanban column for "completed-but-not-shipped" jobs. Operators reported jobs they had finished feeling "lost" — same risk that a job could leave the building without a CoC.
|
||||
|
||||
## Goal
|
||||
|
||||
Three things, decided as one unit of work:
|
||||
|
||||
1. **Stop completed-but-uncertified jobs from disappearing** from the Plant Kanban — they must stay visible to shop staff so jobs aren't forgotten or shipped without paperwork.
|
||||
2. **Give the Quality Manager a dedicated surface** (Quality Dashboard tab + email + in-app activity) for CoC issuance, with hard ACL gating so Technicians cannot self-issue.
|
||||
3. **Model the actual lifecycle** in `fp.job.state` so reporting, queries, and future workflow gates derive from a single source of truth instead of cross-module joins.
|
||||
|
||||
## Out of scope (deferred to follow-on work)
|
||||
|
||||
- **Shipping label printing, carrier dispatch, tracking-number capture, BoL generation.** These remain manual for now; the new `awaiting_ship` state is a parking column so jobs are visible to the shipping crew. `awaiting_ship → done` is a manual button click. (User: *"lets first finish this certification step then we will look into shipping"*.)
|
||||
- **Auto-transition from `awaiting_ship → done` on `delivery.action_mark_delivered`.** Scaffolded as a future hook in the design; not wired in this scope.
|
||||
- **RMA-aware regression** (job re-opening on RMA receive). Already handled by existing RMA flow — not touched here.
|
||||
- **Per-cert-type ACL** (e.g. only QM can issue Nadcap, but Manager can issue CoC). Out of scope; single QM-or-higher gate for all cert types in v1.
|
||||
|
||||
## Decisions reached during brainstorming
|
||||
|
||||
| # | Decision | Rationale |
|
||||
|---|---|---|
|
||||
| D1 | Use **Approach A** — add two new `fp.job.state` values (`awaiting_cert`, `awaiting_ship`) | Cleanest semantics; `state='done'` will once again mean "fully complete and shipped". Single source of truth for kanban, dashboard, reporting, and future delivery automation. |
|
||||
| D2 | **Auto-advance** `in_progress → awaiting_cert` when every recipe step is terminal AND a cert is required | No new operator button; removes risk of forgetting to advance. Hooks into existing `all_steps_terminal` computed field. |
|
||||
| D3 | **Auto-advance** `awaiting_cert → awaiting_ship` from `fp.certificate.action_issue` when every required cert is `issued` | The QM clicking Issue is the natural trigger; no separate "mark inspection complete" button needed. The recipe's final-inspection step already captures inspection data via custom prompts. |
|
||||
| D4 | **Cert void regresses state** (`awaiting_ship → awaiting_cert` if a previously-issued cert is voided) | Defensive against late-discovered issues. Re-fires `cert_awaiting_issuance` notification under a `cert_voided_re_notify` event so dedupe doesn't suppress it. |
|
||||
| D5 | **Manual `awaiting_ship → done` button** in this scope; auto-hook on delivery deferred | User explicitly scoped shipping work out. The button is repurposed from the existing milestone-advance "Mark Done" button (renamed "Mark Shipped"), restricted to Manager/Owner. |
|
||||
| D6 | **ACL gate on `action_issue`** — Manager / Quality Manager / Owner only | User: *"certifications can only be issued by Manager, Quality Manager and Owner, i don't want technicians to issue certifications"*. Two-layer enforcement: Python `AccessError` + view-level `groups=` on the Issue button. |
|
||||
| D7 | **Group-membership resolved via `all_group_ids`** (transitive) — Owners reach QM authority via implication chain | Rule 23 (CLAUDE.md). Owners don't carry `group_fp_quality_manager` directly; the `all_group_ids` lookup catches them via implication. |
|
||||
| D8 | **Notification fires on the auto-transition**, not on the operator's last step-finish click | Decouples notification from operator UX; transition is the authoritative trigger so all paths (UI, RPC, scripted) notify consistently. |
|
||||
| D9 | **Belt-and-suspenders: in-app `mail.activity`** in addition to email | If email bounces / spam-folders, the red activity badge on the QM's home is the floor-of-truth signal. Activity auto-resolves on `awaiting_ship` transition. |
|
||||
| D10 | **Jobs that don't require certs skip `awaiting_cert`** and land directly in `awaiting_ship` | Visibility consistency — every completed-but-unshipped job is in the Shipping column, regardless of cert requirements. No silent path to `done`. |
|
||||
| D11 | **Plant kanban shows the two new states in `Final inspection` and `Shipping` columns**; old per-step `area_kind` logic untouched for `in_progress` | Repurposes the two right-most columns that are currently almost always empty. State drives column for the new values; nothing else changes. |
|
||||
| D12 | **Gates from `button_mark_done` (bake/qty/QC) move UP into `fp.job.step.button_finish` on the LAST step** instead of running at auto-transition time | The auto-transition itself must not raise — if it raised when the operator finishes their last step, the step would finish but the auto-advance would silently fail with no error path. Better: when finishing the last step, surface the pending gates as UserError on the finish click. Operator fixes (qty, bake, QC), retries finish, transition fires cleanly. Step-completion gate is implied (the step IS finishing). |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─ STATE MACHINE ─────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ confirmed → in_progress → awaiting_cert → awaiting_ship → done│
|
||||
│ │ ▲ ▲ ▲ │
|
||||
│ │ │ │ │ │
|
||||
│ all steps terminal QM Issue (last cert) Mark Shipped│ │
|
||||
│ + cert required button │ │
|
||||
│ │ │ │
|
||||
│ └──── (no cert required) ─────────────────┘ │
|
||||
│ │
|
||||
│ awaiting_ship ──(cert voided after issue)──► awaiting_cert
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ KANBAN VISIBILITY (PLANT VIEW) ────────────────────────────────┐
|
||||
│ │
|
||||
│ Receiving · Masking · Blasting · Racking · Plating │
|
||||
│ · Baking · De-Racking · ★Final inspection · ★Shipping │
|
||||
│ ▲ ▲ │
|
||||
│ │ │ │
|
||||
│ awaiting_cert awaiting_ship │
|
||||
│ (column = state, not active_step) │
|
||||
│ │
|
||||
│ Domain widens: state IN (confirmed, in_progress, │
|
||||
│ awaiting_cert, awaiting_ship) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ QUALITY DASHBOARD ─────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Holds | Checks | NCRs | CAPAs | RMAs | ★Certificates │
|
||||
│ (new 6th tab) │
|
||||
│ │
|
||||
│ Tab content: kanban grouped by state, Draft folded open, │
|
||||
│ filters (My Customer / Today / Overdue >24h / │
|
||||
│ Missing Fischerscope), buttons Open Cert / Open Job. │
|
||||
│ │
|
||||
│ Header KPI: "Certificates Awaiting Issuance: N (M overdue)" │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ NOTIFICATION (when state hits awaiting_cert) ──────────────────┐
|
||||
│ │
|
||||
│ Email via fp.notification.template, event: │
|
||||
│ cert_awaiting_issuance (first notification) │
|
||||
│ cert_voided_re_notify (re-fires after cert void) │
|
||||
│ │
|
||||
│ Recipients: every active non-share user with all_group_ids │
|
||||
│ containing QM | Manager | Owner. Resolved by helper │
|
||||
│ _fp_resolve_cert_authority_users() (see Rule 13l pattern). │
|
||||
│ │
|
||||
│ + mail.activity ("To Do") assigned to one QM, round-robin │
|
||||
│ by last_activity_at. Auto-marks done on awaiting_ship. │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Schema changes (additive)
|
||||
|
||||
### `fp.job` model
|
||||
|
||||
| Change | Type | Notes |
|
||||
|---|---|---|
|
||||
| `state` selection — add `('awaiting_cert', 'Awaiting Cert')` and `('awaiting_ship', 'Awaiting Ship')` | Selection extension | Selection extension via `_inherit` keeps tracking + chatter audit working. Sequence: confirmed → in_progress → awaiting_cert → awaiting_ship → done → cancelled. |
|
||||
| New method `_fp_check_advance_post_shop()` | Method | Called from step-state-change hooks (specifically from `fp.job.step.button_finish` after `super()`). If all `step_ids` are in `('done','skipped','cancelled')` AND state is `in_progress`: transitions to `awaiting_cert` (if `_resolve_required_cert_types()` non-empty) or `awaiting_ship` (if empty). Idempotent. Does NOT raise — gates moved into step.button_finish per D12. |
|
||||
| Hardened `fp.job.step.button_finish` for the LAST open step | Method | When finishing a step that would leave all steps terminal, run the bake-window / qty-reconciliation / QC gates from the old `button_mark_done` BEFORE allowing the finish. On failure: raise UserError, step stays open, operator fixes + retries. Same manager-bypass context flags as today (`fp_skip_bake_gate`, `fp_skip_qty_reconcile`, `fp_skip_qc_gate`). |
|
||||
| New method `_fp_check_advance_after_cert_issue()` | Method | Called from `fp.certificate.action_issue`. If every required cert for the job is `issued`: transition `awaiting_cert → awaiting_ship`. Idempotent. |
|
||||
| New method `_fp_check_regress_after_cert_void()` | Method | Called from `fp.certificate.write({'state':'voided'})`. If any required cert is no longer `issued`: transition `awaiting_ship → awaiting_cert` and re-fire notification under `cert_voided_re_notify`. |
|
||||
| New method `button_mark_shipped()` | Method | Manual transition `awaiting_ship → done`. Restricted via `groups=` on the form button (Manager/Owner). Reuses the existing `button_mark_done` body for side effects (delivery wiring, notifications, chatter), but **does not include the step/QC/bake/qty gates** (those already passed when we transitioned to `awaiting_cert`). |
|
||||
| Repurpose existing `button_mark_done()` | Method | Becomes an internal-only method called from the auto-transitions. Visible operator action is now `button_mark_shipped`. Existing callers remain valid but are now triggered by the state machine rather than direct user clicks. |
|
||||
|
||||
### `fp.certificate` model
|
||||
|
||||
| Change | Type | Notes |
|
||||
|---|---|---|
|
||||
| Hardened `action_issue()` | Method | Adds Python-side `AccessError` if user lacks QM/Manager/Owner. Calls `job._fp_check_advance_after_cert_issue()` after successful issue. Manager bypass via context flag `fp_skip_cert_authority_gate=True` (posts chatter audit). |
|
||||
| `write({'state':'voided'})` override | Method | Calls `job._fp_check_regress_after_cert_void()` after the void completes. |
|
||||
| `x_fc_age_hours` | Float, non-stored, computed | Drives the Quality Dashboard age chip + overdue filter. `(now - create_date).total_seconds() / 3600`. |
|
||||
|
||||
### `fp.notification.template` data
|
||||
|
||||
| Change | Notes |
|
||||
|---|---|
|
||||
| New selection values on `trigger_event`: `('cert_awaiting_issuance', 'Cert Awaiting Issuance')`, `('cert_voided_re_notify', 'Cert Voided — Please Re-Issue')` | Loaded via `data/fp_notification_events_data.xml`. |
|
||||
| Two seeded `fp.notification.template` records (one per event) | `data/fp_cert_authority_templates.xml`. Default body shown under "Notification changes" further below. Editable via UI (Plating → Configuration → Quality & Documents → Notification Templates). |
|
||||
| New recipient resolver helper `_fp_resolve_cert_authority_users(job)` on `fp.notification.template` | Wraps the group-membership search (Rule 13l pattern). Dispatched when `trigger_event` is `cert_awaiting_issuance` or `cert_voided_re_notify`. |
|
||||
|
||||
### `mail.activity` integration
|
||||
|
||||
| Change | Notes |
|
||||
|---|---|
|
||||
| New `mail.activity.type` xmlid `fusion_plating_jobs.activity_type_issue_coc` | Title: "Issue CoC". Default summary template: `Issue CoC for {{ job.display_wo_name }}`. |
|
||||
| `_fp_schedule_cert_activity(job)` helper on `fp.job` | Picks one QM from `_fp_resolve_cert_authority_users`, sorted by `login_date asc nulls first` — the QM who logged in least recently (likely least busy / hasn't been on the system in a while). Creates the activity. Auto-resolves on `awaiting_ship` transition. `login_date` chosen over a custom `last_activity_at` field because it's standard on `res.users` and always populated. |
|
||||
|
||||
## Plant Kanban changes
|
||||
|
||||
### Controller — `fusion_plating_shopfloor/controllers/plant_kanban.py`
|
||||
|
||||
```python
|
||||
# Line 73-75 — widen the domain
|
||||
domain = [
|
||||
('state', 'in', ('confirmed', 'in_progress',
|
||||
'awaiting_cert', 'awaiting_ship')),
|
||||
]
|
||||
|
||||
# Line ~165 — extend _resolve_card_area
|
||||
def _resolve_card_area(job):
|
||||
if job.card_state == 'no_parts':
|
||||
return 'receiving'
|
||||
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
|
||||
return 'receiving' # orphan fallback (unchanged)
|
||||
```
|
||||
|
||||
### Card-state catalog — `fusion_plating_jobs/models/fp_job.py`
|
||||
|
||||
Two new values added to the `_compute_card_state` precedence chain. Inserted BEFORE the existing `done` rule (which is now unreachable from `state='done'` jobs anyway because they're filtered off the board):
|
||||
|
||||
```python
|
||||
# Before existing Rule 8 (done):
|
||||
if job.state == 'awaiting_cert':
|
||||
job.card_state = 'awaiting_cert'
|
||||
continue
|
||||
if job.state == 'awaiting_ship':
|
||||
job.card_state = 'awaiting_ship'
|
||||
continue
|
||||
```
|
||||
|
||||
### Chip rendering — `_state_chip()` in plant_kanban.py
|
||||
|
||||
```python
|
||||
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'}
|
||||
```
|
||||
|
||||
### Sort priority — `_SORT_PRIORITY` in plant_kanban.py
|
||||
|
||||
```python
|
||||
'awaiting_cert': 3.5, # right after awaiting_signoff
|
||||
'awaiting_ship': 8.5, # right after running
|
||||
```
|
||||
|
||||
(Floats are fine — `_sort_key` returns a tuple sorted ascending, no integer assumption.)
|
||||
|
||||
### SCSS — `fusion_plating_shopfloor/static/src/scss/_plant_card.scss`
|
||||
|
||||
Add two new state modifier classes following the existing pattern:
|
||||
|
||||
```scss
|
||||
.o_fp_plant_card.state-awaiting_cert {
|
||||
background-color: var(--fp-state-awaiting-cert-bg, #fff3cd);
|
||||
border-left: 4px solid var(--fp-state-awaiting-cert-border, #ff9800);
|
||||
}
|
||||
.o_fp_plant_card.state-awaiting_ship {
|
||||
background-color: var(--fp-state-awaiting-ship-bg, #d1f1d4);
|
||||
border-left: 4px solid var(--fp-state-awaiting-ship-border, #2e7d32);
|
||||
}
|
||||
```
|
||||
|
||||
Tokens go in `_plant_tokens.scss` first (per Rule 8), with `@if $o-webclient-color-scheme == dark` darker variants (per Rule 9).
|
||||
|
||||
### KPI strip + filter chips — `fusion_plating_shopfloor/static/src/js/plant_kanban.js`
|
||||
|
||||
```javascript
|
||||
// Two new KPI tiles
|
||||
{ key: 'awaiting_cert', label: _t('Awaiting CoC'),
|
||||
count: kpis.awaiting_cert, kind: 'awaiting_cert' },
|
||||
{ key: 'awaiting_ship', label: _t('Ready to Ship'),
|
||||
count: kpis.awaiting_ship, kind: 'awaiting_ship' },
|
||||
|
||||
// Two new filter chip
|
||||
{ key: 'awaiting_cert', label: _t('Awaiting CoC') },
|
||||
{ key: 'awaiting_ship', label: _t('Ready to Ship') },
|
||||
```
|
||||
|
||||
Server-side KPI compute (in `plant_kanban` endpoint):
|
||||
|
||||
```python
|
||||
'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'),
|
||||
```
|
||||
|
||||
### Mini-timeline strip — `_compute_mini_timeline_json` in fp_job.py
|
||||
|
||||
When `state='awaiting_cert'`: the `inspection` dot renders as `current` with `variant='awaiting_cert'`; all 7 earlier dots render `done`; shipping dot renders `upcoming`. Same shape when `state='awaiting_ship'` — shipping is `current` with `variant='awaiting_ship'`, inspection is `done`. Lets the QM see at a glance "this card has cleared the whole line, just waiting on paperwork/shipping."
|
||||
|
||||
## Quality Dashboard changes
|
||||
|
||||
### Counts endpoint — `fusion_plating_quality/controllers/fp_quality_dashboard.py`
|
||||
|
||||
Extend the existing `/fp/quality/dashboard/counts` response with one block:
|
||||
|
||||
```python
|
||||
Cert = env['fp.certificate']
|
||||
return {
|
||||
# existing blocks (holds, checks, ncrs, capas, rmas) unchanged
|
||||
'certificates': {
|
||||
'open': Cert.search_count([('state', '=', 'draft')]),
|
||||
'overdue': Cert.search_count([
|
||||
('state', '=', 'draft'),
|
||||
('create_date', '<', d1), # >24h = overdue
|
||||
]),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Tab UI — `fusion_plating_quality/static/src/{js,xml,scss}/fp_quality_dashboard.*`
|
||||
|
||||
Sixth tab "Certificates" with:
|
||||
|
||||
- **Kanban view** grouped by `state` (Draft / Issued / Voided), Draft folded open by default.
|
||||
- **Card content** (per cert): WO# + part# (large), customer name, cert-type chip (CoC / CoC+Thickness / Nadcap), age chip (`Created 4h ago`, red past 24h), status badges (🟢 Thickness PDF ready · 📋 Inspection prompts captured · ⚠ Missing spec ref).
|
||||
- **Two card actions**: `Open Cert` (cert form) · `Open Job` (source job, lets QM audit inspection prompts before issuing).
|
||||
- **Filters above kanban**: My Customer / Today / Overdue (>24h) / Missing Fischerscope (status='pending' from S19) / High-severity Customer.
|
||||
- **Group-by option**: Customer.
|
||||
|
||||
**Header KPI** card on the dashboard gains: `Certificates Awaiting Issuance: <count>` with overdue sub-count in red.
|
||||
|
||||
**Header KPI strip** across all 6 tabs stays glanceable — six small tiles in a row.
|
||||
|
||||
### Menu entry
|
||||
|
||||
No new menu — the Quality Dashboard already lives at Plating → Quality → Dashboard. New tab is a sibling render inside the existing client action.
|
||||
|
||||
### Deep-link from notification email
|
||||
|
||||
The email body includes `{{ url('/odoo/action-fp_quality_dashboard?tab=certificates') }}`. New URL param `?tab=certificates` is parsed by the OWL action's `setup()` to focus the right tab on load.
|
||||
|
||||
## ACL changes
|
||||
|
||||
### `fp.certificate.action_issue` Python guard
|
||||
|
||||
```python
|
||||
def action_issue(self):
|
||||
if not self.env.context.get('fp_skip_cert_authority_gate'):
|
||||
cert_authority_gids = [
|
||||
self.env.ref('fusion_plating.group_fp_quality_manager').id,
|
||||
self.env.ref('fusion_plating.group_fp_manager').id,
|
||||
self.env.ref('fusion_plating.group_fp_owner').id,
|
||||
]
|
||||
if not (set(self.env.user.all_group_ids.ids)
|
||||
& set(cert_authority_gids)):
|
||||
raise AccessError(_(
|
||||
"Only Quality Managers, Managers, and Owners can issue "
|
||||
"certificates. Ask your QM to review and issue this CoC."
|
||||
))
|
||||
else:
|
||||
self.message_post(body=Markup(_(
|
||||
'Cert authority gate <b>bypassed</b> by '
|
||||
'<b>%(u)s</b> (context flag fp_skip_cert_authority_gate).'
|
||||
)) % {'u': self.env.user.name})
|
||||
# existing issue logic...
|
||||
```
|
||||
|
||||
### View-level button gating
|
||||
|
||||
`fp_certificate_views.xml` — Issue button on form:
|
||||
|
||||
```xml
|
||||
<button name="action_issue" string="Issue" type="object"
|
||||
invisible="state != 'draft'"
|
||||
groups="fusion_plating.group_fp_quality_manager,
|
||||
fusion_plating.group_fp_manager,
|
||||
fusion_plating.group_fp_owner"/>
|
||||
```
|
||||
|
||||
### Tablet impact
|
||||
|
||||
S18-era cert flow ran any operator session. Post-change: remove the Issue affordance from operator-facing tablet surfaces. The cert form is still openable (read-only for Technicians); the action button is hidden. Surface a "Send to QM" hint on the job workspace footer when state is `awaiting_cert`, but the actual notification fires automatically — no operator click required.
|
||||
|
||||
## Notification changes
|
||||
|
||||
### New trigger events
|
||||
|
||||
Add to `fp.notification.template.trigger_event` selection:
|
||||
|
||||
```python
|
||||
('cert_awaiting_issuance', _('Cert Awaiting Issuance')),
|
||||
('cert_voided_re_notify', _('Cert Voided — Please Re-Issue')),
|
||||
```
|
||||
|
||||
### Seeded template — `data/fp_cert_authority_templates.xml`
|
||||
|
||||
Single template per event (subject, body, recipient_resolver = `cert_authority_group_members`). Default body (per the Architecture diagram above and the section that follows on the recipient resolver):
|
||||
|
||||
```
|
||||
Subject: 🏷️ Job {{ object.display_wo_name }} ready for CoC issuance
|
||||
|
||||
Hi {{ recipient.name|first_name }},
|
||||
|
||||
Job {{ object.display_wo_name }} ({{ object.partner_id.name }})
|
||||
has finished the shop floor and is awaiting CoC issuance.
|
||||
|
||||
Part: {{ object.part_catalog_id.part_number }}
|
||||
Quantity: {{ object.qty_done }}
|
||||
Recipe: {{ object.recipe_id.name }}
|
||||
|
||||
Review the inspection prompts captured by the operator on the Final
|
||||
Inspection step, then issue the CoC from the Quality Dashboard:
|
||||
|
||||
→ {{ url('/odoo/action-fp_quality_dashboard?tab=certificates') }}
|
||||
|
||||
Or open the job directly:
|
||||
|
||||
→ {{ object.x_fc_record_url }}
|
||||
```
|
||||
|
||||
Marked `noupdate="1"` so admin edits in the UI survive `-u` (Rule 22).
|
||||
|
||||
### Recipient resolver — `_fp_resolve_cert_authority_users()`
|
||||
|
||||
```python
|
||||
@api.model
|
||||
def _fp_resolve_cert_authority_users(self, job=None):
|
||||
"""Return active, non-share users with QM | Manager | Owner role
|
||||
(transitive via all_group_ids). See Rule 13l for the rationale —
|
||||
direct user_ids on group records does NOT include implied
|
||||
memberships."""
|
||||
gids = [
|
||||
self.env.ref('fusion_plating.group_fp_quality_manager').id,
|
||||
self.env.ref('fusion_plating.group_fp_manager').id,
|
||||
self.env.ref('fusion_plating.group_fp_owner').id,
|
||||
]
|
||||
return self.env['res.users'].sudo().search([
|
||||
('all_group_ids', 'in', gids),
|
||||
('share', '=', False),
|
||||
('active', '=', True),
|
||||
])
|
||||
```
|
||||
|
||||
### Throttling / dedupe
|
||||
|
||||
`fp.notification.log` dedupe key is `(template_id, source_record_id, event)`. The two events have distinct keys, so a `cert_voided_re_notify` after `cert_awaiting_issuance` re-fires (correct — different signal). Repeated `cert_awaiting_issuance` for the same job is suppressed (correct — already on the QM's radar).
|
||||
|
||||
### `mail.activity` belt + suspenders
|
||||
|
||||
After firing the notification, schedule one activity per job (not per QM — round-robin assignment to a single QM, who can re-assign if needed). Auto-resolves when state transitions to `awaiting_ship`.
|
||||
|
||||
```python
|
||||
def _fp_schedule_cert_activity(self):
|
||||
self.ensure_one()
|
||||
activity_type = self.env.ref(
|
||||
'fusion_plating_jobs.activity_type_issue_coc',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if not activity_type:
|
||||
return
|
||||
qm = self.env['fp.notification.template']._fp_resolve_cert_authority_users(self)
|
||||
if not qm:
|
||||
return
|
||||
# Round-robin: pick the QM who logged in least recently (likely
|
||||
# least busy). NULL login_date sorts first.
|
||||
qm = qm.sorted(lambda u: u.login_date or fields.Datetime.from_string('1970-01-01'))[:1]
|
||||
self.activity_schedule(
|
||||
activity_type_id=activity_type.id,
|
||||
user_id=qm.id,
|
||||
summary=_('Issue CoC for %s') % (self.display_wo_name or self.name),
|
||||
note=_('Job has finished the shop floor. Review the inspection '
|
||||
'prompts captured on the final step, then issue the CoC.'),
|
||||
)
|
||||
```
|
||||
|
||||
Auto-resolve on `awaiting_ship` transition:
|
||||
|
||||
```python
|
||||
def _fp_resolve_cert_activities(self):
|
||||
self.ensure_one()
|
||||
activity_type = self.env.ref(
|
||||
'fusion_plating_jobs.activity_type_issue_coc',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if not activity_type:
|
||||
return
|
||||
self.activity_ids.filtered(
|
||||
lambda a: a.activity_type_id == activity_type
|
||||
).action_feedback(feedback=_('Cert issued — auto-resolved.'))
|
||||
```
|
||||
|
||||
## Migration plan
|
||||
|
||||
### Pre-migration state assessment
|
||||
|
||||
```sql
|
||||
-- Count jobs currently affected
|
||||
SELECT state, count(*)
|
||||
FROM fp_job
|
||||
WHERE state IN ('confirmed', 'in_progress', 'done')
|
||||
GROUP BY state;
|
||||
|
||||
-- Jobs in-progress with all steps terminal (will migrate to awaiting_cert or awaiting_ship)
|
||||
SELECT j.id, j.name, j.state,
|
||||
count(s.id) AS total_steps,
|
||||
count(s.id) FILTER (WHERE s.state IN ('done','skipped','cancelled')) AS terminal_steps
|
||||
FROM fp_job j
|
||||
LEFT JOIN fp_job_step s ON s.job_id = j.id
|
||||
WHERE j.state = 'in_progress'
|
||||
GROUP BY j.id, j.name, j.state
|
||||
HAVING count(s.id) > 0
|
||||
AND count(s.id) = count(s.id) FILTER (WHERE s.state IN ('done','skipped','cancelled'));
|
||||
|
||||
-- Done jobs with draft certs (would need backfill in the new world, but we leave them alone)
|
||||
SELECT j.id, j.name, count(c.id) AS draft_certs
|
||||
FROM fp_job j
|
||||
JOIN fp_certificate c ON c.x_fc_job_id = j.id AND c.state = 'draft'
|
||||
WHERE j.state = 'done'
|
||||
GROUP BY j.id, j.name;
|
||||
```
|
||||
|
||||
### Migration script — `fusion_plating_jobs/migrations/19.0.11.0.0/post-migrate.py`
|
||||
|
||||
```python
|
||||
def migrate(cr, version):
|
||||
"""Backfill new states for jobs caught mid-transition by the upgrade.
|
||||
|
||||
Rules:
|
||||
- in_progress + all steps terminal + draft cert exists → awaiting_cert
|
||||
- in_progress + all steps terminal + no cert required → awaiting_ship
|
||||
- done jobs LEFT ALONE — they're historical (already shipped)
|
||||
"""
|
||||
cr.execute("""
|
||||
UPDATE fp_job
|
||||
SET state = 'awaiting_cert'
|
||||
WHERE id IN (
|
||||
SELECT j.id
|
||||
FROM fp_job j
|
||||
JOIN fp_job_step s ON s.job_id = j.id
|
||||
WHERE j.state = 'in_progress'
|
||||
GROUP BY j.id
|
||||
HAVING count(*) FILTER (
|
||||
WHERE s.state NOT IN ('done','skipped','cancelled')
|
||||
) = 0
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM fp_certificate c
|
||||
WHERE c.x_fc_job_id = fp_job.id AND c.state = 'draft'
|
||||
);
|
||||
""")
|
||||
cr.execute("""
|
||||
UPDATE fp_job
|
||||
SET state = 'awaiting_ship'
|
||||
WHERE id IN (
|
||||
SELECT j.id
|
||||
FROM fp_job j
|
||||
JOIN fp_job_step s ON s.job_id = j.id
|
||||
WHERE j.state = 'in_progress'
|
||||
GROUP BY j.id
|
||||
HAVING count(*) FILTER (
|
||||
WHERE s.state NOT IN ('done','skipped','cancelled')
|
||||
) = 0
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fp_certificate c
|
||||
WHERE c.x_fc_job_id = fp_job.id
|
||||
AND c.state IN ('draft', 'issued')
|
||||
);
|
||||
""")
|
||||
```
|
||||
|
||||
Idempotent: re-running on a fresh upgrade is a no-op because no `in_progress` job will match the all-terminal predicate after the first run.
|
||||
|
||||
### Card_state recompute
|
||||
|
||||
After the migration script runs, force a recompute of `fp.job.card_state` (stored compute) so the kanban renders correctly:
|
||||
|
||||
```python
|
||||
# In post-migrate.py, after the state UPDATEs:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
affected = env['fp.job'].search([
|
||||
('state', 'in', ('awaiting_cert', 'awaiting_ship')),
|
||||
])
|
||||
affected.invalidate_recordset(['card_state'])
|
||||
affected._compute_card_state()
|
||||
```
|
||||
|
||||
## Edge cases + defensive design
|
||||
|
||||
| Case | Behavior |
|
||||
|---|---|
|
||||
| Job with no recipe steps at all | `_fp_check_advance_post_shop` returns early (nothing to check). State stays at whatever it was — operator can still manually move it via the existing milestone-advance button. |
|
||||
| Recipe doesn't have a Final Inspection step | Card still lands in Final Inspection column (state drives column, not recipe shape). The recipe author probably forgot to add the step — Quality Dashboard surface catches it. |
|
||||
| Customer not flagged for any cert | `_resolve_required_cert_types()` returns empty set → state transitions `in_progress → awaiting_ship` directly. Card visible in Shipping column. No notification fires (no QM action needed). |
|
||||
| QM voids a cert AFTER `awaiting_ship` | `_fp_check_regress_after_cert_void()` flips state back to `awaiting_cert`, re-fires `cert_voided_re_notify` (different dedupe key from initial notification, so not suppressed). Card visibly moves left from Shipping back to Final Inspection. |
|
||||
| Operator marks a step as `cancelled` mid-flow that bricks the all-terminal check | `cancelled` IS terminal per the existing logic — so cancelling the last open step DOES trigger the transition. Operator-error path: if a manager opens a previously-terminal step (e.g. `_fp_reopen_step`) the state should regress to `in_progress`. Add inverse trigger on step reopen. |
|
||||
| Cron-triggered cert issuance (e.g. some future auto-issue) with no `env.user` | The Python guard hits `self.env.user` which would be the cron user. Need cron-safe bypass: use `fp_skip_cert_authority_gate=True` in the cron's `with_context(...)` call. Documented but no cron exists today. |
|
||||
| Migration: existing `state='done'` jobs that haven't shipped | Left alone — historically completed jobs are out of scope. Backfill action (`action_backfill_missing_certs` in fp_job.py) already exists for cert gaps; that path is unchanged. |
|
||||
| Multiple QMs assigned a single activity | Round-robin picks ONE QM (oldest `login_date`). They can reassign via the activity widget if needed. Activity auto-resolves on `awaiting_ship` regardless of which QM acted. |
|
||||
|
||||
## Testing strategy
|
||||
|
||||
### Smoke test (entech-style, scriptable)
|
||||
|
||||
```python
|
||||
# scripts/bt_post_shop_states.py
|
||||
# 1. Create SO + job with cert-requiring customer
|
||||
# 2. Walk every step to terminal → assert state='awaiting_cert'
|
||||
# 3. Assert card appears in plant_kanban under 'inspection' column
|
||||
# 4. Assert email + activity scheduled on a QM
|
||||
# 5. As a Technician, call cert.action_issue() → assert AccessError
|
||||
# 6. As a QM, call cert.action_issue() → state='issued', job state→'awaiting_ship'
|
||||
# 7. Assert card moves to 'shipping' column, activity auto-resolves
|
||||
# 8. Void the cert → assert state back to 'awaiting_cert', activity re-scheduled
|
||||
# 9. Re-issue → 'awaiting_ship' again
|
||||
# 10. Click button_mark_shipped (as Manager) → state='done', card off board
|
||||
```
|
||||
|
||||
### Unit-level
|
||||
|
||||
- `fp.job._fp_check_advance_post_shop` — exhaustive matrix of (state, all_terminal, certs_required) → expected new state
|
||||
- `fp.certificate.action_issue` ACL — separate tests per role (Tech/Mgr/QM/Owner/Sales Rep)
|
||||
- `_fp_resolve_cert_authority_users` — entech-realistic group setup, confirm Owners are returned via implication
|
||||
|
||||
### Manual QA on entech
|
||||
|
||||
1. Pick a currently-done WO-30058 (the triggering job) → run migration → confirm it stays at `state='done'` (untouched).
|
||||
2. Find an `in_progress` job with all steps terminal — confirm migration script moves it to the right state.
|
||||
3. Walk a fresh SO end-to-end: confirm card visibility at each column transition.
|
||||
4. Try issuing a cert as a Technician via the JS-rpc console → confirm AccessError.
|
||||
|
||||
## Files touched
|
||||
|
||||
### `fusion_plating_jobs/`
|
||||
- `models/fp_job.py` — state extension, new methods, repurpose button_mark_done, new button_mark_shipped, hooks for cert state changes, mini-timeline update, card-state extension
|
||||
- `views/fp_job_views.xml` — Mark Shipped button (replaces Mark Done), groups gating
|
||||
- `data/fp_activity_types_data.xml` — new mail.activity.type `activity_type_issue_coc`
|
||||
- `migrations/19.0.<next>/post-migrate.py` — state backfill
|
||||
- `__manifest__.py` — version bump, data file additions
|
||||
|
||||
### `fusion_plating_certificates/`
|
||||
- `models/fp_certificate.py` — Python ACL guard in action_issue, write override for state=voided, x_fc_age_hours computed
|
||||
- `views/fp_certificate_views.xml` — `groups=` on Issue button
|
||||
- `__manifest__.py` — version bump
|
||||
|
||||
### `fusion_plating_shopfloor/`
|
||||
- `controllers/plant_kanban.py` — domain widen, column resolution, chip rendering, KPI compute, sort priority
|
||||
- `static/src/js/plant_kanban.js` — KPI tile + filter chip additions
|
||||
- `static/src/scss/_plant_card.scss` + `_plant_tokens.scss` — new state modifier classes (light + dark via `$o-webclient-color-scheme`)
|
||||
- `__manifest__.py` — version bump
|
||||
|
||||
### `fusion_plating_quality/`
|
||||
- `controllers/fp_quality_dashboard.py` — certificates block in counts response
|
||||
- `static/src/{js,xml,scss}/fp_quality_dashboard.*` — sixth tab, header KPI, deep-link `?tab=certificates` parsing
|
||||
- `__manifest__.py` — version bump (and add `fusion_plating_certificates` to depends if not already)
|
||||
|
||||
### `fusion_plating_notifications/`
|
||||
- `models/fp_notification_template.py` — new trigger_event selection values, recipient resolver helper
|
||||
- `data/fp_cert_authority_templates.xml` — seeded templates for both events
|
||||
- `__manifest__.py` — version bump
|
||||
|
||||
## Open questions for implementation phase
|
||||
|
||||
1. **Where exactly does the auto-transition fire?** Most likely `fp.job.step.write` post-hook when `state` changes — needs centralization so all step-completion paths (button_finish, action_skip, action_cancel, etc.) trigger consistently. Implementation plan should validate by grepping for every site that sets `step.state`.
|
||||
2. **Should `button_mark_shipped` add ANY gates** (e.g. delivery exists / draft cert isn't lingering)? Default answer: no — the state machine has already validated correctness; the button is just "yes, shipped". But worth re-confirming.
|
||||
3. **Tablet "Send to QM" footer hint** — exact wording and link target. Minor UX; can be settled during implementation.
|
||||
4. **Mini-timeline dot for `done` (state at end-of-lifecycle)** — currently the timeline is always 9 dots regardless of state. After shipping the card is off the board, so this only matters for historical viewers. Probably no change needed; flag for implementation review.
|
||||
|
||||
## Status & deployment notes
|
||||
|
||||
Target version bumps (suggestion, finalize at implementation time):
|
||||
- `fusion_plating_jobs` 19.0.10.24.0 → 19.0.11.0.0 (state extension is a minor-version bump per existing convention)
|
||||
- `fusion_plating_certificates` 19.0.5.4.0 → 19.0.6.0.0
|
||||
- `fusion_plating_shopfloor` 19.0.34.0.0 → 19.0.35.0.0
|
||||
- `fusion_plating_quality` 19.0.5.0.0 → 19.0.6.0.0
|
||||
- `fusion_plating_notifications` minor bump
|
||||
|
||||
Deploy order: notifications → jobs (post-migrate runs here) → certificates → shopfloor → quality. Each gets its own `-u` step to keep blast radius small per [CLAUDE.md → Sub 12 build order rule 11](../../CLAUDE.md).
|
||||
Reference in New Issue
Block a user