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