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

22 KiB
Raw Blame History

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

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:

@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

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

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:

@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

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 Reviewfp.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_quality19.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.