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>
This commit is contained in:
gsinghpal
2026-04-22 21:31:05 -04:00
parent d3b4eadbec
commit 98a8bc234b

View File

@@ -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.*