feat(sub12c): chronological CoC body + body_style opt-in router (Task 3)
New template: fusion_plating_reports.coc_body_chronological.
Walks fp.job.step.move records in time order (chain-of-custody).
Per-move heading 'Step Name (Tank Code)' with 'Moved By / Time / Qty'
meta line + a 5-column measurement sub-table (Name / Description /
Target / Actual / Recorded By) when the destination step has captured
inputs. Heading-only when there are no inputs (gating moves).
New router template: coc_body_router. Picks chronological vs classic
based on fp.certificate.body_style. Existing certs default to 'classic'
so no regressions. Both English + French CoC templates rerouted.
fp.certificate.body_style ('classic' | 'chronological') exposed on
the cert form alongside certified_by_id. Operator picks per cert.
Sign-off block reuses the existing owner_user_id signature pattern +
x_fc_coc_signature_override fallback. Cert statement boilerplate is
inline (Sub 12d will move it to a configurable per-customer field).
The Actual column in the measurement sub-table is rendered blank
because Sub 12a/12b runtime captures step_input values via the
operator's per-step input form which lives in a model not yet wired
into this template — Sub 12d follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,18 @@ class FpCertificate(models.Model):
|
||||
certified_by_id = fields.Many2one(
|
||||
'res.users', string='Certified By', help='Signing authority (e.g. Quality Manager).',
|
||||
)
|
||||
|
||||
# ===== Sub 12c — chronological CoC opt-in ===============================
|
||||
body_style = fields.Selection(
|
||||
[
|
||||
('classic', 'Classic (recipe-order)'),
|
||||
('chronological', 'Chronological (chain-of-custody)'),
|
||||
],
|
||||
string='CoC Body Style', default='classic',
|
||||
help='Chronological walks fp.job.step.move records in time order '
|
||||
'with measurement sub-tables per move, matching Steelhead\'s '
|
||||
'CoC PDF layout. Classic uses the existing recipe-order body.',
|
||||
)
|
||||
issue_date = fields.Date(string='Issue Date', default=fields.Date.today, tracking=True)
|
||||
attachment_id = fields.Many2one('ir.attachment', string='Certificate PDF')
|
||||
thickness_reading_ids = fields.One2many(
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
<group>
|
||||
<field name="issued_by_id"/>
|
||||
<field name="certified_by_id"/>
|
||||
<field name="body_style"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="reading_count" readonly="1"/>
|
||||
|
||||
@@ -307,7 +307,8 @@
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="LANG" t-value="'en'"/>
|
||||
<div class="page">
|
||||
<t t-call="fusion_plating_reports.coc_body"/>
|
||||
<!-- Sub 12c — router picks chronological vs classic body -->
|
||||
<t t-call="fusion_plating_reports.coc_body_router"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
@@ -324,7 +325,8 @@
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="LANG" t-value="'fr'"/>
|
||||
<div class="page">
|
||||
<t t-call="fusion_plating_reports.coc_body"/>
|
||||
<!-- Sub 12c — router picks chronological vs classic body -->
|
||||
<t t-call="fusion_plating_reports.coc_body_router"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<?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 — Chronological CoC body.
|
||||
Walks fp.job.step.move records in time order (chain-of-custody),
|
||||
rendering each transition as a heading ("Step Name (Tank Code)")
|
||||
with a "Moved By / Time" meta line + a 5-column measurement
|
||||
sub-table when the destination step has captured input values.
|
||||
Mirrors Steelhead's CoC PDF layout (screens 19-24).
|
||||
|
||||
Wired via fp.certificate.body_style = 'chronological' through the
|
||||
coc_body_router template.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<template id="coc_body_chronological">
|
||||
<!-- Resolve the linked job. fp.certificate has x_fc_job_id (Sub 11+). -->
|
||||
<t t-set="job" t-value="('x_fc_job_id' in doc._fields and doc.x_fc_job_id) or False"/>
|
||||
<t t-set="moves" t-value="(job and 'move_ids' in job._fields and job.move_ids.sorted('move_datetime')) or []"/>
|
||||
|
||||
<style>
|
||||
.fp-coc-chrono { font-family: Arial, sans-serif; font-size: 9pt; color: #000; padding-top: 8mm; }
|
||||
.fp-coc-chrono h1 { text-align: center; font-size: 18pt; margin: 0 0 6px 0; font-weight: bold; }
|
||||
.fp-coc-chrono h3 { font-size: 11pt; margin: 8px 0 2px 0; font-weight: bold; }
|
||||
.fp-coc-chrono .fp-chrono-meta { font-size: 8.5pt; color: #444; margin-bottom: 4px; }
|
||||
.fp-coc-chrono table.bordered,
|
||||
.fp-coc-chrono table.bordered th,
|
||||
.fp-coc-chrono table.bordered td { border: 1px solid #000; border-collapse: collapse; }
|
||||
.fp-coc-chrono table.bordered { width: 100%; margin-bottom: 8px; }
|
||||
.fp-coc-chrono table.bordered th { background: #ededed; padding: 4px 6px; font-size: 8.5pt; text-align: center; }
|
||||
.fp-coc-chrono table.bordered td { padding: 4px 6px; vertical-align: top; font-size: 8.5pt; }
|
||||
.fp-coc-chrono .text-center { text-align: center; }
|
||||
.fp-coc-chrono hr.heavy { border: 0; border-top: 2px solid #000; margin: 8px 0; }
|
||||
</style>
|
||||
|
||||
<div class="fp-coc-chrono">
|
||||
|
||||
<h1>Certificate of Conformance</h1>
|
||||
|
||||
<!-- Job header (compact) -->
|
||||
<table class="bordered">
|
||||
<tr>
|
||||
<th style="width: 18%;">Part Number</th>
|
||||
<th style="width: 28%;">Description</th>
|
||||
<th style="width: 8%;">Quantity</th>
|
||||
<th style="width: 8%;">Work Order</th>
|
||||
<th style="width: 14%;">PO Number</th>
|
||||
<th style="width: 14%;">Packing List No</th>
|
||||
<th style="width: 10%;">Date</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<t t-if="job and 'part_catalog_id' in job._fields and job.part_catalog_id">
|
||||
<span t-esc="job.part_catalog_id.part_number or job.product_id.default_code or '—'"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-esc="(job and job.product_id and job.product_id.default_code) or '—'"/>
|
||||
</t>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="job and 'part_catalog_id' in job._fields and job.part_catalog_id">
|
||||
<span t-esc="job.part_catalog_id.name or job.product_id.name or '—'"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-esc="(job and job.product_id and job.product_id.name) or '—'"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="(job and job.qty) or ''"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="(job and job.name) or '—'"/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-esc="(job and job.sale_order_id and job.sale_order_id.client_order_ref) or '—'"/>
|
||||
</td>
|
||||
<td/>
|
||||
<td>
|
||||
<span t-esc="(doc.create_date and doc.create_date.strftime('%Y-%m-%d')) or ''"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3 style="margin-top: 6px;">Specification(s):
|
||||
<span style="font-weight: normal;"
|
||||
t-esc="(job and job.recipe_id and job.recipe_id.name) or '—'"/>
|
||||
</h3>
|
||||
|
||||
<hr class="heavy"/>
|
||||
|
||||
<!-- Chain-of-custody walk -->
|
||||
<t t-foreach="moves" t-as="mv">
|
||||
<t t-set="dest" t-value="mv.to_step_id"/>
|
||||
<t t-set="tank_code" t-value="(mv.to_tank_id and mv.to_tank_id.code) or (dest and dest.tank_id and dest.tank_id.code) or ''"/>
|
||||
<t t-set="captured" t-value="(dest and dest.input_ids.filtered(lambda i: (i.kind or 'step_input') == 'step_input').sorted('sequence')) or []"/>
|
||||
|
||||
<h3>
|
||||
<span t-esc="(dest and dest.name) or '—'"/>
|
||||
<t t-if="tank_code"> (<span t-esc="tank_code"/>)</t>
|
||||
</h3>
|
||||
<div class="fp-chrono-meta">
|
||||
<strong>Moved By:</strong> <span t-esc="mv.moved_by_user_id.name"/>
|
||||
<span> · </span>
|
||||
<strong>Time:</strong>
|
||||
<span t-esc="mv.move_datetime and mv.move_datetime.strftime('%b %d, %Y %I:%M:%S %p') or ''"/>
|
||||
<t t-if="mv.qty_moved">
|
||||
<span> · </span>
|
||||
<strong>Qty:</strong> <span t-esc="mv.qty_moved"/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Measurement sub-table — only when the destination step has step_input prompts -->
|
||||
<t t-if="captured">
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 24%;">Name</th>
|
||||
<th style="width: 30%;">Description</th>
|
||||
<th style="width: 14%;">Target</th>
|
||||
<th style="width: 18%;">Actual</th>
|
||||
<th style="width: 14%;">Recorded By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="captured" t-as="inp">
|
||||
<tr>
|
||||
<td><span t-esc="inp.name"/></td>
|
||||
<td>
|
||||
<t t-if="inp.hint">
|
||||
<span t-esc="inp.hint"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="'target_min' in inp._fields and inp.target_min and inp.target_max">
|
||||
<span t-esc="inp.target_min"/>–<span t-esc="inp.target_max"/>
|
||||
<t t-if="'target_unit' in inp._fields and inp.target_unit">
|
||||
<span> </span><span t-esc="inp.target_unit"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="'target_unit' in inp._fields and inp.target_unit">
|
||||
<span t-esc="inp.target_unit"/>
|
||||
</t>
|
||||
</td>
|
||||
<td/>
|
||||
<td>
|
||||
<span t-esc="(mv.moved_by_user_id and mv.moved_by_user_id.name) or ''"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<hr class="heavy"/>
|
||||
|
||||
<!-- Sign-off block (re-uses owner_user_id signature pattern from coc_body) -->
|
||||
<t t-set="owner_sig" t-value="False"/>
|
||||
<t t-if="company.x_fc_owner_user_id">
|
||||
<t t-set="_emp" t-value="company.x_fc_owner_user_id.employee_ids[:1]"/>
|
||||
<t t-if="_emp and 'signature' in _emp._fields">
|
||||
<t t-set="owner_sig" t-value="_emp['signature']"/>
|
||||
</t>
|
||||
</t>
|
||||
<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 ''"/>
|
||||
|
||||
<table class="bordered">
|
||||
<tr>
|
||||
<td style="width: 50%; vertical-align: top;">
|
||||
<strong>Certified By:</strong><br/>
|
||||
<t t-if="signature_img">
|
||||
<img t-att-src="'data:image/png;base64,%s' % signature_img.decode()"
|
||||
style="max-height: 22mm; max-width: 70mm;"/>
|
||||
</t><br/>
|
||||
<strong>Name:</strong> <span t-esc="signer_name"/>
|
||||
</td>
|
||||
<td style="width: 50%; vertical-align: top;">
|
||||
<strong>Certification Statement:</strong>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ============================================================== -->
|
||||
<!-- Router — picks chronological vs classic body -->
|
||||
<!-- Wired into the existing CoC actions in report_coc.xml. -->
|
||||
<!-- ============================================================== -->
|
||||
<template id="coc_body_router">
|
||||
<t t-if="doc.body_style == 'chronological' and 'x_fc_job_id' in doc._fields and doc.x_fc_job_id">
|
||||
<t t-call="fusion_plating_reports.coc_body_chronological"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="fusion_plating_reports.coc_body"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user