changes
This commit is contained in:
@@ -100,6 +100,87 @@ These modules have **source code in this repo** but are **intentionally NOT inst
|
||||
- SCSS class prefix: `o_fp_*` (shopfloor: `o_fp_po_*`, `o_fp_pt_*`; recipes: `o_fp_recipe_*`)
|
||||
- Monetary fields: always pair with `currency_id` field on the same model
|
||||
|
||||
## Smart Buttons — Anatomy + Conventions
|
||||
|
||||
Smart buttons sit in the `<div class="oe_button_box" name="button_box">` at the top of a form view. Every smart button MUST follow this canonical pattern so the row stays visually consistent — icon on top, count in the middle, label on the bottom.
|
||||
|
||||
### Canonical button shape
|
||||
|
||||
```xml
|
||||
<button name="action_view_holds" <!-- method on the underlying model -->
|
||||
type="object"
|
||||
class="oe_stat_button" <!-- mandatory — drives the box styling -->
|
||||
icon="fa-hand-paper-o" <!-- Font Awesome 4.x class, always fa-* -->
|
||||
invisible="x_fc_hold_count == 0"> <!-- optional; see "Conditional visibility" -->
|
||||
<field name="x_fc_hold_count" widget="statinfo" string="Holds"/>
|
||||
</button>
|
||||
```
|
||||
|
||||
What each piece does:
|
||||
- `name=` — the Python method called on click (an `action_view_X` returning a window action dict).
|
||||
- `class="oe_stat_button"` — REQUIRED. Without it the button doesn't get the stat-box styling and renders as a plain action button.
|
||||
- `icon=` — Font Awesome 4 (`fa-cogs`, `fa-truck`, `fa-list-alt`, `fa-th-large`, etc.). Pick one that telegraphs the target model.
|
||||
- `<field widget="statinfo">` — REQUIRED for the count-on-top label-below format. Don't use `string="Foo"` on the `<button>` itself when you want a count — that produces a label-only button (the empty `BOM Items` issue we fixed in v19.0.17.6.0).
|
||||
|
||||
### Don'ts (every one of these is a real bug we shipped + reverted)
|
||||
|
||||
- **Don't use `string="Label"` on `<button>` if the button has a meaningful count** — you get a plain `Label` button with no number. Use the `<field widget="statinfo">` form instead.
|
||||
- **Don't anchor smart-button xpath to a model that may not exist** (e.g. `//button[@name='action_view_mrp_production']` — `mrp.production` is gone post-Sub 11). Anchor to a stable button this same view adds (e.g. `action_view_pickings`) or to `//div[hasclass('oe_button_box')]` directly.
|
||||
- **Don't add a smart button that always shows zero** because the underlying field/model is gone (the dead `Work Orders` button we removed in 19.0.17.4.0). If the count is structurally zero, drop the button entirely.
|
||||
- **Don't compute counts via `env.get('model')`** — `Environment` in Odoo 19 has no `get`. Use `'model.name' in self.env` then `self.env['model.name']` (see Critical Rules — Odoo 19).
|
||||
- **Don't put the same data behind two different buttons.** "Plating Jobs" and "Work Orders" were both fp.job lookups — we kept Plating Jobs and dropped Work Orders.
|
||||
|
||||
### Conditional visibility
|
||||
|
||||
If a button is only meaningful for some SOs (e.g. `BOM Items` is noise on a single-part SO; `By Job Group` is noise on an SO with no group tags), HIDE it conditionally rather than letting it render as `0 Foo`:
|
||||
|
||||
```xml
|
||||
invisible="x_fc_distinct_part_count < 2" <!-- BOM Items: 2+ parts -->
|
||||
invisible="not x_fc_has_wo_group_tag" <!-- By Job Group: at least one tag -->
|
||||
invisible="x_fc_ncr_count == 0" <!-- NCRs: only when there are open ones -->
|
||||
```
|
||||
|
||||
Add the supporting boolean / count as a stored or non-stored compute on the model. Group multiple visibility helpers in ONE compute method to keep the `_compute_smart_button_visibility` chain cheap (one pass over `order_line`).
|
||||
|
||||
### Ordering / placement
|
||||
|
||||
- **Always-visible meaningful buttons go first** — they're the workflow signals an operator scans for first (Receiving, Plating Jobs, Holds, Checks).
|
||||
- **NCRs / RMAs sit in the middle** — visible only when present (so they pop only when there's actual quality work).
|
||||
- **Conditional / multi-lens analytical buttons go LAST** (BOM Items, By Job Group). They overflow into the `More ▾` dropdown when the row is full, which is fine — they're the "I'm zooming into a complex SO" tools, not the daily-driver buttons.
|
||||
|
||||
To add a button at the end of the row regardless of where the inherited view positions things, use a second xpath:
|
||||
```xml
|
||||
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
|
||||
<button .../>
|
||||
</xpath>
|
||||
```
|
||||
`position="inside"` appends to the end of the button box.
|
||||
|
||||
### Action method shape
|
||||
|
||||
```python
|
||||
def action_view_holds(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Holds'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.plating.quality.hold',
|
||||
'view_mode': 'list,form', # always 'list,form' or 'kanban,list,form'
|
||||
'domain': [('job_id', '=', self.id)], # filter to this record's data
|
||||
'context': {'default_job_id': self.id}, # so the Create button pre-fills
|
||||
}
|
||||
```
|
||||
Always include a `context` with `default_*` keys for the Create button on the empty-list state — otherwise the operator hits Create on an empty list and gets a blank form with no link back to the source record.
|
||||
|
||||
### Smart-button row checklist before merge
|
||||
|
||||
- [ ] Uses `class="oe_stat_button"` and `widget="statinfo"` if it shows a count
|
||||
- [ ] Has an `icon=` (FA 4 class)
|
||||
- [ ] Has an `invisible=` clause if the count is structurally zero in some scenarios
|
||||
- [ ] Action method returns a window action with `view_mode`, `domain`, and `context.default_*`
|
||||
- [ ] Conditional/analytical buttons are pushed to the end of the button box via a second `position="inside"` xpath
|
||||
- [ ] No two buttons surface the same underlying records (no MRP/native duplicates)
|
||||
|
||||
## Process Recipe System (NEW — v19.0.2.x)
|
||||
**Model**: `fusion.plating.process.node` (in `fusion_plating` core)
|
||||
- Hierarchical tree with `_parent_store = True`
|
||||
@@ -711,3 +792,88 @@ UNION ALL SELECT 'capa', count(*) FROM fusion_plating_capa
|
||||
UNION ALL SELECT 'hold', count(*) FROM fusion_plating_quality_hold
|
||||
UNION ALL SELECT 'check', count(*) FROM fusion_plating_quality_check;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Battle Tests — Real-World Operator Scenario Coverage
|
||||
|
||||
Persona-driven shop-floor scenarios that surfaced bugs / workflow holes. Every scenario has:
|
||||
- A test script in `fusion_plating_quality/scripts/bt_s*.py` you can re-run end-to-end on entech (or any DB)
|
||||
- A fix shipped at a specific module version
|
||||
- A description of how a real operator would trip the gap and what the system now does
|
||||
|
||||
### How to re-run any scenario
|
||||
|
||||
```bash
|
||||
# From a fresh shell, point at the entech DB:
|
||||
ssh pve-worker5 "pct exec 111 -- bash -c 'echo \"exec(open(\\\"/mnt/extra-addons/custom/fusion_plating_quality/scripts/bt_sN_NAME.py\\\").read())\" | su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\"'"
|
||||
```
|
||||
|
||||
Each script is self-contained — builds a fresh SO + job, walks the scenario, asserts the fix is in effect.
|
||||
|
||||
### Scenario index
|
||||
|
||||
| ID | Persona / Scenario | Gap before | Fix shipped | Module version | Test script |
|
||||
|----|---|---|---|---|---|
|
||||
| **S1** | Carlos forgot to click Start; realizes 2h later | `date_started` readonly + no way to back-date | `action_recompute_duration_from_timelogs` on `fp.job.step` re-sums after timelog edits | `fusion_plating_jobs 19.0.6.10.0` | `bt_s2_*` (covered with S2) |
|
||||
| **S2** | Carlos finished step physically; forgot Finish; went home (12h ghost) | Same as S1 | Same fix — supervisor edits the timelog row, clicks Recompute Duration | `fusion_plating_jobs 19.0.6.10.0` | `battle_test_v2.py` Fix 4 |
|
||||
| **S3** | Two operators tap Start on same step | ✓ already blocked correctly | n/a | — | `battle_test.py` |
|
||||
| **S4** | Out-of-order step finish (intentional for parallel tanks) | Allowed by design (parallel work). Use S14 for opt-in serial | n/a | — | `battle_test.py` |
|
||||
| **S5** | Manager takes over a stuck step (operator on vacation) | ✓ reassign + finish work — added audit in S9 | See S9 | — | `battle_test.py` |
|
||||
| **S6** | Bake window expired; operator wants to start anyway | Silently allowed → no audit | `action_start_bake` blocks `missed_window`; manager-only `action_force_start_missed` overrides + posts chatter audit | `fusion_plating_shopfloor 19.0.24.1.0` | `battle_test_v2.py` Fix 1 |
|
||||
| **S7** | Step ran 12× expected duration | Silent | Chatter warning posted on the job at 1.5×+ overrun | `fusion_plating_jobs 19.0.6.10.0` | `battle_test_v2.py` Fix 2 |
|
||||
| **S8** | Job closed with `qty_done=0` despite `qty=5` | Silent — invoiced for parts that may not exist | `button_mark_done` blocks until `qty_done + qty_scrapped == qty`. Manager bypass `fp_skip_qty_reconcile=True` | `fusion_plating_jobs 19.0.6.10.0` | `battle_test_v2.py` Fix 3 |
|
||||
| **S9** | Bob takes over Carlos's in_progress step | Silent reassignment (only step's own chatter logged) | `write()` override on `fp.job.step` posts to JOB chatter when `assigned_user_id` changes on active state | `fusion_plating_jobs 19.0.6.11.0` | `bt_s9_reassign.py` |
|
||||
| **S10** | Operator paused for lunch, never resumed → 14 stale-paused steps in prod | No alert / cron / activity | Daily cron `_cron_nudge_stale_paused` (24h threshold) — schedules `mail.activity` on parent job for the manager. Idempotent | `fusion_plating_jobs 19.0.6.12.0` | `bt_s10_stale_paused.py` |
|
||||
| **S11** | Rectifier dies mid-plating → operator has no abort+retry path | Only options: cancel (kills step) or pause+writetank+start (no audit) | New `action_abort_for_retry(reason, new_tank_id)` — closes timelog, swaps tank, posts chatter, resets to `ready` | `fusion_plating_jobs 19.0.6.13.0` | `bt_s11_verify.py` |
|
||||
| **S12** | Sarah edits SO line qty 5→8 mid-job | Silent — Carlos plates 5, invoice ships 8 | `sale.order.line.write` posts warning to job chatter; new `action_sync_qty_from_so` button on job for explicit propagation | `fusion_plating_jobs 19.0.6.14.0` | `bt_s12_verify.py` |
|
||||
| **S13** | Recipe author wrote detailed step instructions; operator never sees them on tablet | Tablet payload omitted `instructions`/`thickness_target`/`dwell_time_minutes`/`bake_setpoint_temp`/`requires_signoff` | All 5 fields added to `/fp/shopfloor/scan` response AND `_step_payload` for tablet_overview | `fusion_plating_shopfloor 19.0.24.2.0` | `bt_s13_verify.py` |
|
||||
| **S14** | No way to enforce serial-required steps (e.g. acid etch → plating) | Out-of-order start always allowed | New `requires_predecessor_done` Boolean on `fusion.plating.process.node` → related on `fp.job.step` → `button_start` blocks if any earlier-sequence step isn't done/skipped/cancelled. Manager bypass `fp_skip_predecessor_check=True` | `fusion_plating 19.0.9.2.0`, `fusion_plating_jobs 19.0.6.15.0` | `bt_s14_verify.py` |
|
||||
| **S15** | Job marked done but bake.window still `awaiting_bake` | **Compliance bomb** — parts ship without bake record | `button_mark_done` blocks if any linked `fusion.plating.bake.window` is `awaiting_bake` or `bake_in_progress`. Manager bypass `fp_skip_bake_gate=True` for documented customer deviation | `fusion_plating_jobs 19.0.6.16.0` | `bt_s15_bake_close.py` |
|
||||
| **S16** | 45 phantom in_progress steps in DB (operator clocked Start, never moved) | No alert / cron / activity | Hourly cron `_cron_nudge_stale_in_progress` (8h threshold) — sister to S10 cron | `fusion_plating_jobs 19.0.6.17.0` | `bt_s16_phantom_inprogress.py` |
|
||||
| **S17** | Operator drops parts, bumps `qty_scrapped` 0→2 | Silent — no AS9100 disposition record | `fp.job.write` hook auto-spawns `fusion.plating.quality.hold` for the scrap delta. Operator updates description with cause | `fusion_plating_jobs 19.0.6.18.0` | `bt_s17_scrap_ncr.py` |
|
||||
| **S18** | CoC issuance broken in 4 places — operator can't actually email a cert | (a) auto-spawn left every useful field blank → Issue blocked on missing spec_reference; (b) Issue button never generated PDF → `attachment_id` stayed empty; (c) Send to Customer opened email composer with no attachment; (d) auto-spawn had no idempotency → dupes on `button_mark_done` retry | `_fp_create_certificates` now pre-fills `spec_reference` (from coating), `part_number`, `quantity_shipped` (qty − scrap), `po_number`, `customer_job_no`, `process_description`, `entech_wo_number`, `sale_order_id`. Idempotency check skips dupes. `action_issue` now renders the EN CoC PDF via new `_fp_render_and_attach_pdf` and sets `attachment_id` so Send to Customer attaches it automatically. Smart button "Certificates" already on the job form (visible when count > 0) so Tom finds the cert from the job he just closed | `fusion_plating_certificates 19.0.5.1.0`, `fusion_plating_jobs 19.0.6.19.0` | `bt_s18_cert_flow.py` |
|
||||
| **S19** | Lisa uploads Fischerscope X-Ray thickness PDF to QC; CoC ships without it as page 2 — and even after the back-end merge worked, operators couldn't *see* in the cert form whether the merge would happen | Existing merge logic lived in uninstalled `fusion_plating_bridge_mrp` (keyed off `mrp.production` — gone with Sub 11). Post-Sub-11 cert path rendered CoC only; Fischerscope PDF stayed orphaned on the QC record. Even after Phase 1 fix shipped, the cert form had **zero** indicator that a thickness PDF was on file or had been merged → user reported "I did not see anything in the certification issue" | **Phase 1 (back-end merge):** Ported merge to `fp.certificate._fp_merge_thickness_into_pdf`. New `_fp_render_and_attach_pdf` wraps cert PDF generation: renders the CoC via QWeb, then looks up the linked `fusion.plating.quality.check` (`x_fc_job_id → fp.job → QC`), finds the most recent passed QC with `thickness_report_pdf_id`, merges via `pypdf.PdfWriter.append()` (PyPDF2 `PdfMerger` fallback), posts chatter audit `Fischerscope thickness report from QC <name> appended to CoC PDF.`. Hooked into `action_issue` so the multi-page PDF lands on `attachment_id` automatically. **Phase 2 (UI surface):** Added 3 computed fields on `fp.certificate` (in `fusion_plating_jobs`): `x_fc_thickness_qc_id` (linked QC), `x_fc_thickness_pdf_id` (Fischerscope PDF), `x_fc_thickness_status` (`none` / `pending` / `merged`). Cert form now shows: (1) coloured banner above the title — blue "Will Append on Issue" / green "Merged" / amber "No PDF — operator action required"; (2) two new smart buttons (Plating Job, Fischerscope status); (3) new "Thickness Report (Fischerscope)" notebook tab with clickable PDF preview + step-by-step instructions when none uploaded | `fusion_plating_certificates 19.0.5.2.0`, `fusion_plating_jobs 19.0.6.20.0` | `bt_s19_fischer_merge.py` (asserts both pre-Issue `pending` + post-Issue `merged` status flips) |
|
||||
|
||||
### Manager-bypass context flags
|
||||
|
||||
When you need to override a guard (documented customer deviation, emergency rework, etc.), set the context key on the call. All bypasses post to chatter with the user name for audit:
|
||||
|
||||
| Flag | Skips |
|
||||
|------|-------|
|
||||
| `fp_skip_step_gate=True` | step-completion check on `button_mark_done` (S5/S8 era) |
|
||||
| `fp_skip_qc_gate=True` | QC checklist requirement on `button_mark_done` |
|
||||
| `fp_skip_qty_reconcile=True` | qty_done + qty_scrapped == qty check on `button_mark_done` |
|
||||
| `fp_skip_bake_gate=True` | bake.window pending check on `button_mark_done` (S15) |
|
||||
| `fp_skip_predecessor_check=True` | requires_predecessor_done check on `button_start` (S14) |
|
||||
| `fp_skip_missed_window=True` | missed_window block on `bake.window.action_start_bake` (S6) |
|
||||
|
||||
### Daily / hourly crons added by battle tests
|
||||
|
||||
| Cron | Schedule | What it does |
|
||||
|------|----------|--------------|
|
||||
| `Fusion Plating: Nudge stale paused steps` | daily | 24h threshold, schedules activity on job for stale `paused` steps |
|
||||
| `Fusion Plating: Nudge stale in-progress steps` | hourly | 8h threshold, sister cron for `in_progress` (phantom-time guard) |
|
||||
| `Fusion Plating: Update Bake Window states` | every 5 min | (pre-existing) flips awaiting_bake → missed_window past required_by |
|
||||
|
||||
### Open scenarios — flagged for next session
|
||||
|
||||
- **S20** — Operator clocks two steps simultaneously across different jobs (multi-tasking conflict)
|
||||
- **S21** — Bath chemistry drift mid-step — operator measures bath while plating, value out of spec; no alert on the step
|
||||
- **S22** — Wrong recipe attached — Carlos sees mismatch with the part he's holding; recovery path?
|
||||
- **S23** — Customer orders 100 parts spread across 3 jobs; one job's recipe gets edited — does it propagate to siblings?
|
||||
- **S24** — Hold-aging cron + 3-day escalation (flagged in original audit, not yet built)
|
||||
- **S25** — Calibration + permit-expiry cron (flagged in original audit, not yet built)
|
||||
- **S26** — FAIR detection on first-shipment to a new customer/part combo (flagged in original audit, not yet built)
|
||||
|
||||
### Where the test scripts live
|
||||
|
||||
`K:/Github/Odoo-Modules/fusion_plating/fusion_plating_quality/scripts/`
|
||||
- `battle_test.py` — original S1–S8 (mixed, some not-bug scenarios)
|
||||
- `battle_test_v2.py` — re-verify of S6/S7/S8/S2 fixes
|
||||
- `bt_s9_reassign.py` through `bt_s17_scrap_ncr.py` — one script per scenario
|
||||
- `bt_s18_cert_flow.py` — full CSR→operator→QC→shipper cert issuance + Send to Customer
|
||||
- `bt_s19_fischer_merge.py` — uploads fake Fischerscope PDF to QC, asserts CoC + thickness merged into 2-page output
|
||||
- `step_internal_full.py` — full pause/resume/skip/bake-spawn walk
|
||||
|
||||
To re-test the whole battle suite after a future change, run each `bt_s*.py` in sequence and confirm green.
|
||||
|
||||
Reference in New Issue
Block a user