feat(plating): Sub 4 — Contract Review (optional, QA-005 1:1 PDF)

Per-part contract review record (fp.contract.review) gated by a
customer-level toggle, signed in two sections (QA Assistant → QA
Manager), settings-based signer rosters (no new res.groups), banner on
the part form that auto-dismisses once the first MO for the part hits
confirmed. QA-005 Rev. 0 paper form reproduced 1:1 in a QWeb PDF.

Never blocks MO/SO/WO — review is purely an audit artefact.

Smoke test run on entech: 12 assertions pass including the 25-cell
risk matrix parity with the paper form and 22 KB PDF render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-22 21:43:06 -04:00
parent 98a8bc234b
commit 21da526aa7
17 changed files with 1472 additions and 2 deletions

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 4 — Contract Review QWeb report action.
-->
<odoo>
<record id="action_report_contract_review" model="ir.actions.report">
<field name="name">Contract Review (QA-005)</field>
<field name="model">fp.contract.review</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_quality.report_contract_review_qa005</field>
<field name="report_file">fusion_plating_quality.report_contract_review_qa005</field>
<field name="print_report_name">'QA-005 - %s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="model_fp_contract_review"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@@ -0,0 +1,296 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 4 — Contract Review QA-005 QWeb template (1:1 paper form).
-->
<odoo>
<template id="report_contract_review_qa005">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<div class="page" style="font-family: Arial, sans-serif; font-size: 10pt; color: #000;">
<style>
.qa005-title-row { display: table; width: 100%; border-collapse: collapse; margin-bottom: 8pt; }
.qa005-title-row &gt; div { display: table-cell; border: 1.5pt solid #000; padding: 6pt; vertical-align: middle; }
.qa005-title-logo { width: 22%; text-align: center; }
.qa005-title-center { width: 44%; text-align: center; font-weight: bold; font-size: 13pt; }
.qa005-title-frm { width: 17%; font-weight: bold; }
.qa005-title-date { width: 17%; font-weight: bold; }
.qa005-header-tbl { width: 100%; border-collapse: collapse; margin-bottom: 6pt; }
.qa005-header-tbl td { border: 1pt solid #000; padding: 4pt 6pt; }
.qa005-section-title { font-weight: bold; margin-top: 6pt; margin-bottom: 3pt; border-bottom: 1pt solid #000; padding-bottom: 2pt; }
.qa005-section-desc { font-style: italic; margin-bottom: 6pt; }
.qa005-check-tbl { width: 100%; border-collapse: collapse; }
.qa005-check-tbl td { border: 1pt solid #000; padding: 6pt; width: 25%; }
.qa005-sig-tbl { width: 100%; border-collapse: collapse; margin-top: 4pt; }
.qa005-sig-tbl td { border: 1pt solid #000; padding: 6pt; }
.qa005-risk-line { margin: 6pt 0; }
.qa005-matrix { border-collapse: collapse; margin-top: 8pt; }
.qa005-matrix td { border: 1pt solid #000; width: 28pt; height: 22pt; text-align: center; }
.qa005-matrix .cell-g { background-color: #8bb36b; }
.qa005-matrix .cell-y { background-color: #fff59d; }
.qa005-matrix .cell-r { background-color: #e06666; }
.qa005-matrix .cell-sel { outline: 3pt solid #000; outline-offset: -3pt; font-weight: bold; }
.qa005-consequences-tbl, .qa005-likelihood-tbl { font-size: 9pt; width: 100%; }
.qa005-consequences-tbl td, .qa005-likelihood-tbl td { padding: 2pt 4pt; }
.qa005-box-filled::before { content: "\2612 "; font-size: 12pt; }
.qa005-box-empty::before { content: "\2610 "; font-size: 12pt; }
.qa005-evaluate-inline span { margin-right: 14pt; }
</style>
<!-- ============ HEADER BAR (logo + title + code + date) ============ -->
<div class="qa005-title-row">
<div class="qa005-title-logo">
<img t-if="doc.company_id.logo"
t-att-src="'data:image/png;base64,%s' % to_text(doc.company_id.logo)"
style="max-height: 45pt; max-width: 100%;"/>
</div>
<div class="qa005-title-center">
CONTRACT REVIEW AND<br/>RISK ASSESSMENT
</div>
<div class="qa005-title-frm">
FRM: QA-005<br/>Rev. 0
</div>
<div class="qa005-title-date">
Issue Date:<br/>Nov 25, 2021
</div>
</div>
<!-- ============ IDENTITY TABLE ============ -->
<table class="qa005-header-tbl">
<tr>
<td style="width:12%;"><b>Customer:</b></td>
<td style="width:28%;"><span t-field="doc.customer_id.name"/></td>
<td style="width:14%;"><b>Quote or Job #:</b></td>
<td style="width:14%;"><span t-field="doc.quote_or_job_number"/></td>
<td style="width:10%;"><b>Part No:<br/>Prime</b></td>
<td style="width:12%;">
<span t-field="doc.part_number"/>
<br/>Rev: <span t-field="doc.part_revision"/>
</td>
</tr>
<tr>
<td><b>Contract / P.O. No:</b></td>
<td><span t-field="doc.contract_po_number"/></td>
<td><b>Date Rec'd:</b></td>
<td><span t-field="doc.date_received"/></td>
<td><b>Qty</b></td>
<td><span t-field="doc.qty"/>
— Due <span t-field="doc.due_date"/></td>
</tr>
</table>
<p class="qa005-section-desc">
I have reviewed the Purchase Order / RFQ and have verified that all
the information supplied is accurate and reflects the terms and
conditions as specified on the referenced quotation (if applicable),
and meets the general criteria for acceptance for the reviewing
function.
</p>
<!-- ============ SECTION 2.0 ============ -->
<div class="qa005-section-title">2.0 Planning / Production Review</div>
<table class="qa005-check-tbl">
<tr>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_acceptable_lead_time else 'empty' }}"/>Acceptable Lead Time</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_capacity_to_process else 'empty' }}"/>Capacity to Process</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_skills_to_process else 'empty' }}"/>Skills to Process</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_fixtures_required else 'empty' }}"/>Fixtures Required</td>
</tr>
<tr>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_prime_approvals else 'empty' }}"/>Prime approvals on file</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_pricing else 'empty' }}"/>Pricing</td>
<td colspan="2"><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_approved_technique else 'empty' }}"/>Approved Technique by Customer</td>
</tr>
<tr>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_drawings_available else 'empty' }}"/>Drawings available</td>
<td colspan="2"><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_process_type_class_grade else 'empty' }}"/>Process Type / Class / Grade</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s20_pre_post_processing_steps else 'empty' }}"/>Pre / Post Processing Steps</td>
</tr>
</table>
<table class="qa005-sig-tbl">
<tr>
<td style="width:12%;">
<b>Accepted</b>
<span t-attf-class="qa005-box-{{ 'filled' if doc.s20_accepted else 'empty' }}"/>
</td>
<td style="width:20%;"><b>Production Signature</b></td>
<td style="width:48%;">
<span t-field="doc.s20_signed_by.name"/>
</td>
<td style="width:8%;"><b>Date:</b></td>
<td style="width:12%;">
<span t-field="doc.s20_signed_date"/>
</td>
</tr>
</table>
<div class="qa005-risk-line">
<b>Comments:</b> <span t-field="doc.s20_comments"/>
</div>
<div class="qa005-risk-line">
<b><i>EVALUATE RISK</i></b>
<span class="qa005-evaluate-inline">
<span>
<span t-attf-class="qa005-box-{{ 'filled' if doc.s20_evaluate_risk else 'empty' }}"/>YES
</span>
<span>
<span t-attf-class="qa005-box-{{ 'filled' if not doc.s20_evaluate_risk else 'empty' }}"/>NO
</span>
<b>Level:</b>
<span t-esc="' '.join(['[%s]' % n if str(n) == doc.s20_risk_level else str(n) for n in range(1, 6)])"/>
</span>
</div>
<!-- ============ SECTION 3.0 ============ -->
<div class="qa005-section-title" style="margin-top:10pt;">3.0 Quality Review</div>
<table class="qa005-check-tbl">
<tr>
<td colspan="2"><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_source_control_docs else 'empty' }}"/>Source Control Documents (Customer Spec's)</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_quality_clauses_supplied else 'empty' }}"/>Quality Clause(s) supplied</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_quality_clauses_attainable else 'empty' }}"/>Quality Clause(s) attainable</td>
</tr>
<tr>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_critical_tolerance else 'empty' }}"/>Critical Tolerance(s)</td>
<td colspan="2"><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_measuring_tooling else 'empty' }}"/>Measuring Tooling Available</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_quality_tests_verified else 'empty' }}"/>Quality Tests Requirements Verified</td>
</tr>
<tr>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_specification_revisions else 'empty' }}"/>Specification Revisions</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_certifications_requirements else 'empty' }}"/>Certifications Requirements</td>
<td colspan="2"><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_psd_rfd_reviewed else 'empty' }}"/>PSD, RFD etc. Reviewed</td>
</tr>
<tr>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_specification_deviations else 'empty' }}"/>Specification Deviations</td>
<td><span t-attf-class="qa005-box-{{ 'filled' if doc.s30_design_authority else 'empty' }}"/>Design Authority</td>
<td colspan="2"></td>
</tr>
</table>
<table class="qa005-sig-tbl">
<tr>
<td style="width:12%;">
<b>Accepted</b>
<span t-attf-class="qa005-box-{{ 'filled' if doc.s30_accepted else 'empty' }}"/>
</td>
<td style="width:20%;"><b>Quality Signature</b></td>
<td style="width:48%;">
<span t-field="doc.s30_signed_by.name"/>
</td>
<td style="width:8%;"><b>Date:</b></td>
<td style="width:12%;">
<span t-field="doc.s30_signed_date"/>
</td>
</tr>
</table>
<div class="qa005-risk-line">
<b><i>EVALUATE RISK</i></b>
<span class="qa005-evaluate-inline">
<span>
<span t-attf-class="qa005-box-{{ 'filled' if doc.s30_evaluate_risk else 'empty' }}"/>YES
</span>
<span>
<span t-attf-class="qa005-box-{{ 'filled' if not doc.s30_evaluate_risk else 'empty' }}"/>NO
</span>
<b>Level:</b>
<span t-esc="doc.s30_risk_band and doc.s30_risk_band.upper() or ''"/>
</span>
</div>
<!-- ============ RISK MATRIX ============ -->
<div style="display: table; width: 100%; margin-top: 10pt;">
<!-- Consequences legend (left) -->
<div style="display: table-cell; width: 42%; vertical-align: top; padding-right: 10pt;">
<b>CONSEQUENCES</b>
<table class="qa005-consequences-tbl">
<tr><td>1</td><td>Minimal</td><td>No impact</td></tr>
<tr><td>2</td><td>Moderate</td><td>Additional activities req'd</td></tr>
<tr><td>3</td><td>Mod. / Applicable</td><td>Unable to meet commitments</td></tr>
<tr><td>4</td><td>Major / Changes</td><td>Unable to meet commitments</td></tr>
<tr><td>5</td><td>Unacceptable</td><td>Unable to meet commitments</td></tr>
</table>
</div>
<!-- Likelihood legend (right) -->
<div style="display: table-cell; width: 58%; vertical-align: top;">
<b>LIKELIHOOD</b>
<table class="qa005-likelihood-tbl">
<tr><td>1</td><td>Not Likely</td><td>Current approach / process will effectively avoid this risk</td></tr>
<tr><td>2</td><td>Low Likelihood</td><td>Current approach / process has usually mitigated</td></tr>
<tr><td>3</td><td>Likely</td><td>Current approach / process may mitigate this risk</td></tr>
<tr><td>4</td><td>Highly Likely</td><td>Current approach / process cannot mitigate — different approach might</td></tr>
<tr><td>5</td><td>Near Certainty</td><td>Current approach / process cannot mitigate the risk and no processes are available</td></tr>
</table>
</div>
</div>
<!-- 5x5 coloured grid. The selected cell (consequence, likelihood)
gets an extra black outline for visual pickup on print. -->
<div style="display: table; width: 100%; margin-top: 10pt;">
<div style="display: table-cell; vertical-align: top;">
<table class="qa005-matrix">
<t t-set="bands" t-value="{
(1,5):'g',(2,5):'y',(3,5):'r',(4,5):'r',(5,5):'r',
(1,4):'g',(2,4):'y',(3,4):'y',(4,4):'r',(5,4):'r',
(1,3):'g',(2,3):'y',(3,3):'y',(4,3):'y',(5,3):'r',
(1,2):'g',(2,2):'g',(3,2):'y',(4,2):'y',(5,2):'y',
(1,1):'g',(2,1):'g',(3,1):'g',(4,1):'y',(5,1):'y',
}"/>
<t t-set="sel_c" t-value="int(doc.s30_risk_consequence) if doc.s30_risk_consequence else 0"/>
<t t-set="sel_l" t-value="int(doc.s30_risk_likelihood) if doc.s30_risk_likelihood else 0"/>
<tr t-foreach="[5,4,3,2,1]" t-as="lik">
<td style="border:none; font-weight:bold;" t-esc="lik"/>
<t t-foreach="[1,2,3,4,5]" t-as="cons">
<t t-set="band" t-value="bands.get((cons, lik))"/>
<t t-set="is_sel" t-value="(cons == sel_c) and (lik == sel_l)"/>
<td t-attf-class="cell-#{band}#{' cell-sel' if is_sel else ''}">
<t t-if="is_sel">X</t>
</td>
</t>
</tr>
<tr>
<td style="border:none;"/>
<td style="border:none; font-weight:bold;">1</td>
<td style="border:none; font-weight:bold;">2</td>
<td style="border:none; font-weight:bold;">3</td>
<td style="border:none; font-weight:bold;">4</td>
<td style="border:none; font-weight:bold;">5</td>
</tr>
</table>
<div style="font-size: 8pt; margin-top: 2pt;">
<span style="writing-mode: vertical-rl; transform: rotate(180deg);">LIKELIHOOD</span>
&#160;&#160;&#160;&#160;&#160;&#160;CONSEQUENCES
</div>
</div>
<!-- Mitigation plan required Y/N -->
<div style="display: table-cell; vertical-align: top; padding-left: 20pt;">
<div style="border: 1pt solid #000; padding: 6pt; margin-top: 10pt;">
<b>Mitigation Plan Required?</b>
<div style="margin-top: 4pt;">
<span>
<span t-attf-class="qa005-box-{{ 'filled' if doc.s30_mitigation_plan_required else 'empty' }}"/>YES
</span>
&#160;&#160;
<span>
<span t-attf-class="qa005-box-{{ 'filled' if not doc.s30_mitigation_plan_required else 'empty' }}"/>NO
</span>
</div>
</div>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>