From 98a8bc234ba2a7e94ef331dd3dde5061b1bd7c81 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 22 Apr 2026 21:31:05 -0400 Subject: [PATCH] =?UTF-8?q?docs(plating):=20Sub=204=20design=20spec=20?= =?UTF-8?q?=E2=80=94=20Contract=20Review=20(optional,=20QA-005=201:1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dedicated fp.contract.review model in fusion_plating_quality, triggered by a per-customer toggle, two-section QA sign-off (QA Assistant + QA Manager), settings-based roster (no new res.groups), printable 1:1 QA-005 PDF. Never blocks MO/SO/WO. Banner auto-dismisses once first MO for the part reaches confirmed state. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-04-22-sub4-contract-review-design.md | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 fusion_plating/docs/superpowers/specs/2026-04-22-sub4-contract-review-design.md diff --git a/fusion_plating/docs/superpowers/specs/2026-04-22-sub4-contract-review-design.md b/fusion_plating/docs/superpowers/specs/2026-04-22-sub4-contract-review-design.md new file mode 100644 index 00000000..e97812de --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-04-22-sub4-contract-review-design.md @@ -0,0 +1,492 @@ +# Sub 4 — Contract Review (Optional, Per-Part, QA-005 1:1) + +**Date:** 2026-04-22 +**Module scope:** `fusion_plating_quality` (gains `fusion_plating_configurator` dependency) +**Status:** Design approved; implementing in this session +**Predecessor context:** Fine-Tuning Initiative, entry in `fusion_plating/CLAUDE.md`; supersedes the name-matched `contract_review_user_ids` gate on `mrp.workorder` + +--- + +## 1. Scope (Revised) + +Originally framed as a pre-production gate (block MO confirm until two-stage QA sign-off). +**Revised on client feedback during brainstorming (2026-04-22):** the review is **fully optional** +and **never blocks** MO/SO/WO progression. Its value is as an audit artefact, not a gate. + +### In scope + +1. **Customer-level toggle** (`res.partner.x_fc_contract_review_required`) — when ON, new + parts under that customer surface a reminder banner on the part form. +2. **Per-part review record** (`fp.contract.review`) — one per `fp.part.catalog` record. + Two-section structure mirroring the paper QA-005 form (Section 2.0 QA Assistant / + Section 3.0 QA Manager). +3. **Settings-driven QA roster** — two Many2many user lists (`qa_assistant_user_ids`, + `qa_manager_user_ids`) stored on `res.company`, exposed via `res.config.settings`. + Controls *who can sign*, without creating new `res.groups` that cascade other permissions. +4. **Printable QWeb PDF** reproducing QA-005 Rev. 0 one-to-one, so a completed digital + review prints identically to the existing paper form. +5. **Lazy creation** — no `fp.contract.review` exists until the user clicks "Start + Contract Review" on the part. Clean data for parts that never get reviewed. +6. **Auto-dismiss** — banner disappears once the part's first `mrp.production` reaches + `confirmed` state; the review record (if created) is preserved for audit. +7. **Dedicated menu** — "Plating → Quality → Contract Reviews" for QA team triage + (filters: pending / in progress / complete / dismissed). + +### Out of scope (explicit) + +- Pre-production gate — withdrawn in brainstorm. +- New `res.groups` for QA Assistant / QA Manager — rejected; settings-roster replaces. +- Contract-review activity/reminder via `mail.activity` — user wants a view banner, not + an activity. +- Auto-copy from prior revision — explicit "Copy from previous revision" button only + if requested later. +- Per-customer roster override — deferred to Sub 6 (contact profiles) if needed. +- Retroactive removal of `fp.process.node.contract_review_user_ids` and the WO-finish + gate in `fusion_plating_bridge_mrp`. They stay for now; Sub 4's review is orthogonal. + Clean-up is a separate concern (can fold into Sub 6 or later). + +--- + +## 2. Data Model + +### 2.1 New model: `fp.contract.review` + +```python +class FpContractReview(models.Model): + _name = 'fp.contract.review' + _description = 'Contract Review (QA-005)' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'create_date desc, id desc' + + # ---- Identity & header ------------------------------------------------ + name = fields.Char(compute='_compute_name', store=True) + part_id = fields.Many2one('fp.part.catalog', required=True, + ondelete='cascade', tracking=True) + customer_id = fields.Many2one(related='part_id.customer_id', store=True) + part_number = fields.Char(related='part_id.part_number', store=True) + part_revision = fields.Char(related='part_id.revision', store=True) + company_id = fields.Many2one('res.company', required=True, + default=lambda s: s.env.company) + date_received = fields.Date(default=fields.Date.context_today, tracking=True) + + # Optional commercial context — blank unless triggered from an SO line + contract_po_number = fields.Char(string='Contract / PO No.') + quote_or_job_number = fields.Char(string='Quote or Job #') + qty = fields.Integer() + due_date = fields.Date() + + # ---- Section 2.0 — Planning / Production Review (QA Assistant) ------- + s20_acceptable_lead_time = fields.Boolean(string='Acceptable Lead Time') + s20_capacity_to_process = fields.Boolean(string='Capacity to Process') + s20_skills_to_process = fields.Boolean(string='Skills to Process') + s20_fixtures_required = fields.Boolean(string='Fixtures Required') + s20_prime_approvals = fields.Boolean(string='Prime approvals on file') + s20_pricing = fields.Boolean(string='Pricing') + s20_approved_technique = fields.Boolean(string='Approved Technique by Customer') + s20_drawings_available = fields.Boolean(string='Drawings available') + s20_process_type_class_grade = fields.Boolean(string='Process Type/Class/Grade') + s20_pre_post_processing_steps = fields.Boolean(string='Pre/Post Processing Steps') + s20_accepted = fields.Boolean(string='Accepted') + s20_comments = fields.Text(string='Comments') + s20_evaluate_risk = fields.Boolean(string='Evaluate Risk') + s20_risk_level = fields.Selection( + [('1','1'),('2','2'),('3','3'),('4','4'),('5','5')], + string='Risk Level') + s20_signed_by = fields.Many2one('res.users', readonly=True, + string='Production Signature') + s20_signed_date = fields.Datetime(readonly=True) + s20_locked = fields.Boolean(readonly=True, default=False) + + # ---- Section 3.0 — Quality Review (QA Manager) ----------------------- + s30_source_control_docs = fields.Boolean(string="Source Control Documents (Customer Spec's)") + s30_quality_clauses_supplied = fields.Boolean(string='Quality Clause(s) supplied') + s30_quality_clauses_attainable = fields.Boolean(string='Quality Clause(s) attainable') + s30_critical_tolerance = fields.Boolean(string='Critical Tolerance(s)') + s30_measuring_tooling = fields.Boolean(string='Measuring Tooling Available') + s30_quality_tests_verified = fields.Boolean(string='Quality Tests Requirements Verified') + s30_specification_revisions = fields.Boolean(string='Specification Revisions') + s30_certifications_requirements = fields.Boolean(string='Certifications Requirements') + s30_psd_rfd_reviewed = fields.Boolean(string='PSD, RFD etc. Reviewed') + s30_specification_deviations = fields.Boolean(string='Specification Deviations') + s30_design_authority = fields.Boolean(string='Design Authority') + s30_accepted = fields.Boolean(string='Accepted') + s30_evaluate_risk = fields.Boolean(string='Evaluate Risk') + s30_risk_consequence = fields.Selection( + [('1','1 — Minimal'),('2','2 — Moderate'),('3','3 — Mod./Applicable'), + ('4','4 — Major/Changes'),('5','5 — Unacceptable')]) + s30_risk_likelihood = fields.Selection( + [('1','1 — Not Likely'),('2','2 — Low Likelihood'),('3','3 — Likely'), + ('4','4 — Highly Likely'),('5','5 — Near Certainty')]) + s30_risk_band = fields.Selection( + [('green','Green'),('yellow','Yellow'),('red','Red')], + compute='_compute_risk_band', store=True) + s30_mitigation_plan_required = fields.Boolean(string='Mitigation Plan Required') + s30_signed_by = fields.Many2one('res.users', readonly=True, + string='Quality Signature') + s30_signed_date = fields.Datetime(readonly=True) + s30_locked = fields.Boolean(readonly=True, default=False) + + # ---- State ----------------------------------------------------------- + state = fields.Selection([ + ('draft', 'Draft'), + ('assistant_review', 'QA Assistant Review'), + ('manager_review', 'QA Manager Review'), + ('complete', 'Complete'), + ('dismissed', 'Dismissed'), + ], default='draft', tracking=True, required=True) +``` + +### 2.2 Risk band compute (5×5 matrix, matches QA-005) + +Severity × Likelihood matrix from the printed form (reading bottom-row as likelihood=1): +``` +Likelihood + 5 | G Y R R R (row reads left→right for consequence 1..5) + 4 | G Y Y R R + 3 | G Y Y Y R + 2 | G G Y Y Y + 1 | G G G Y Y +``` + +Compute: +```python +@api.depends('s30_risk_consequence', 's30_risk_likelihood') +def _compute_risk_band(self): + MATRIX = { + (1,1):'green',(2,1):'green',(3,1):'green',(4,1):'yellow',(5,1):'yellow', + (1,2):'green',(2,2):'green',(3,2):'yellow',(4,2):'yellow',(5,2):'yellow', + (1,3):'green',(2,3):'yellow',(3,3):'yellow',(4,3):'yellow',(5,3):'red', + (1,4):'green',(2,4):'yellow',(3,4):'yellow',(4,4):'red',(5,4):'red', + (1,5):'green',(2,5):'yellow',(3,5):'red',(4,5):'red',(5,5):'red', + } + for rec in self: + c = int(rec.s30_risk_consequence) if rec.s30_risk_consequence else 0 + l = int(rec.s30_risk_likelihood) if rec.s30_risk_likelihood else 0 + rec.s30_risk_band = MATRIX.get((c, l), False) +``` + +### 2.3 New fields — `res.partner` + +```python +x_fc_contract_review_required = fields.Boolean( + string='Require Contract Review for new parts', + help='When enabled, newly created parts under this customer will ' + 'show a reminder banner on the part form inviting QA to complete ' + 'a Contract Review (QA-005). The review is always optional — ' + 'the reminder can be dismissed and never blocks production.', +) +``` + +Placed on the customer form's `Sales & Purchase` tab next to the existing +`x_fc_send_coc` / `x_fc_send_thickness_report` flags. + +### 2.4 New fields — `fp.part.catalog` + +```python +x_fc_contract_review_id = fields.Many2one( + 'fp.contract.review', string='Contract Review', + ondelete='set null', copy=False, +) +x_fc_contract_review_dismissed = fields.Boolean( + string='Reminder dismissed', copy=False, +) +x_fc_has_confirmed_mo = fields.Boolean( + compute='_compute_has_confirmed_mo', + store=False, +) +x_fc_contract_review_banner_visible = fields.Boolean( + compute='_compute_contract_review_banner_visible', + store=False, +) +``` + +Compute semantics: + +```python +@api.depends('customer_id.x_fc_contract_review_required', + 'x_fc_contract_review_dismissed', + 'x_fc_contract_review_id.state') +def _compute_contract_review_banner_visible(self): + for part in self: + needs = part.customer_id.x_fc_contract_review_required + dismissed = part.x_fc_contract_review_dismissed + complete = (part.x_fc_contract_review_id + and part.x_fc_contract_review_id.state == 'complete') + in_prod = part._fp_has_confirmed_mo() # helper, see 2.4.1 + part.x_fc_contract_review_banner_visible = ( + needs and not dismissed and not complete and not in_prod + ) +``` + +**2.4.1 `_fp_has_confirmed_mo()` helper.** Searches `mrp.production` joined to +`sale.order.line` by `part_catalog_id` (Sub 3 field). Returns True if any MO in +`confirmed`, `progress`, `to_close`, or `done` state exists for the part. Uses a +single `search_count` for efficiency; compute stays `store=False` to avoid +write-amplification on MO state changes. + +### 2.5 New fields — `res.company` + +```python +x_fc_qa_assistant_user_ids = fields.Many2many( + 'res.users', 'res_company_qa_assistant_rel', + 'company_id', 'user_id', + string='QA Assistant Signers', + domain=[('share', '=', False)], + help='Users authorised to sign Section 2.0 (Planning/Production Review) ' + 'on a Contract Review. Plating Managers can sign regardless.', +) +x_fc_qa_manager_user_ids = fields.Many2many( + 'res.users', 'res_company_qa_manager_rel', + 'company_id', 'user_id', + string='QA Manager Signers', + domain=[('share', '=', False)], + help='Users authorised to sign Section 3.0 (Quality Review) ' + 'on a Contract Review. Plating Managers can sign regardless.', +) +``` + +Exposed via `res.config.settings` as `related=` fields with `readonly=False`. + +### 2.6 Migration + +New fields only. No data migration required. +- `fusion_plating_quality` version bump: `19.0.1.3.0 → 19.0.2.0.0` +- `fusion_plating_quality.__manifest__.depends` adds `fusion_plating_configurator` +- Existing parts will compute `x_fc_contract_review_banner_visible = False` + (because `customer_id.x_fc_contract_review_required` defaults to False). + No false-positive banners. + +--- + +## 3. User Flow + +### 3.1 Banner & Start + +1. Admin enables `x_fc_contract_review_required` on a customer. +2. Estimator / user creates a new part under that customer. +3. Part form renders a blue info banner: + + > ℹ️ *New part created — please complete Contract Review if applicable.* + > [Start Contract Review] [Dismiss] + +4. Clicking **Start Contract Review** → + `fp.part.catalog.action_start_contract_review()`: + - Creates `fp.contract.review` with `part_id = self.id`, `state = 'assistant_review'` + - Sets `part.x_fc_contract_review_id = review.id` + - Returns `ir.actions.act_window` opening the review form +5. Clicking **Dismiss** → sets `x_fc_contract_review_dismissed = True`. Banner disappears. + Dismissal is reversible: Plating Manager sees an "Undismiss" button on the part's + Contract Review tab. + +### 3.2 Signing sections + +- On the review form, the user fills Section 2.0 fields (editable while + `not s20_locked`), then clicks **Sign Section 2.0**. +- `action_sign_section_20()`: + - Checks `self.env.user in company.x_fc_qa_assistant_user_ids` + OR `self.env.user.has_group('fusion_plating.group_fusion_plating_manager')` + - If blocked: `UserError` listing the allowed signers. + - If pass: writes `s20_signed_by = env.user`, `s20_signed_date = now`, + `s20_locked = True`, `state = 'manager_review'`. Posts a chatter + message ("Section 2.0 signed by ..."). +- Same pattern for Section 3.0 (`action_sign_section_30()`). +- On Section 3.0 sign: `state = 'complete'`. Part's banner flips off via + compute (state transition triggers recompute). + +### 3.3 Reset / Re-open + +- Plating Manager sees a **Re-open** button on complete reviews. +- Resets whichever section(s) the user chooses (wizard confirmation), + clearing `sN0_signed_by/date`, `sN0_locked`, and bumping state back. + +### 3.4 Auto-dismiss on production + +- No explicit write; `x_fc_contract_review_banner_visible` is a compute. + When the first `mrp.production` reaches `confirmed` (or later state) for + the part, `_fp_has_confirmed_mo()` returns True → banner hides. +- The review record persists for audit regardless of MO state. + +### 3.5 Part form — Contract Review tab + +Always present (not invisible). Shows: +- **If no review:** "No contract review. [Start Contract Review]" (button enabled iff + customer toggle is on; otherwise text "Customer does not require contract review"). +- **If review in progress:** state badge, section 2.0/3.0 completion indicators, + "[Open Review]". +- **If complete:** green badge "Complete — signed by *X* on *date*, *Y* on *date*", + "[Open Review]" + "[Print PDF]". +- **If dismissed:** "Reminder dismissed [Undismiss]" (Plating Manager only). + +### 3.6 QA worklist menu + +New menu: *Plating → Quality → Contract Reviews* (sequence after NCRs). +List view: Part Number, Revision, Customer, State, Created On, Section 2.0 Signer, +Section 3.0 Signer. Default filter: `state != 'complete' and state != 'dismissed'`. +Kanban grouped by state. + +--- + +## 4. QWeb Report — QA-005 Rev. 0 (1:1 Paper Form) + +**External ID:** `fusion_plating_quality.action_report_contract_review` +**Template:** `fusion_plating_quality.report_contract_review_qa005` +**Paper size:** Letter, portrait, 0.5" margins. + +**Layout target:** visually indistinguishable from the provided QA-005 PDF +(colour logo top-left, title block "CONTRACT REVIEW AND RISK ASSESSMENT", form +code "FRM: QA-005 Rev. 0", issue date "Nov 25, 2021"; identical section table +arrangement; checkboxes render filled (■) when True, empty (☐) when False; 5×5 +risk matrix reproduced as a coloured HTML table with the current consequence × +likelihood cell highlighted). + +**Company logo:** `env.company.logo` at ~100 px wide. On the EN Technologies +tenant this is already the ENTECH colour logo; no per-module image asset needed. + +**Bilingual / blank-printable:** the template renders empty boxes and blank +signature areas when fields are unset, so the same report can be printed with a +blank review and filled by hand — matches the existing paper workflow for +customers who prefer it. + +**Print button:** on the `fp.contract.review` form and on the part's Contract +Review tab when review exists. + +--- + +## 5. Settings UI + +**Location:** Settings → Fusion Plating → Contract Review section (new). + +``` +Contract Review + ├── QA Assistant Signers [many2many_tags widget] + └── QA Manager Signers [many2many_tags widget] +``` + +Backed by `res.company.x_fc_qa_assistant_user_ids` / `x_fc_qa_manager_user_ids` +(multi-company safe). Domain `[('share','=','False')]` excludes portal users. + +--- + +## 6. Security & Access + +- `fp.contract.review` CRUD: + - **Operator:** read own company's records only + - **Supervisor:** read all, write if signer rosters allow + - **Manager:** full CRUD + re-open + - **Admin:** full CRUD + delete +- `ir.model.access.csv` entries for each group. +- `ir.rule` for company-scoped visibility (`company_id in company_ids`). +- Sign-method rosters are enforced in Python (`_check_signer`), not via access rules. + +--- + +## 7. Defensive Measures (Prevent Rework for Later Subs) + +1. **Cert-resolver-style roster helper** — single `res.company._fp_get_qa_signers(section)` + method returns the effective signer list for a given section (20 or 30). Every + sign action calls this. When Sub 6 adds per-customer / per-contact overrides, + only this helper changes; call sites stay put. +2. **Banner compute is stateless** — derived purely from customer toggle + MO state + + review state + dismissed flag. No separate banner-state column to migrate when + Sub 8 restructures receiving/inspection. +3. **Review record survives MO lifecycle** — never deleted on MO confirm/cancel, so + audit trail is stable across Sub 5's order-line changes. +4. **QWeb template isolates styling** — uses `env.company.logo` and inline CSS. No + reliance on `fusion_plating_reports` macros. Sub 5's customer-line-header macro + can be added later without touching the QA-005 template. +5. **No `res.groups` added** — means Sub 6's contact-profile permission work can + layer on freely. If aerospace customers later require named signers, that's a + per-customer override on `res.partner`, not a group. + +--- + +## 8. Testing Strategy + +### 8.1 Smoke (Python unit test) +- Create customer with `x_fc_contract_review_required = True` +- Create part under that customer → assert `x_fc_contract_review_banner_visible` +- Click `action_start_contract_review()` → assert review created, state = + `assistant_review` +- Sign section 2.0 with roster user → assert locked + state = `manager_review` +- Sign section 3.0 → assert `state = complete` + banner hidden +- Confirm MO linked to part → re-open blank review part, assert banner hidden + +### 8.2 Negative-path tests +- Non-roster user attempts sign → `UserError` +- Plating Manager bypass → succeeds +- Customer toggle OFF → banner never shows +- Dismiss → banner hidden; Plating Manager undismisses → banner returns + +### 8.3 Risk-matrix compute test +- Parametrise all 25 (consequence, likelihood) pairs → assert band matches + QA-005 paper form colour map. + +### 8.4 PDF render smoke +- Call `action_report_contract_review` on a complete review → assert bytes + returned and content-type `application/pdf`. +- Visually compare first-page render to the provided QA-005 PDF during manual QA. + +### 8.5 Migration smoke +- Upgrade module on a DB with existing parts → assert no banner appears for any + existing part (customer toggle defaults to False). + +--- + +## 9. File Manifest (what gets touched) + +``` +fusion_plating_quality/ +├── __manifest__.py ← depends +=[configurator], version bump +├── models/ +│ ├── __init__.py ← +fp_contract_review, +res_partner, +│ │ +res_company, +res_config_settings, +│ │ +fp_part_catalog (inherit) +│ ├── fp_contract_review.py ← NEW +│ ├── res_partner.py ← NEW (x_fc_contract_review_required) +│ ├── res_company.py ← NEW (QA signer rosters) +│ ├── res_config_settings.py ← NEW (related fields) +│ └── fp_part_catalog.py ← NEW (banner compute + actions) +├── views/ +│ ├── fp_contract_review_views.xml ← NEW (form, list, kanban, search, action, menu) +│ ├── res_partner_views.xml ← NEW (toggle on Sales tab) +│ ├── res_config_settings_views.xml ← NEW (settings UI) +│ └── fp_part_catalog_views.xml ← NEW (banner + tab inherit) +├── reports/ +│ ├── __init__.py +│ ├── contract_review_report.xml ← NEW (report action) +│ └── contract_review_template.xml ← NEW (QWeb 1:1 QA-005) +├── security/ +│ ├── fp_quality_security.xml ← unchanged +│ └── ir.model.access.csv ← +fp.contract.review rows +└── data/ + └── (no data seeds needed for Sub 4) +``` + +Approximate LOC: ~900 Python, ~500 XML. Single-session implementable. + +--- + +## 10. Rollout & Versioning + +- `fusion_plating_quality` → `19.0.2.0.0` +- Entech deployment order: + 1. Rsync module to `/mnt/extra-addons/custom/fusion_plating_quality/` + 2. `systemctl stop odoo` + 3. `odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_quality --stop-after-init` + 4. `systemctl start odoo` +- Post-deploy verification: + - Open customer, toggle `x_fc_contract_review_required`, create test part, + confirm banner appears, complete review end-to-end, print PDF. + +--- + +## 11. Open Items for Future Subs (Tracked Here, Not Here) + +- Sub 6 — per-customer QA signer overrides +- Eventual retirement of legacy `contract_review_user_ids` on `fusion.plating.process.node` + and the WO-finish gate in `fusion_plating_bridge_mrp` (orthogonal to Sub 4; + clean up when Sub 6 lands or earlier if requested). + +--- + +*End of spec.*