fix(sub12c+): close 3 known gaps — rack travel ticket, cert statement, CoC actuals

Gap 1 — Rack Travel Ticket PDF (Sub 12b's Save+Print 404):
  + report_fp_rack_travel.xml in fusion_plating_reports — A5 landscape
    single page, big rack name, Code 128 of FP-RACK:<name>, tag chips,
    contained part-batches table.
  + ir.actions.report bound to fusion.plating.rack so it appears in
    the rack form's Print menu too.
  + Sub 12b's rack_parts_dialog.js Save+Print URL fixed to use the
    standard /report/pdf/<xmlid>/<id> route.

Gap 2 — Per-customer cert statement:
  + res.company.x_fc_default_cert_statement (company-level fallback).
  + res.partner.x_fc_cert_statement (per-customer override).
  + Surfaced on the partner form under the existing Cert + Document
    Routing block.
  + Chronological CoC body resolves: customer override → company
    default → hardcoded AS9100/ISO 9001 boilerplate. Three-tier
    fallback so existing certs without overrides keep working.

Gap 3 — Chronological CoC 'Actual' column:
  + Build a captured_values_by_input dict from the move's
    transition_input_value_ids (Sub 12b captures these on every
    Move Parts commit).
  + Render typed Actual: text → as-is, number → with target unit,
    boolean → PASS/FAIL, date → formatted, attachment → '[Attachment]'
    placeholder.
  + Falls back to prompts from the destination step's step_input list
    when no values were captured (still useful as audit-of-what-was-
    asked even if blank).

Version bumps:
  fusion_plating → 19.0.10.3.0
  fusion_plating_reports → 19.0.10.1.0
  fusion_plating_certificates → 19.0.5.3.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-27 21:55:48 -04:00
parent 504c8f34db
commit 7d3b8f132a
9 changed files with 224 additions and 17 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating',
'version': '19.0.10.2.0',
'version': '19.0.10.3.0',
'category': 'Manufacturing/Plating',
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
'description': """

View File

@@ -173,3 +173,15 @@ class ResCompany(models.Model):
'recipe with preferred_editor=auto is selected. Per-recipe '
'preferred_editor (tree/simple) overrides this.',
)
# =====================================================================
# Sub 12c+ — Default Certification Statement
# =====================================================================
x_fc_default_cert_statement = fields.Text(
string='Default Cert Statement',
help='Boilerplate text printed in the Certificate of Conformance '
'"Certification Statement" block. Per-customer override on '
'res.partner.x_fc_cert_statement takes precedence when set. '
'When BOTH are blank the report falls back to a hardcoded '
'AS9100/ISO 9001 statement.',
)

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Certificates',
'version': '19.0.5.2.0',
'version': '19.0.5.3.0',
'category': 'Manufacturing/Plating',
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
'description': """

View File

@@ -87,3 +87,14 @@ class ResPartner(models.Model):
'use: a primary account-manager contact who wants full '
'visibility into everything the shop sends out.',
)
# ---- Sub 12c+ — Per-customer cert statement override ----------------
x_fc_cert_statement = fields.Text(
string='Cert Statement Override',
help='Override boilerplate text printed in the Certificate of '
'Conformance "Certification Statement" block. When blank, '
'falls back to the company default (res.company.'
'x_fc_default_cert_statement) and finally to a hardcoded '
'AS9100/ISO 9001 boilerplate. Useful for aerospace customers '
'who require specific NIST or DFARS language.',
)

View File

