Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-04-22-sub4-contract-review-design.md
gsinghpal 98a8bc234b docs(plating): Sub 4 design spec — Contract Review (optional, QA-005 1:1)
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) <noreply@anthropic.com>
2026-04-22 21:31:05 -04:00

493 lines
22 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.*