# Receiving Gate on Step Start / Finish **Date:** 2026-05-18 **Status:** Approved for implementation **Author:** Brainstorming session (gsinghpal) **Triggering observation:** WO-30040 closed with `qty_received` blank and chatter warnings on Post-plate Inspection / Final Inspection ("Step started before parts were received"). The existing soft chatter warning is not strong enough — operators ignore it and the job still completes. ## Goal Block step transitions (start AND finish) on any non-Contract-Review step until the SO's receiving record is closed. Future-proof for custom steps added later. Allow manager bypass via the existing `fp_skip_*` context-flag pattern. ## Decisions reached | # | Decision | Rationale | |---|---|---| | D1 | Scope: all step kinds EXCEPT Contract Review | CR is paperwork — doesn't need parts on the floor. Every other step (including future custom steps) involves physical work. | | D2 | Timing: both `button_start` AND `button_finish` | Strongest. Operator can't begin OR complete physical work without receiving closed. Catches both "started too early" and "started before parts arrived, completed before they did". | | D3 | Threshold: `sale_order.x_fc_receiving_status == 'received'` | Post-Sub-8 (and the 2026-05-18 cleanup), `received` is the terminal receiving state. `not_received` and `partial` block. | | D4 | Manager bypass: `fp_skip_receiving_gate=True` context flag | Matches existing `fp_skip_*` pattern (qty_reconcile, qc_gate, step_gate, bake_gate). Auditor trail via chatter on the state transition. | | D5 | Implementation: single helper called from both buttons | Mirrors existing `_fp_check_contract_review_complete` pattern. DRY — same code tested once. | ## Out of scope - Receiving model's state machine (already correct post-Sub-8). - The `_update_so_receiving_status` mapping (already maps `closed → received`). - Other gates (qty_reconcile, qc_gate, bake_gate) — untouched. - Schema changes — pure behavior change. ## Architecture ``` fp.job.step.button_start fp.job.step.button_finish 1. Sequential-order gate (existing) 1. _fp_check_contract_review_complete (existing) 2. _fp_check_receiving_gate() ← NEW 2. _fp_check_receiving_gate() ← NEW 3. Contract Review auto-open (existing) 3. super().button_finish() + downstream (existing) 4. Racking auto-open (existing) 5. Standard path + serial promote (existing) [old soft chatter warning removed] ``` ## Helper method ```python def _fp_check_receiving_gate(self): """Block step transitions until parts are physically received. Applied to every step EXCEPT Contract Review. Fires from both button_start and button_finish. Manager bypass via context flag `fp_skip_receiving_gate=True`. """ if self.env.context.get('fp_skip_receiving_gate'): return for step in self: if step._fp_is_contract_review_step(): continue so = step.job_id.sale_order_id if not so: continue # internal rework — gate doesn't apply if 'x_fc_receiving_status' not in so._fields: continue # defensive: configurator not installed if so.x_fc_receiving_status != 'received': label = dict( so._fields['x_fc_receiving_status'].selection ).get(so.x_fc_receiving_status, so.x_fc_receiving_status or 'unknown') raise UserError(_( 'Step "%(step)s" cannot proceed — parts not received yet ' '(SO %(so)s receiving status: %(status)s).\n\n' 'Close the receiving record (Sales > %(so)s > Receiving) ' 'before starting or finishing work on this step. A ' 'manager can bypass this gate for documented exceptions.' ) % { 'step': step.name, 'so': so.name or '?', 'status': label, }) ``` ## Module changes | Module | Bump | Files | |---|---|---| | `fusion_plating_jobs` | 19.0.10.12.0 → 19.0.10.13.0 | `models/fp_job_step.py` (helper + 2 callers + remove soft warning); `tests/test_fp_job_milestone_cascade.py` (new TestReceivingGate class) | ## Edge cases | Case | Behavior | |---|---| | Step on job with no SO link (internal rework) | Gate doesn't fire — `continue`. | | Configurator module not installed (`x_fc_receiving_status` field absent) | Gate doesn't fire — `continue`. | | Contract Review step on `not_received` SO | Gate exempt; step proceeds (paperwork). | | Step on `partial` SO | Blocks — `partial` is not `received`. Operator waits for all boxes to land. | | Manager bypass via context | All gates skipped uniformly. Audit trail preserved via state-transition tracking. | ## Test plan 8 unit tests in new `TestReceivingGate` class in `test_fp_job_milestone_cascade.py`: - `test_start_blocks_when_not_received` - `test_start_allows_when_received` - `test_start_skips_contract_review` - `test_start_bypass_via_context` - `test_finish_blocks_when_not_received` - `test_finish_allows_when_received` - `test_finish_skips_contract_review` - `test_finish_bypass_via_context` **Manual verification on entech post-deploy:** 1. Open SO-30041 (currently `not_received`) → fp.job → try `button_start` on first non-CR step → UserError raised. 2. Close the receiving record (counted → staged → closed) → SO flips to `received`. 3. Re-try `button_start` → succeeds. 4. Repeat the start/finish flow with `fp_skip_receiving_gate=True` from a shell to verify bypass. ## Backwards compatibility - The old soft chatter warning at fp_job_step.py:894-907 is removed. The information is no longer useful — it was a soft warning for a behavior we're now hard-blocking. The job's chatter still tracks the state transition via Odoo's tracking. - Jobs already in `in_progress` on `not_received` SOs at deploy time: any future button_finish will block. Manager must either close receiving OR use bypass. - No DB migration needed. ## Deployment - Single-module deploy to entech LXC 111 (`fusion_plating_jobs`). - No restart of dependent modules required. - Verify with manual flow above.