@@ -32,6 +32,17 @@
<field name="x_fc_send_bol" widget="boolean_toggle"/>
</group>
</group>
<separator string="Cert Statement Override (Sub 12c+)"/>
<p class="text-muted">
Boilerplate text printed in the "Certification Statement"
block on this customer's CoC. Leave blank to use the
company default, then a hardcoded AS9100/ISO 9001
statement.
</p>
<group>
<field name="x_fc_cert_statement" nolabel="1"
placeholder="e.g. We certify these parts conform to MIL-DTL-5541F Class 1A and have been processed in accordance with…"/>
</group>
</page>
</xpath>
</field>

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Reports',
'version': '19.0.10.0.0',
'version': '19.0.10.1.0',
'category': 'Manufacturing/Plating',
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
'depends': [
@@ -33,6 +33,7 @@
# Quality + compliance reports
'report/report_coc.xml',
'report/report_coc_chronological.xml',
'report/report_fp_rack_travel.xml',
'report/report_ncr.xml',
'report/report_capa.xml',
'report/report_bath_chemistry_log.xml',

View File

@@ -112,20 +112,45 @@
</t>
</div>
<!-- Measurement sub-table — only when the destination step has step_input prompts -->
<t t-if="captured">
<!-- Sub 12c+ — index captured values from the move's transition_input_value_ids
by node_input_id so we can render Actual alongside Target. -->
<t t-set="captured_values_by_input" t-value="{v.node_input_id.id: v for v in mv.transition_input_value_ids}"/>
<!-- Measurement sub-table — show whenever destination has any
step_input prompts OR the move recorded any captured values. -->
<t t-set="prompts" t-value="captured"/>
<t t-if="not prompts and mv.transition_input_value_ids">
<t t-set="prompts" t-value="mv.transition_input_value_ids.mapped('node_input_id')"/>
</t>
<t t-if="prompts">
<table class="bordered">
<thead>
<tr>
<th style="width: 24%;">Name</th>
<th style="width: 30%;">Description</th>
<th style="width: 28%;">Description</th>
<th style="width: 14%;">Target</th>
<th style="width: 18%;">Actual</th>
<th style="width: 20%;">Actual</th>
<th style="width: 14%;">Recorded By</th>
</tr>
</thead>
<tbody>
<t t-foreach="captured" t-as="inp">
<t t-foreach="prompts" t-as="inp">
<t t-set="cv" t-value="captured_values_by_input.get(inp.id)"/>
<t t-set="actual_str" t-value="''"/>
<t t-if="cv">
<t t-if="cv.value_text">
<t t-set="actual_str" t-value="cv.value_text"/>
</t>
<t t-elif="cv.value_number">
<t t-set="actual_str" t-value="('%s %s' % (cv.value_number, (inp.target_unit if 'target_unit' in inp._fields and inp.target_unit else ''))).strip()"/>
</t>
<t t-elif="cv.value_boolean is not False">
<t t-set="actual_str" t-value="'PASS' if cv.value_boolean else 'FAIL'"/>
</t>
<t t-elif="cv.value_date">
<t t-set="actual_str" t-value="cv.value_date.strftime('%Y-%m-%d %H:%M')"/>
</t>
</t>
<tr>
<td><span t-esc="inp.name"/></td>
<td>
@@ -144,7 +169,14 @@
<span t-esc="inp.target_unit"/>
</t>
</td>
<td/>
<td>
<t t-if="actual_str">
<strong t-esc="actual_str"/>
</t>
<t t-elif="cv and cv.value_attachment_id">
<span style="font-size: 7.5pt; color: #555;">[Attachment]</span>
</t>
</td>
<td>
<span t-esc="(mv.moved_by_user_id and mv.moved_by_user_id.name) or ''"/>
</td>
@@ -168,6 +200,11 @@
<t t-set="signature_img" t-value="company.x_fc_coc_signature_override or owner_sig"/>
<t t-set="signer_name" t-value="(doc.certified_by_id and doc.certified_by_id.name) or (company.x_fc_owner_user_id and company.x_fc_owner_user_id.name) or ''"/>
<!-- Sub 12c+ — cert statement: per-customer override → company default → hardcoded fallback -->
<t t-set="_cust_stmt" t-value="('x_fc_cert_statement' in doc.partner_id._fields and doc.partner_id.x_fc_cert_statement) or False"/>
<t t-set="_co_stmt" t-value="('x_fc_default_cert_statement' in company._fields and company.x_fc_default_cert_statement) or False"/>
<t t-set="cert_statement" t-value="_cust_stmt or _co_stmt or 'We certify that the parts listed above have been processed in accordance with the specifications referenced and that all required tests have been performed. Records on file at our facility per AS9100 / ISO 9001 retention policy.'"/>
<table class="bordered">
<tr>
<td style="width: 50%; vertical-align: top;">
@@ -183,12 +220,8 @@
<span style="font-size: 8.5pt;">
Ref. WO# <span t-esc="(job and job.name) or ''"/>
</span>
<p style="font-size: 8pt; margin-top: 4px;">
We certify that the parts listed above have been processed
in accordance with the specifications referenced and that
all required tests have been performed. Records on file at
our facility per AS9100 / ISO 9001 retention policy.
</p>
<p style="font-size: 8pt; margin-top: 4px; white-space: pre-wrap;"
t-esc="cert_statement"/>
</td>
</tr>
</table>

View File

@@ -0,0 +1,138 @@
<?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 12c+ — Rack Travel Ticket.
Closes the gap left by Sub 12b's Rack Parts dialog 'Save + Print'
button. Operator presses Save + Print → tablet opens
/web/report/pdf/fp.rack.travel/<rack_id> in a new tab → this report
renders.
Single-page A5 landscape, large fonts, big QR/Code128 barcode.
Designed to be physically attached to the rack itself so
downstream operators can scan and pull up the rack's parts list.
-->
<odoo>
<record id="paperformat_fp_rack_travel" model="report.paperformat">
<field name="name">FP Rack Travel — A5 landscape</field>
<field name="format">A5</field>
<field name="orientation">Landscape</field>
<field name="margin_top">8</field>
<field name="margin_bottom">8</field>
<field name="margin_left">8</field>
<field name="margin_right">8</field>
<field name="header_spacing">5</field>
<field name="dpi">90</field>
</record>
<record id="action_report_fp_rack_travel" model="ir.actions.report">
<field name="name">Rack Travel Ticket</field>
<field name="model">fusion.plating.rack</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_rack_travel_template</field>
<field name="report_file">fusion_plating_reports.report_fp_rack_travel_template</field>
<field name="print_report_name">'Rack-Travel-%s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="fusion_plating.model_fusion_plating_rack"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_rack_travel"/>
</record>
<template id="report_fp_rack_travel_template">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="rack">
<t t-call="web.basic_layout">
<t t-set="batches" t-value="env['fp.job.step'].search([('rack_id','=',rack.id)])"/>
<div class="page fp-rack-travel">
<style>
.fp-rack-travel { font-family: Arial, sans-serif; font-size: 11pt; color: #000; padding-top: 4mm; }
.fp-rack-travel h1 { font-size: 28pt; margin: 0; font-weight: bold; }
.fp-rack-travel .fp-rack-id { font-size: 36pt; font-weight: bold; }
.fp-rack-travel table.bordered,
.fp-rack-travel table.bordered th,
.fp-rack-travel table.bordered td { border: 2px solid #000; border-collapse: collapse; }
.fp-rack-travel table.bordered { width: 100%; margin-top: 6px; }
.fp-rack-travel table.bordered th { background: #ededed; padding: 4px 8px; font-size: 11pt; text-align: left; }
.fp-rack-travel table.bordered td { padding: 6px 8px; vertical-align: top; font-size: 11pt; }
.fp-rack-travel .fp-tag-chip { display: inline-block; padding: 2px 8px; margin-right: 4px;
border-radius: 999px; border: 1px solid #000; font-size: 10pt; font-weight: bold; }
</style>
<table style="width: 100%;">
<tr>
<td style="width: 60%; vertical-align: middle;">
<div style="font-size: 10pt; color: #666;">RACK TRAVEL TICKET</div>
<h1>Rack <span class="fp-rack-id" t-esc="rack.name"/></h1>
<div style="font-size: 10pt; margin-top: 4px;">
<strong>Type:</strong> <span t-esc="rack.rack_type"/>
&#160;·&#160;
<strong>State:</strong>
<t t-if="'racking_state' in rack._fields">
<span t-esc="dict(rack._fields['racking_state'].selection).get(rack.racking_state, rack.racking_state) or '—'"/>
</t>
<t t-else=""></t>
</div>
<div style="margin-top: 6px;">
<t t-if="'tag_ids' in rack._fields">
<span t-foreach="rack.tag_ids" t-as="tag" t-key="tag.id"
class="fp-tag-chip">
<t t-esc="tag.name"/>
</span>
</t>
</div>
</td>
<td style="width: 40%; vertical-align: middle; text-align: right;">
<img t-att-src="'/report/barcode/Code128/FP-RACK:%s?humanreadable=1' % rack.name"
style="height: 22mm;"/>
</td>
</tr>
</table>
<table class="bordered">
<thead>
<tr>
<th style="width: 10%;">Qty</th>
<th style="width: 22%;">Part Number</th>
<th style="width: 18%;">Work Order</th>
<th style="width: 30%;">Customer</th>
<th style="width: 20%;">Current Step</th>
</tr>
</thead>
<tbody>
<t t-if="batches">
<tr t-foreach="batches" t-as="b">
<td><span t-esc="(b.qty_done or 0) - (b.qty_scrapped or 0)"/></td>
<td>
<t t-if="b.job_id and b.job_id.product_id">
<span t-esc="b.job_id.product_id.default_code or b.job_id.product_id.name or '—'"/>
</t>
<t t-else=""></t>
</td>
<td><span t-esc="(b.job_id and b.job_id.name) or '—'"/></td>
<td><span t-esc="(b.job_id and b.job_id.partner_id and b.job_id.partner_id.name) or '—'"/></td>
<td><span t-esc="b.name or '—'"/></td>
</tr>
</t>
<t t-else="">
<tr>
<td colspan="5" style="text-align: center; color: #888;">
No part batches currently on this rack.
</td>
</tr>
</t>
</tbody>
</table>
<div style="margin-top: 8px; font-size: 9pt; color: #666;">
Printed <span t-esc="datetime.datetime.now().strftime('%Y-%m-%d %H:%M')"/>
&#160;·&#160; Operator scans FP-RACK:<span t-esc="rack.name"/> at any tablet to load this rack.
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -76,9 +76,10 @@ export class FpRackPartsDialog extends Component {
}
this.props.close();
if (printAfter) {
// Sub 12c report — until it ships, this returns 404.
// Sub 12c+ rack travel ticket
// (fusion_plating_reports.action_report_fp_rack_travel).
window.open(
`/web/report/pdf/fp.rack.travel/${this.state.selectedRackId}`,
`/report/pdf/fusion_plating_reports.action_report_fp_rack_travel/${this.state.selectedRackId}`,
"_blank",
);
}