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:
@@ -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.*
|
||||||
Reference in New Issue
Block a user