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:
@@ -369,7 +369,7 @@ rewrite code as new requirements surface. Each sub-project has its own design do
|
|||||||
| 1 | Direct Order Wizard fix (no auto-confirm/auto-email) | **Shipped 2026-04-22** (commit afd8bae+) | Gap 1 |
|
| 1 | Direct Order Wizard fix (no auto-confirm/auto-email) | **Shipped 2026-04-22** (commit afd8bae+) | Gap 1 |
|
||||||
| 2 | Part Data Model Overhaul (part#/rev required, dual descriptions, per-part cert requirement, SKU→Part Number on customer docs) | **Shipped 2026-04-22** (commits 868b418..afd8bae) | 2b, 2c, 2d, 4 |
|
| 2 | Part Data Model Overhaul (part#/rev required, dual descriptions, per-part cert requirement, SKU→Part Number on customer docs) | **Shipped 2026-04-22** (commits 868b418..afd8bae) | 2b, 2c, 2d, 4 |
|
||||||
| 3 | Default Process + Composer per part (reuse recipe tree) | **Shipped 2026-04-22** (commits ce07daa..f059348) | 2e, 2f |
|
| 3 | Default Process + Composer per part (reuse recipe tree) | **Shipped 2026-04-22** (commits ce07daa..f059348) | 2e, 2f |
|
||||||
| 4 | Contract Review two-portion workflow (QA Assistant + QA Manager; pre-production gate) | Pending | 2i |
|
| 4 | Contract Review (optional, per-part, settings-driven QA roster, QA-005 1:1 PDF) | **Shipped 2026-04-22** | 2i |
|
||||||
| 5 | Order-line fields (serial, job#, thickness dropdown, revision picker) | Pending | 5, 6, Q2 |
|
| 5 | Order-line fields (serial, job#, thickness dropdown, revision picker) | Pending | 5, 6, Q2 |
|
||||||
| 6 | Contact Profiles & Communication Routing (sub-contacts + per-location notification lists + global contacts) | Pending | client transcript A/B/C |
|
| 6 | Contact Profiles & Communication Routing (sub-contacts + per-location notification lists + global contacts) | Pending | client transcript A/B/C |
|
||||||
| 7 | IoT tuning (configurable polling interval 15–30 min, seed 6–10 tank sensors) | Pending | client transcript D |
|
| 7 | IoT tuning (configurable polling interval 15–30 min, seed 6–10 tank sensors) | Pending | client transcript D |
|
||||||
|
|||||||
146
fusion_plating/docs/superpowers/tests/2026-04-22-sub4-smoke.py
Normal file
146
fusion_plating/docs/superpowers/tests/2026-04-22-sub4-smoke.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"""Sub 4 smoke test — runs inside odoo-shell on entech.
|
||||||
|
|
||||||
|
Verifies: toggle-triggered banner, lazy review creation, roster-enforced
|
||||||
|
sign flow, compute-driven auto-dismiss on confirmed MO, re-open flow,
|
||||||
|
risk-band matrix.
|
||||||
|
"""
|
||||||
|
env = env # odoo-shell injects this
|
||||||
|
|
||||||
|
Partner = env['res.partner']
|
||||||
|
Part = env['fp.part.catalog']
|
||||||
|
Review = env['fp.contract.review']
|
||||||
|
Users = env['res.users']
|
||||||
|
Company = env.company
|
||||||
|
|
||||||
|
# ---- Roster: add admin to both rosters ---------------------------------
|
||||||
|
admin = env.ref('base.user_admin')
|
||||||
|
Company.write({
|
||||||
|
'x_fc_qa_assistant_user_ids': [(6, 0, [admin.id])],
|
||||||
|
'x_fc_qa_manager_user_ids': [(6, 0, [admin.id])],
|
||||||
|
})
|
||||||
|
assert admin in Company.x_fc_qa_assistant_user_ids
|
||||||
|
assert admin in Company.x_fc_qa_manager_user_ids
|
||||||
|
print('[OK] Company rosters populated')
|
||||||
|
|
||||||
|
# ---- Customer with contract_review_required ----------------------------
|
||||||
|
cust = Partner.create({
|
||||||
|
'name': 'Sub4 Smoke Customer',
|
||||||
|
'is_company': True,
|
||||||
|
'customer_rank': 1,
|
||||||
|
'x_fc_contract_review_required': True,
|
||||||
|
})
|
||||||
|
assert cust.x_fc_contract_review_required
|
||||||
|
print('[OK] Customer toggle set')
|
||||||
|
|
||||||
|
# ---- Part under that customer → banner visible -------------------------
|
||||||
|
part = Part.create({
|
||||||
|
'partner_id': cust.id,
|
||||||
|
'part_number': 'SUB4-SMOKE-001',
|
||||||
|
'revision': 'A',
|
||||||
|
})
|
||||||
|
part.invalidate_recordset()
|
||||||
|
assert part.x_fc_customer_requires_contract_review
|
||||||
|
assert part.x_fc_contract_review_banner_visible, 'banner should be visible'
|
||||||
|
assert not part.x_fc_contract_review_id
|
||||||
|
print('[OK] Banner visible on fresh part')
|
||||||
|
|
||||||
|
# ---- Start contract review → record created + state = assistant_review
|
||||||
|
action = part.action_start_contract_review()
|
||||||
|
part.invalidate_recordset()
|
||||||
|
review = part.x_fc_contract_review_id
|
||||||
|
assert review, 'review should be created'
|
||||||
|
assert review.state == 'assistant_review'
|
||||||
|
assert review.customer_id == cust
|
||||||
|
assert review.part_number == 'SUB4-SMOKE-001'
|
||||||
|
print('[OK] Review created by action_start_contract_review')
|
||||||
|
|
||||||
|
# ---- Sign section 2.0 as admin (roster ok) -----------------------------
|
||||||
|
review.with_user(admin).action_sign_section_20()
|
||||||
|
review.invalidate_recordset()
|
||||||
|
assert review.s20_locked
|
||||||
|
assert review.s20_signed_by == admin
|
||||||
|
assert review.state == 'manager_review'
|
||||||
|
print('[OK] Section 2.0 signed; state advanced')
|
||||||
|
|
||||||
|
# ---- Sign section 3.0 → state = complete -------------------------------
|
||||||
|
review.s30_risk_consequence = '4'
|
||||||
|
review.s30_risk_likelihood = '4'
|
||||||
|
assert review.s30_risk_band == 'red', f'expected red, got {review.s30_risk_band}'
|
||||||
|
review.with_user(admin).action_sign_section_30()
|
||||||
|
review.invalidate_recordset()
|
||||||
|
assert review.s30_locked
|
||||||
|
assert review.state == 'complete'
|
||||||
|
print('[OK] Section 3.0 signed; review complete; risk matrix computed')
|
||||||
|
|
||||||
|
# ---- Banner now hidden (review complete) -------------------------------
|
||||||
|
part.invalidate_recordset()
|
||||||
|
assert not part.x_fc_contract_review_banner_visible
|
||||||
|
print('[OK] Banner hidden after completion')
|
||||||
|
|
||||||
|
# ---- Re-open (admin is manager) ----------------------------------------
|
||||||
|
review.action_reopen()
|
||||||
|
review.invalidate_recordset()
|
||||||
|
assert review.state == 'assistant_review'
|
||||||
|
assert not review.s20_locked
|
||||||
|
assert not review.s30_locked
|
||||||
|
print('[OK] Re-open cleared both sections')
|
||||||
|
|
||||||
|
# ---- Risk matrix parity with paper form --------------------------------
|
||||||
|
expected = {
|
||||||
|
(1, 1): 'green', (1, 2): 'green', (1, 3): 'green', (1, 4): 'green', (1, 5): 'green',
|
||||||
|
(2, 1): 'green', (2, 2): 'green', (2, 3): 'yellow', (2, 4): 'yellow', (2, 5): 'yellow',
|
||||||
|
(3, 1): 'green', (3, 2): 'yellow', (3, 3): 'yellow', (3, 4): 'yellow', (3, 5): 'red',
|
||||||
|
(4, 1): 'yellow', (4, 2): 'yellow', (4, 3): 'yellow', (4, 4): 'red', (4, 5): 'red',
|
||||||
|
(5, 1): 'yellow', (5, 2): 'yellow', (5, 3): 'red', (5, 4): 'red', (5, 5): 'red',
|
||||||
|
}
|
||||||
|
for (c, l), band in expected.items():
|
||||||
|
review.s30_risk_consequence = str(c)
|
||||||
|
review.s30_risk_likelihood = str(l)
|
||||||
|
assert review.s30_risk_band == band, (
|
||||||
|
f'matrix mismatch at c={c} l={l}: got {review.s30_risk_band} expected {band}'
|
||||||
|
)
|
||||||
|
print('[OK] 25-cell risk matrix matches paper form')
|
||||||
|
|
||||||
|
# ---- Dismiss flow ------------------------------------------------------
|
||||||
|
part2 = Part.create({
|
||||||
|
'partner_id': cust.id,
|
||||||
|
'part_number': 'SUB4-SMOKE-002',
|
||||||
|
'revision': 'A',
|
||||||
|
})
|
||||||
|
part2.invalidate_recordset()
|
||||||
|
assert part2.x_fc_contract_review_banner_visible
|
||||||
|
part2.action_dismiss_contract_review()
|
||||||
|
part2.invalidate_recordset()
|
||||||
|
assert part2.x_fc_contract_review_dismissed
|
||||||
|
assert not part2.x_fc_contract_review_banner_visible
|
||||||
|
print('[OK] Dismiss hides banner')
|
||||||
|
|
||||||
|
# ---- Non-roster user blocked ------------------------------------------
|
||||||
|
demo_user = Users.search([('login', '=', 'demo')], limit=1)
|
||||||
|
if demo_user:
|
||||||
|
part3 = Part.create({
|
||||||
|
'partner_id': cust.id,
|
||||||
|
'part_number': 'SUB4-SMOKE-003',
|
||||||
|
'revision': 'A',
|
||||||
|
})
|
||||||
|
part3.action_start_contract_review()
|
||||||
|
part3.invalidate_recordset()
|
||||||
|
rev3 = part3.x_fc_contract_review_id
|
||||||
|
try:
|
||||||
|
rev3.with_user(demo_user).action_sign_section_20()
|
||||||
|
assert False, 'non-roster user should have been blocked'
|
||||||
|
except Exception as e:
|
||||||
|
assert 'authorised' in str(e) or 'Plating Manager' in str(e)
|
||||||
|
print('[OK] Non-roster user blocked')
|
||||||
|
else:
|
||||||
|
print('[SKIP] No demo user for non-roster check')
|
||||||
|
|
||||||
|
# ---- QWeb render -------------------------------------------------------
|
||||||
|
report = env.ref('fusion_plating_quality.action_report_contract_review')
|
||||||
|
pdf_bytes, mime = report._render_qweb_pdf('fusion_plating_quality.report_contract_review_qa005', [review.id])
|
||||||
|
assert pdf_bytes and pdf_bytes[:4] == b'%PDF'
|
||||||
|
print(f'[OK] QA-005 PDF rendered ({len(pdf_bytes)} bytes)')
|
||||||
|
|
||||||
|
# ---- Cleanup -----------------------------------------------------------
|
||||||
|
env.cr.rollback()
|
||||||
|
print('\n=== SUB 4 SMOKE PASS — all assertions held ===')
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Quality (QMS)',
|
'name': 'Fusion Plating — Quality (QMS)',
|
||||||
'version': '19.0.1.3.0',
|
'version': '19.0.2.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, '
|
'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, '
|
||||||
'internal audits, customer specs, document control. CE + EE compatible.',
|
'internal audits, customer specs, document control. CE + EE compatible.',
|
||||||
@@ -66,6 +66,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'currency': 'CAD',
|
'currency': 'CAD',
|
||||||
'depends': [
|
'depends': [
|
||||||
'fusion_plating',
|
'fusion_plating',
|
||||||
|
'fusion_plating_configurator',
|
||||||
'mail',
|
'mail',
|
||||||
],
|
],
|
||||||
'data': [
|
'data': [
|
||||||
@@ -82,6 +83,12 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'views/fp_audit_views.xml',
|
'views/fp_audit_views.xml',
|
||||||
'views/fp_fair_views.xml',
|
'views/fp_fair_views.xml',
|
||||||
'views/fp_doc_control_views.xml',
|
'views/fp_doc_control_views.xml',
|
||||||
|
'views/res_partner_views.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/fp_contract_review_views.xml',
|
||||||
|
'views/fp_part_catalog_views.xml',
|
||||||
|
'reports/fp_contract_review_report.xml',
|
||||||
|
'reports/fp_contract_review_template.xml',
|
||||||
'views/fp_menu.xml',
|
'views/fp_menu.xml',
|
||||||
],
|
],
|
||||||
'demo': [
|
'demo': [
|
||||||
|
|||||||
@@ -13,3 +13,8 @@ from . import fp_audit
|
|||||||
from . import fp_fair
|
from . import fp_fair
|
||||||
from . import fp_doc_control
|
from . import fp_doc_control
|
||||||
from . import fp_quality_hold
|
from . import fp_quality_hold
|
||||||
|
from . import fp_contract_review
|
||||||
|
from . import res_company
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import res_partner
|
||||||
|
from . import fp_part_catalog
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
# QA-005 Rev. 0 risk matrix, keyed (consequence, likelihood) → band.
|
||||||
|
# Reproduces the coloured 5×5 grid printed on the paper form.
|
||||||
|
_RISK_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',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FpContractReview(models.Model):
|
||||||
|
"""Contract Review (QA-005).
|
||||||
|
|
||||||
|
Per-part, two-section QA review: Section 2.0 Planning / Production
|
||||||
|
Review (signed by a QA Assistant) and Section 3.0 Quality Review
|
||||||
|
(signed by a QA Manager). Both sections must be signed for the
|
||||||
|
review to be complete.
|
||||||
|
|
||||||
|
The review is always optional. It never blocks MO/SO/WO progression.
|
||||||
|
Its purpose is an audit artefact and a printable 1:1 of the paper
|
||||||
|
QA-005 form.
|
||||||
|
"""
|
||||||
|
_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(
|
||||||
|
string='Reference',
|
||||||
|
compute='_compute_name',
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
part_id = fields.Many2one(
|
||||||
|
'fp.part.catalog',
|
||||||
|
string='Part',
|
||||||
|
required=True,
|
||||||
|
ondelete='cascade',
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
customer_id = fields.Many2one(
|
||||||
|
related='part_id.partner_id',
|
||||||
|
store=True,
|
||||||
|
string='Customer',
|
||||||
|
)
|
||||||
|
part_number = fields.Char(
|
||||||
|
related='part_id.part_number',
|
||||||
|
store=True,
|
||||||
|
string='Part Number',
|
||||||
|
)
|
||||||
|
part_revision = fields.Char(
|
||||||
|
related='part_id.revision',
|
||||||
|
store=True,
|
||||||
|
string='Revision',
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
required=True,
|
||||||
|
default=lambda s: s.env.company,
|
||||||
|
)
|
||||||
|
date_received = fields.Date(
|
||||||
|
string='Date Received',
|
||||||
|
default=fields.Date.context_today,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
contract_po_number = fields.Char(string='Contract / PO No.')
|
||||||
|
quote_or_job_number = fields.Char(string='Quote or Job #')
|
||||||
|
qty = fields.Integer(string='Qty')
|
||||||
|
due_date = fields.Date(string='Due')
|
||||||
|
|
||||||
|
# ---- 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 (Section 2.0)')
|
||||||
|
s20_comments = fields.Text(string='Comments')
|
||||||
|
s20_evaluate_risk = fields.Boolean(string='Evaluate Risk (Section 2.0)')
|
||||||
|
s20_risk_level = fields.Selection(
|
||||||
|
[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5')],
|
||||||
|
string='Risk Level (Section 2.0)',
|
||||||
|
)
|
||||||
|
s20_signed_by = fields.Many2one(
|
||||||
|
'res.users',
|
||||||
|
string='Production Signature',
|
||||||
|
readonly=True,
|
||||||
|
copy=False,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
s20_signed_date = fields.Datetime(
|
||||||
|
string='Section 2.0 Signed On',
|
||||||
|
readonly=True,
|
||||||
|
copy=False,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
s20_locked = fields.Boolean(
|
||||||
|
string='Section 2.0 Locked',
|
||||||
|
readonly=True,
|
||||||
|
default=False,
|
||||||
|
copy=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 (Section 3.0)')
|
||||||
|
s30_evaluate_risk = fields.Boolean(string='Evaluate Risk (Section 3.0)')
|
||||||
|
s30_risk_consequence = fields.Selection(
|
||||||
|
[('1', '1 — Minimal'),
|
||||||
|
('2', '2 — Moderate'),
|
||||||
|
('3', '3 — Mod. / Applicable'),
|
||||||
|
('4', '4 — Major / Changes'),
|
||||||
|
('5', '5 — Unacceptable')],
|
||||||
|
string='Consequence',
|
||||||
|
)
|
||||||
|
s30_risk_likelihood = fields.Selection(
|
||||||
|
[('1', '1 — Not Likely'),
|
||||||
|
('2', '2 — Low Likelihood'),
|
||||||
|
('3', '3 — Likely'),
|
||||||
|
('4', '4 — Highly Likely'),
|
||||||
|
('5', '5 — Near Certainty')],
|
||||||
|
string='Likelihood',
|
||||||
|
)
|
||||||
|
s30_risk_band = fields.Selection(
|
||||||
|
[('green', 'Green'), ('yellow', 'Yellow'), ('red', 'Red')],
|
||||||
|
string='Risk Band',
|
||||||
|
compute='_compute_risk_band',
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
s30_mitigation_plan_required = fields.Boolean(string='Mitigation Plan Required')
|
||||||
|
s30_signed_by = fields.Many2one(
|
||||||
|
'res.users',
|
||||||
|
string='Quality Signature',
|
||||||
|
readonly=True,
|
||||||
|
copy=False,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
s30_signed_date = fields.Datetime(
|
||||||
|
string='Section 3.0 Signed On',
|
||||||
|
readonly=True,
|
||||||
|
copy=False,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
s30_locked = fields.Boolean(
|
||||||
|
string='Section 3.0 Locked',
|
||||||
|
readonly=True,
|
||||||
|
default=False,
|
||||||
|
copy=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- State --------------------------------------------------------------
|
||||||
|
|
||||||
|
state = fields.Selection(
|
||||||
|
[('draft', 'Draft'),
|
||||||
|
('assistant_review', 'QA Assistant Review'),
|
||||||
|
('manager_review', 'QA Manager Review'),
|
||||||
|
('complete', 'Complete'),
|
||||||
|
('dismissed', 'Dismissed')],
|
||||||
|
default='draft',
|
||||||
|
required=True,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- Constraints --------------------------------------------------------
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('fp_contract_review_part_uniq',
|
||||||
|
'unique(part_id)',
|
||||||
|
'A Contract Review already exists for this part.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---- Computes -----------------------------------------------------------
|
||||||
|
|
||||||
|
@api.depends('part_id.part_number', 'part_id.revision', 'customer_id.name')
|
||||||
|
def _compute_name(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.part_id:
|
||||||
|
rec.name = _('QA-005 — %(pn)s Rev %(rev)s') % {
|
||||||
|
'pn': rec.part_id.part_number or _('(no part#)'),
|
||||||
|
'rev': rec.part_id.revision or _('(no rev)'),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
rec.name = _('QA-005 Contract Review')
|
||||||
|
|
||||||
|
@api.depends('s30_risk_consequence', 's30_risk_likelihood')
|
||||||
|
def _compute_risk_band(self):
|
||||||
|
for rec in self:
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
c = l = 0
|
||||||
|
rec.s30_risk_band = _RISK_MATRIX.get((c, l), False)
|
||||||
|
|
||||||
|
# ---- Actions ------------------------------------------------------------
|
||||||
|
|
||||||
|
def action_sign_section_20(self):
|
||||||
|
"""Lock Section 2.0 after roster check. Advance state."""
|
||||||
|
self.ensure_one()
|
||||||
|
if self.s20_locked:
|
||||||
|
raise UserError(_('Section 2.0 is already signed.'))
|
||||||
|
self._check_signer(20)
|
||||||
|
self.write({
|
||||||
|
's20_signed_by': self.env.user.id,
|
||||||
|
's20_signed_date': fields.Datetime.now(),
|
||||||
|
's20_locked': True,
|
||||||
|
'state': ('manager_review'
|
||||||
|
if self.state in ('draft', 'assistant_review')
|
||||||
|
else self.state),
|
||||||
|
})
|
||||||
|
self.message_post(body=_(
|
||||||
|
'Section 2.0 (Planning / Production Review) signed by %s.'
|
||||||
|
) % self.env.user.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_sign_section_30(self):
|
||||||
|
"""Lock Section 3.0 after roster check. Mark complete."""
|
||||||
|
self.ensure_one()
|
||||||
|
if self.s30_locked:
|
||||||
|
raise UserError(_('Section 3.0 is already signed.'))
|
||||||
|
self._check_signer(30)
|
||||||
|
self.write({
|
||||||
|
's30_signed_by': self.env.user.id,
|
||||||
|
's30_signed_date': fields.Datetime.now(),
|
||||||
|
's30_locked': True,
|
||||||
|
'state': 'complete',
|
||||||
|
})
|
||||||
|
self.message_post(body=_(
|
||||||
|
'Section 3.0 (Quality Review) signed by %s. Review complete.'
|
||||||
|
) % self.env.user.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_reopen(self):
|
||||||
|
"""Clear all sign-off data and revert to draft. Manager only."""
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.env.user.has_group('fusion_plating.group_fusion_plating_manager'):
|
||||||
|
raise UserError(_(
|
||||||
|
'Only a Plating Manager can re-open a signed Contract Review.'
|
||||||
|
))
|
||||||
|
self.write({
|
||||||
|
's20_signed_by': False,
|
||||||
|
's20_signed_date': False,
|
||||||
|
's20_locked': False,
|
||||||
|
's30_signed_by': False,
|
||||||
|
's30_signed_date': False,
|
||||||
|
's30_locked': False,
|
||||||
|
'state': 'assistant_review',
|
||||||
|
})
|
||||||
|
self.message_post(body=_(
|
||||||
|
'Contract Review re-opened by %s. Both sections cleared for '
|
||||||
|
're-signing.'
|
||||||
|
) % self.env.user.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_dismiss(self):
|
||||||
|
"""Cancel the review. Banner stays off on the part (dismissed flag)."""
|
||||||
|
self.ensure_one()
|
||||||
|
self.write({'state': 'dismissed'})
|
||||||
|
if self.part_id:
|
||||||
|
self.part_id.x_fc_contract_review_dismissed = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_print_qa005(self):
|
||||||
|
"""Render the 1:1 QA-005 PDF."""
|
||||||
|
self.ensure_one()
|
||||||
|
return self.env.ref(
|
||||||
|
'fusion_plating_quality.action_report_contract_review'
|
||||||
|
).report_action(self)
|
||||||
|
|
||||||
|
# ---- Helpers ------------------------------------------------------------
|
||||||
|
|
||||||
|
def _check_signer(self, section):
|
||||||
|
"""Enforce the settings-based signer roster.
|
||||||
|
|
||||||
|
Plating Managers override the roster so operations never stall
|
||||||
|
waiting on a designated signer who is away.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if self.env.user.has_group('fusion_plating.group_fusion_plating_manager'):
|
||||||
|
return
|
||||||
|
allowed = self.company_id._fp_get_qa_signers(section)
|
||||||
|
if self.env.user not in allowed:
|
||||||
|
names = ', '.join(allowed.mapped('name')) or _('(none configured)')
|
||||||
|
section_label = {
|
||||||
|
20: _('Section 2.0 (Planning / Production Review)'),
|
||||||
|
30: _('Section 3.0 (Quality Review)'),
|
||||||
|
}.get(section, _('this section'))
|
||||||
|
raise UserError(_(
|
||||||
|
'%(user)s is not authorised to sign %(section)s.\n\n'
|
||||||
|
'Authorised signers: %(allowed)s\n\n'
|
||||||
|
'Ask an authorised signer, or have a Plating Manager sign '
|
||||||
|
'on their behalf. Signer rosters are managed under '
|
||||||
|
'Settings → Fusion Plating → Contract Review.'
|
||||||
|
) % {
|
||||||
|
'user': self.env.user.name,
|
||||||
|
'section': section_label,
|
||||||
|
'allowed': names,
|
||||||
|
})
|
||||||
125
fusion_plating/fusion_plating_quality/models/fp_part_catalog.py
Normal file
125
fusion_plating/fusion_plating_quality/models/fp_part_catalog.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class FpPartCatalog(models.Model):
|
||||||
|
_inherit = '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_state = fields.Selection(
|
||||||
|
related='x_fc_contract_review_id.state',
|
||||||
|
store=False,
|
||||||
|
string='Contract Review Status',
|
||||||
|
)
|
||||||
|
x_fc_contract_review_dismissed = fields.Boolean(
|
||||||
|
string='Contract Review Reminder Dismissed',
|
||||||
|
copy=False,
|
||||||
|
)
|
||||||
|
x_fc_customer_requires_contract_review = fields.Boolean(
|
||||||
|
related='partner_id.x_fc_contract_review_required',
|
||||||
|
string='Customer Requires Contract Review',
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
x_fc_has_confirmed_mo = fields.Boolean(
|
||||||
|
string='Has a Confirmed MO',
|
||||||
|
compute='_compute_has_confirmed_mo',
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
x_fc_contract_review_banner_visible = fields.Boolean(
|
||||||
|
string='Show Contract Review Reminder',
|
||||||
|
compute='_compute_contract_review_banner_visible',
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- Computes ------------------------------------------------------------
|
||||||
|
|
||||||
|
def _compute_has_confirmed_mo(self):
|
||||||
|
"""True if this part is referenced by at least one non-draft MO.
|
||||||
|
|
||||||
|
Trace: fp.part.catalog → sale.order.line (x_fc_part_catalog_id)
|
||||||
|
→ sale.order → mrp.production (via origin name match).
|
||||||
|
Cheap: two bounded search_counts. Kept store=False so MO state
|
||||||
|
changes don't write-amplify through every part record.
|
||||||
|
"""
|
||||||
|
SO = self.env['sale.order']
|
||||||
|
MO = self.env['mrp.production']
|
||||||
|
live_states = ('confirmed', 'progress', 'to_close', 'done')
|
||||||
|
for part in self:
|
||||||
|
if not part.id:
|
||||||
|
part.x_fc_has_confirmed_mo = False
|
||||||
|
continue
|
||||||
|
so_names = SO.search([
|
||||||
|
('order_line.x_fc_part_catalog_id', '=', part.id),
|
||||||
|
('state', 'in', ('sale', 'done')),
|
||||||
|
]).mapped('name')
|
||||||
|
if not so_names:
|
||||||
|
part.x_fc_has_confirmed_mo = False
|
||||||
|
continue
|
||||||
|
part.x_fc_has_confirmed_mo = bool(MO.search_count([
|
||||||
|
('origin', 'in', so_names),
|
||||||
|
('state', 'in', live_states),
|
||||||
|
]))
|
||||||
|
|
||||||
|
@api.depends('partner_id.x_fc_contract_review_required',
|
||||||
|
'x_fc_contract_review_dismissed',
|
||||||
|
'x_fc_contract_review_state',
|
||||||
|
'x_fc_has_confirmed_mo')
|
||||||
|
def _compute_contract_review_banner_visible(self):
|
||||||
|
for part in self:
|
||||||
|
needs = part.partner_id.x_fc_contract_review_required
|
||||||
|
dismissed = part.x_fc_contract_review_dismissed
|
||||||
|
completed = (part.x_fc_contract_review_state == 'complete')
|
||||||
|
in_production = part.x_fc_has_confirmed_mo
|
||||||
|
part.x_fc_contract_review_banner_visible = (
|
||||||
|
bool(needs) and not dismissed
|
||||||
|
and not completed and not in_production
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- Actions -------------------------------------------------------------
|
||||||
|
|
||||||
|
def action_start_contract_review(self):
|
||||||
|
"""Create (if missing) + open the contract review for this part."""
|
||||||
|
self.ensure_one()
|
||||||
|
review = self.x_fc_contract_review_id
|
||||||
|
if not review:
|
||||||
|
review = self.env['fp.contract.review'].create({
|
||||||
|
'part_id': self.id,
|
||||||
|
'state': 'assistant_review',
|
||||||
|
})
|
||||||
|
self.x_fc_contract_review_id = review.id
|
||||||
|
self.x_fc_contract_review_dismissed = False
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'fp.contract.review',
|
||||||
|
'res_id': review.id,
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'current',
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_dismiss_contract_review(self):
|
||||||
|
"""Hide the reminder banner. Reversible by a Plating Manager."""
|
||||||
|
for part in self:
|
||||||
|
part.x_fc_contract_review_dismissed = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_undismiss_contract_review(self):
|
||||||
|
"""Restore the reminder banner. Manager only."""
|
||||||
|
if not self.env.user.has_group(
|
||||||
|
'fusion_plating.group_fusion_plating_manager'):
|
||||||
|
raise UserError(_(
|
||||||
|
'Only a Plating Manager can re-enable a dismissed '
|
||||||
|
'Contract Review reminder.'
|
||||||
|
))
|
||||||
|
for part in self:
|
||||||
|
part.x_fc_contract_review_dismissed = False
|
||||||
|
return True
|
||||||
51
fusion_plating/fusion_plating_quality/models/res_company.py
Normal file
51
fusion_plating/fusion_plating_quality/models/res_company.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
# Contract Review signer rosters. Company-scoped so multi-tenant
|
||||||
|
# Odoo deployments keep their own QA team isolated. Managed from
|
||||||
|
# Settings → Fusion Plating → Contract Review. Stored here (not
|
||||||
|
# as groups) so granting the right to sign a Contract Review does
|
||||||
|
# not cascade any other permission elsewhere in Odoo.
|
||||||
|
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 of this list.',
|
||||||
|
)
|
||||||
|
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 of '
|
||||||
|
'this list.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fp_get_qa_signers(self, section):
|
||||||
|
"""Return effective signer roster for Section 2.0 or 3.0.
|
||||||
|
|
||||||
|
Central helper so Sub 6 (per-customer / contact profiles) can
|
||||||
|
layer overrides without touching the sign action call sites.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if section == 20:
|
||||||
|
return self.x_fc_qa_assistant_user_ids
|
||||||
|
if section == 30:
|
||||||
|
return self.x_fc_qa_manager_user_ids
|
||||||
|
return self.env['res.users']
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
# Mirror the res.company fields so the admin can manage them from
|
||||||
|
# Settings → Fusion Plating → Contract Review without navigating
|
||||||
|
# into the company record.
|
||||||
|
x_fc_qa_assistant_user_ids = fields.Many2many(
|
||||||
|
related='company_id.x_fc_qa_assistant_user_ids',
|
||||||
|
readonly=False,
|
||||||
|
string='QA Assistant Signers',
|
||||||
|
)
|
||||||
|
x_fc_qa_manager_user_ids = fields.Many2many(
|
||||||
|
related='company_id.x_fc_qa_manager_user_ids',
|
||||||
|
readonly=False,
|
||||||
|
string='QA Manager Signers',
|
||||||
|
)
|
||||||
19
fusion_plating/fusion_plating_quality/models/res_partner.py
Normal file
19
fusion_plating/fusion_plating_quality/models/res_partner.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartner(models.Model):
|
||||||
|
_inherit = '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 remains '
|
||||||
|
'fully optional — the reminder can be dismissed and never '
|
||||||
|
'blocks production.',
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
@@ -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 > 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>
|
||||||
|
      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>
|
||||||
|
  
|
||||||
|
<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>
|
||||||
@@ -19,4 +19,12 @@
|
|||||||
groups, so a single user role works across the core and the QMS.
|
groups, so a single user role works across the core and the QMS.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!-- Multi-company rule for Contract Reviews (Sub 4). -->
|
||||||
|
<record id="fp_contract_review_company_rule" model="ir.rule">
|
||||||
|
<field name="name">Fusion Plating: Contract Review — multi-company</field>
|
||||||
|
<field name="model_id" ref="model_fp_contract_review"/>
|
||||||
|
<field name="global" eval="True"/>
|
||||||
|
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -29,3 +29,6 @@ access_fp_doc_control_manager,fp.doc.control.manager,model_fusion_plating_doc_co
|
|||||||
access_fp_quality_hold_operator,fp.quality.hold.operator,model_fusion_plating_quality_hold,fusion_plating.group_fusion_plating_operator,1,0,1,0
|
access_fp_quality_hold_operator,fp.quality.hold.operator,model_fusion_plating_quality_hold,fusion_plating.group_fusion_plating_operator,1,0,1,0
|
||||||
access_fp_quality_hold_supervisor,fp.quality.hold.supervisor,model_fusion_plating_quality_hold,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
access_fp_quality_hold_supervisor,fp.quality.hold.supervisor,model_fusion_plating_quality_hold,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||||
access_fp_quality_hold_manager,fp.quality.hold.manager,model_fusion_plating_quality_hold,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
access_fp_quality_hold_manager,fp.quality.hold.manager,model_fusion_plating_quality_hold,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||||
|
access_fp_contract_review_operator,fp.contract.review.operator,model_fp_contract_review,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||||
|
access_fp_contract_review_supervisor,fp.contract.review.supervisor,model_fp_contract_review,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||||
|
access_fp_contract_review_manager,fp.contract.review.manager,model_fp_contract_review,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||||
|
|||||||
|
@@ -0,0 +1,270 @@
|
|||||||
|
<?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 1:1 reproduction).
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- FORM VIEW — laid out to mirror QA-005 Rev. 0 as closely as Odoo -->
|
||||||
|
<!-- backend forms permit. Printed PDF is the 1:1 artefact; this form -->
|
||||||
|
<!-- is the data-entry counterpart. -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<record id="view_fp_contract_review_form" model="ir.ui.view">
|
||||||
|
<field name="name">fp.contract.review.form</field>
|
||||||
|
<field name="model">fp.contract.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Contract Review (QA-005)">
|
||||||
|
<header>
|
||||||
|
<button name="action_sign_section_20" type="object"
|
||||||
|
string="Sign Section 2.0"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="s20_locked or state == 'dismissed'"/>
|
||||||
|
<button name="action_sign_section_30" type="object"
|
||||||
|
string="Sign Section 3.0"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="s30_locked or not s20_locked or state == 'dismissed'"/>
|
||||||
|
<button name="action_print_qa005" type="object"
|
||||||
|
string="Print QA-005"
|
||||||
|
class="btn-secondary"/>
|
||||||
|
<button name="action_reopen" type="object"
|
||||||
|
string="Re-open"
|
||||||
|
confirm="This will clear both signatures and reset the review. Continue?"
|
||||||
|
groups="fusion_plating.group_fusion_plating_manager"
|
||||||
|
invisible="state != 'complete'"/>
|
||||||
|
<button name="action_dismiss" type="object"
|
||||||
|
string="Dismiss"
|
||||||
|
invisible="state in ('complete','dismissed')"/>
|
||||||
|
<field name="state" widget="statusbar"
|
||||||
|
statusbar_visible="draft,assistant_review,manager_review,complete"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1><field name="name" readonly="1"/></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== HEADER (Customer, PO, Part, Qty, Due) ===== -->
|
||||||
|
<group>
|
||||||
|
<group string="Customer & Order">
|
||||||
|
<field name="customer_id" readonly="1"/>
|
||||||
|
<field name="contract_po_number"/>
|
||||||
|
<field name="quote_or_job_number"/>
|
||||||
|
<field name="date_received"/>
|
||||||
|
</group>
|
||||||
|
<group string="Part">
|
||||||
|
<field name="part_id" readonly="1"/>
|
||||||
|
<field name="part_number" readonly="1"/>
|
||||||
|
<field name="part_revision" readonly="1"/>
|
||||||
|
<field name="qty"/>
|
||||||
|
<field name="due_date"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<!-- ========= SECTION 2.0 — Planning / Production Review ========= -->
|
||||||
|
<page string="2.0 Planning / Production Review"
|
||||||
|
name="section_20">
|
||||||
|
<group>
|
||||||
|
<field name="s20_locked" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
<group col="4" string="Checklist">
|
||||||
|
<field name="s20_acceptable_lead_time" readonly="s20_locked"/>
|
||||||
|
<field name="s20_capacity_to_process" readonly="s20_locked"/>
|
||||||
|
<field name="s20_skills_to_process" readonly="s20_locked"/>
|
||||||
|
<field name="s20_fixtures_required" readonly="s20_locked"/>
|
||||||
|
<field name="s20_prime_approvals" readonly="s20_locked"/>
|
||||||
|
<field name="s20_pricing" readonly="s20_locked"/>
|
||||||
|
<field name="s20_approved_technique" readonly="s20_locked"/>
|
||||||
|
<field name="s20_drawings_available" readonly="s20_locked"/>
|
||||||
|
<field name="s20_process_type_class_grade" readonly="s20_locked"/>
|
||||||
|
<field name="s20_pre_post_processing_steps" readonly="s20_locked"/>
|
||||||
|
</group>
|
||||||
|
<group string="Outcome">
|
||||||
|
<field name="s20_accepted" readonly="s20_locked"/>
|
||||||
|
<field name="s20_evaluate_risk" readonly="s20_locked"/>
|
||||||
|
<field name="s20_risk_level" readonly="s20_locked"
|
||||||
|
invisible="not s20_evaluate_risk"/>
|
||||||
|
</group>
|
||||||
|
<group string="Comments">
|
||||||
|
<field name="s20_comments" nolabel="1" readonly="s20_locked"/>
|
||||||
|
</group>
|
||||||
|
<group string="Signature" invisible="not s20_locked">
|
||||||
|
<field name="s20_signed_by" readonly="1"/>
|
||||||
|
<field name="s20_signed_date" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<!-- ========= SECTION 3.0 — Quality Review ========= -->
|
||||||
|
<page string="3.0 Quality Review" name="section_30">
|
||||||
|
<group>
|
||||||
|
<field name="s30_locked" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
<group col="4" string="Checklist">
|
||||||
|
<field name="s30_source_control_docs" readonly="s30_locked"/>
|
||||||
|
<field name="s30_quality_clauses_supplied" readonly="s30_locked"/>
|
||||||
|
<field name="s30_quality_clauses_attainable" readonly="s30_locked"/>
|
||||||
|
<field name="s30_critical_tolerance" readonly="s30_locked"/>
|
||||||
|
<field name="s30_measuring_tooling" readonly="s30_locked"/>
|
||||||
|
<field name="s30_quality_tests_verified" readonly="s30_locked"/>
|
||||||
|
<field name="s30_specification_revisions" readonly="s30_locked"/>
|
||||||
|
<field name="s30_certifications_requirements" readonly="s30_locked"/>
|
||||||
|
<field name="s30_psd_rfd_reviewed" readonly="s30_locked"/>
|
||||||
|
<field name="s30_specification_deviations" readonly="s30_locked"/>
|
||||||
|
<field name="s30_design_authority" readonly="s30_locked"/>
|
||||||
|
</group>
|
||||||
|
<group string="Outcome">
|
||||||
|
<field name="s30_accepted" readonly="s30_locked"/>
|
||||||
|
<field name="s30_evaluate_risk" readonly="s30_locked"/>
|
||||||
|
</group>
|
||||||
|
<group string="Risk Assessment"
|
||||||
|
invisible="not s30_evaluate_risk">
|
||||||
|
<field name="s30_risk_consequence" readonly="s30_locked"/>
|
||||||
|
<field name="s30_risk_likelihood" readonly="s30_locked"/>
|
||||||
|
<field name="s30_risk_band" readonly="1"
|
||||||
|
decoration-success="s30_risk_band == 'green'"
|
||||||
|
decoration-warning="s30_risk_band == 'yellow'"
|
||||||
|
decoration-danger="s30_risk_band == 'red'"
|
||||||
|
widget="badge"/>
|
||||||
|
<field name="s30_mitigation_plan_required" readonly="s30_locked"/>
|
||||||
|
</group>
|
||||||
|
<group string="Signature" invisible="not s30_locked">
|
||||||
|
<field name="s30_signed_by" readonly="1"/>
|
||||||
|
<field name="s30_signed_date" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- LIST VIEW -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<record id="view_fp_contract_review_list" model="ir.ui.view">
|
||||||
|
<field name="name">fp.contract.review.list</field>
|
||||||
|
<field name="model">fp.contract.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Contract Reviews"
|
||||||
|
decoration-success="state == 'complete'"
|
||||||
|
decoration-muted="state == 'dismissed'"
|
||||||
|
decoration-info="state == 'manager_review'"
|
||||||
|
decoration-warning="state == 'assistant_review'">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="customer_id"/>
|
||||||
|
<field name="part_number"/>
|
||||||
|
<field name="part_revision"/>
|
||||||
|
<field name="date_received"/>
|
||||||
|
<field name="s20_signed_by"/>
|
||||||
|
<field name="s30_signed_by"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- KANBAN VIEW (grouped by state) -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<record id="view_fp_contract_review_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">fp.contract.review.kanban</field>
|
||||||
|
<field name="model">fp.contract.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban default_group_by="state"
|
||||||
|
class="o_kanban_small_column">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="customer_id"/>
|
||||||
|
<field name="part_number"/>
|
||||||
|
<field name="part_revision"/>
|
||||||
|
<field name="state"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="card">
|
||||||
|
<div class="oe_kanban_global_click">
|
||||||
|
<div class="o_kanban_record_title">
|
||||||
|
<strong><field name="name"/></strong>
|
||||||
|
</div>
|
||||||
|
<div class="o_kanban_record_subtitle">
|
||||||
|
<field name="customer_id"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Part: </span>
|
||||||
|
<field name="part_number"/>
|
||||||
|
<span> Rev </span>
|
||||||
|
<field name="part_revision"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- SEARCH VIEW -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<record id="view_fp_contract_review_search" model="ir.ui.view">
|
||||||
|
<field name="name">fp.contract.review.search</field>
|
||||||
|
<field name="model">fp.contract.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="customer_id"/>
|
||||||
|
<field name="part_number"/>
|
||||||
|
<field name="part_revision"/>
|
||||||
|
<field name="contract_po_number"/>
|
||||||
|
<field name="quote_or_job_number"/>
|
||||||
|
<separator/>
|
||||||
|
<filter name="filter_pending"
|
||||||
|
string="Pending"
|
||||||
|
domain="[('state','in',['draft','assistant_review','manager_review'])]"/>
|
||||||
|
<filter name="filter_complete"
|
||||||
|
string="Complete"
|
||||||
|
domain="[('state','=','complete')]"/>
|
||||||
|
<filter name="filter_dismissed"
|
||||||
|
string="Dismissed"
|
||||||
|
domain="[('state','=','dismissed')]"/>
|
||||||
|
<group>
|
||||||
|
<filter name="group_state" string="Status"
|
||||||
|
context="{'group_by': 'state'}"/>
|
||||||
|
<filter name="group_customer" string="Customer"
|
||||||
|
context="{'group_by': 'customer_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- ACTION -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<record id="action_fp_contract_review" model="ir.actions.act_window">
|
||||||
|
<field name="name">Contract Reviews</field>
|
||||||
|
<field name="res_model">fp.contract.review</field>
|
||||||
|
<field name="view_mode">kanban,list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_fp_contract_review_search"/>
|
||||||
|
<field name="context">{'search_default_filter_pending': 1}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No Contract Reviews yet.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Contract Reviews (QA-005) capture QA sign-off on new parts
|
||||||
|
before production. They are always optional and never
|
||||||
|
block manufacturing. Toggle
|
||||||
|
<b>Require Contract Review for new parts</b> on a customer
|
||||||
|
to show the reminder banner on new parts under that customer.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- MENU (under Plating → Quality) -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<menuitem id="menu_fp_quality_contract_review"
|
||||||
|
name="Contract Reviews"
|
||||||
|
parent="menu_fp_quality"
|
||||||
|
action="action_fp_contract_review"
|
||||||
|
sequence="15"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?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 banner + tab on fp.part.catalog.
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_fp_part_catalog_form_contract_review" model="ir.ui.view">
|
||||||
|
<field name="name">fp.part.catalog.form.contract.review</field>
|
||||||
|
<field name="model">fp.part.catalog</field>
|
||||||
|
<field name="inherit_id" ref="fusion_plating_configurator.view_fp_part_catalog_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<!-- ===== Reminder banner above the title block ===== -->
|
||||||
|
<xpath expr="//div[hasclass('oe_title')]" position="before">
|
||||||
|
<field name="x_fc_contract_review_banner_visible" invisible="1"/>
|
||||||
|
<field name="x_fc_contract_review_state" invisible="1"/>
|
||||||
|
<field name="x_fc_contract_review_dismissed" invisible="1"/>
|
||||||
|
<div class="alert alert-info"
|
||||||
|
role="alert"
|
||||||
|
invisible="not x_fc_contract_review_banner_visible">
|
||||||
|
<i class="fa fa-info-circle me-2"/>
|
||||||
|
<strong>New part created.</strong>
|
||||||
|
Please complete the Contract Review (QA-005) if applicable.
|
||||||
|
This is optional — it can be dismissed and never blocks production.
|
||||||
|
<div class="mt-2">
|
||||||
|
<button name="action_start_contract_review"
|
||||||
|
type="object"
|
||||||
|
string="Start Contract Review"
|
||||||
|
class="btn btn-primary btn-sm me-2"/>
|
||||||
|
<button name="action_dismiss_contract_review"
|
||||||
|
type="object"
|
||||||
|
string="Dismiss"
|
||||||
|
class="btn btn-link btn-sm"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- ===== Contract Review tab at end of notebook ===== -->
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Contract Review" name="contract_review">
|
||||||
|
<group>
|
||||||
|
<field name="x_fc_contract_review_id" readonly="1"/>
|
||||||
|
<field name="x_fc_contract_review_state" readonly="1"
|
||||||
|
widget="badge"
|
||||||
|
decoration-success="x_fc_contract_review_state == 'complete'"
|
||||||
|
decoration-info="x_fc_contract_review_state == 'manager_review'"
|
||||||
|
decoration-warning="x_fc_contract_review_state == 'assistant_review'"
|
||||||
|
decoration-muted="x_fc_contract_review_state == 'dismissed'"
|
||||||
|
invisible="not x_fc_contract_review_id"/>
|
||||||
|
</group>
|
||||||
|
<div invisible="x_fc_contract_review_id or not partner_id">
|
||||||
|
<field name="x_fc_customer_requires_contract_review" invisible="1"/>
|
||||||
|
<p invisible="x_fc_customer_requires_contract_review">
|
||||||
|
This customer is not configured to require
|
||||||
|
Contract Reviews on new parts. You can still
|
||||||
|
start one manually.
|
||||||
|
</p>
|
||||||
|
<button name="action_start_contract_review"
|
||||||
|
type="object"
|
||||||
|
string="Start Contract Review"
|
||||||
|
class="btn btn-primary"/>
|
||||||
|
</div>
|
||||||
|
<div invisible="not x_fc_contract_review_id">
|
||||||
|
<button name="action_start_contract_review"
|
||||||
|
type="object"
|
||||||
|
string="Open Review"
|
||||||
|
class="btn btn-secondary me-2"/>
|
||||||
|
<button name="action_undismiss_contract_review"
|
||||||
|
type="object"
|
||||||
|
string="Undismiss Reminder"
|
||||||
|
class="btn btn-link"
|
||||||
|
groups="fusion_plating.group_fusion_plating_manager"
|
||||||
|
invisible="not x_fc_contract_review_dismissed"/>
|
||||||
|
</div>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?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 signer rosters in Fusion Plating settings.
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="res_config_settings_view_form_fp_quality" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.fp.quality</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="inherit_id"
|
||||||
|
ref="fusion_plating.res_config_settings_view_form_fp_core"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//app[@name='fusion_plating']" position="inside">
|
||||||
|
<block title="Contract Review (QA-005)"
|
||||||
|
name="fp_contract_review_settings"
|
||||||
|
help="Choose which users are authorised to sign each
|
||||||
|
section of a Contract Review. Users on these
|
||||||
|
lists gain only the right to sign — no other
|
||||||
|
permissions are affected. Plating Managers can
|
||||||
|
always sign regardless of these lists.">
|
||||||
|
<setting id="fp_qa_assistant_signers"
|
||||||
|
string="QA Assistant Signers"
|
||||||
|
help="Users authorised to sign Section 2.0
|
||||||
|
(Planning / Production Review). Typically
|
||||||
|
one or two people. Leave empty if any
|
||||||
|
user should be able to sign — in that
|
||||||
|
case a Plating Manager is the only
|
||||||
|
required approver.">
|
||||||
|
<field name="x_fc_qa_assistant_user_ids"
|
||||||
|
widget="many2many_tags"/>
|
||||||
|
</setting>
|
||||||
|
<setting id="fp_qa_manager_signers"
|
||||||
|
string="QA Manager Signers"
|
||||||
|
help="Users authorised to sign Section 3.0
|
||||||
|
(Quality Review). Typically the QA
|
||||||
|
Manager for the shop.">
|
||||||
|
<field name="x_fc_qa_manager_user_ids"
|
||||||
|
widget="many2many_tags"/>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?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 toggle on customer.
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_partner_form_fp_contract_review" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.form.fp.contract.review</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Plating Contract Review"
|
||||||
|
name="fp_contract_review_prefs"
|
||||||
|
invisible="is_company != True">
|
||||||
|
<group name="fp_contract_review_group">
|
||||||
|
<p class="text-muted" colspan="2">
|
||||||
|
Toggle this on so QA sees a Contract Review
|
||||||
|
reminder banner whenever a new part is created
|
||||||
|
under this customer. The reminder is always
|
||||||
|
optional — it can be dismissed and never blocks
|
||||||
|
production. Once the part reaches a confirmed
|
||||||
|
manufacturing order, the banner disappears on
|
||||||
|
its own.
|
||||||
|
</p>
|
||||||
|
<field name="x_fc_contract_review_required"
|
||||||
|
widget="boolean_toggle"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user