This commit is contained in:
gsinghpal
2026-04-29 03:35:33 -04:00
parent 6ac6d24da6
commit a2fe1fcbcc
61 changed files with 4655 additions and 667 deletions

View File

@@ -18,11 +18,16 @@
<field name="name">FP Traveller — A4 landscape narrow margins</field>
<field name="format">A4</field>
<field name="orientation">Landscape</field>
<field name="margin_top">10</field>
<!-- margin_top + header_spacing both reserve room above the body
so the H1 / Item Information table doesn't ride into the
external_layout's company logo band. The screenshot showed
"Work Order / Bon de Travail" overlapping the ENTECH logo
with the prior 10 / 5 values; 28 / 22 buys ~1cm clear gap. -->
<field name="margin_top">28</field>
<field name="margin_bottom">10</field>
<field name="margin_left">8</field>
<field name="margin_right">8</field>
<field name="header_spacing">5</field>
<field name="header_spacing">22</field>
<field name="dpi">90</field>
</record>

View File

@@ -21,11 +21,17 @@
<field name="name">FP Work Order Detail — A4 portrait</field>
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<field name="margin_top">15</field>
<!-- margin_top + header_spacing both reserve room above the body
content. The external_layout puts the company logo + address
in that band; without enough space the header overlaps the
body's first line (the H1 on page 1, the Certified By table
on page 2). 35 / 28 puts a clean ~1cm clear gap below the
logo block. -->
<field name="margin_top">35</field>
<field name="margin_bottom">15</field>
<field name="margin_left">12</field>
<field name="margin_right">12</field>
<field name="header_spacing">8</field>
<field name="header_spacing">28</field>
<field name="dpi">90</field>
</record>
@@ -46,14 +52,42 @@
<t t-foreach="docs" t-as="job">
<t t-call="web.external_layout">
<t t-set="company" t-value="job.company_id"/>
<t t-set="moves" t-value="job.move_ids.sorted('move_datetime')"/>
<t t-set="so" t-value="job.sale_order_id"/>
<!-- All datetimes in Postgres are naive UTC. QWeb's
eval scope exposes neither pytz nor format_datetime,
so timestamp formatting happens via job.fp_format_local()
on the record itself — record methods are always
available in templates. The helper resolves user.tz
→ company.x_fc_default_tz → UTC. -->
<!-- First SO line linked to this job — source of truth
for the customer-facing description, serial(s),
and part metadata. -->
<t t-set="primary_line" t-value="job.sale_order_line_ids[:1]"/>
<t t-set="po_number"
t-value="(so and (so.client_order_ref or (
'x_fc_po_number' in so._fields and so.x_fc_po_number) or ''))
or ''"/>
<t t-set="customer_desc"
t-value="primary_line and primary_line.fp_customer_description() or ''"/>
<t t-set="serial_names"
t-value="primary_line and 'x_fc_serial_ids' in primary_line._fields
and ', '.join(primary_line.x_fc_serial_ids.mapped('name'))
or ''"/>
<!-- Walk EVERY step in sequence, not just moves. The
old report only rendered moves so steps without
recorded measurements (just Finish & Next) never
appeared on the cert. -->
<t t-set="all_steps" t-value="job.step_ids.filtered(
lambda s: s.state not in ('cancelled',)
).sorted('sequence')"/>
<div class="page fp-wo-detail">
<style>
.fp-wo-detail { font-family: Arial, sans-serif; font-size: 9pt; color: #000; }
.fp-wo-detail h1 { text-align: center; font-size: 18pt; margin: 0 0 6px 0; font-weight: bold; color: #1a4d80; }
.fp-wo-detail h3 { font-size: 11pt; margin: 8px 0 2px 0; font-weight: bold; }
.fp-wo-detail .fp-meta { font-size: 8.5pt; color: #444; margin-bottom: 4px; }
.fp-wo-detail h1 { text-align: center; font-size: 18pt; margin: 0 0 14px 0; font-weight: bold; color: #1a4d80; }
.fp-wo-detail h3 { font-size: 11pt; margin: 12px 0 4px 0; font-weight: bold; }
.fp-wo-detail .fp-meta { font-size: 8.5pt; color: #444; margin-bottom: 6px; }
.fp-wo-detail table.bordered,
.fp-wo-detail table.bordered th,
.fp-wo-detail table.bordered td { border: 1px solid #000; border-collapse: collapse; }
@@ -61,15 +95,16 @@
.fp-wo-detail table.bordered th { background: #ededed; padding: 4px 6px; font-size: 8.5pt; text-align: left; }
.fp-wo-detail table.bordered td { padding: 4px 6px; vertical-align: top; font-size: 8.5pt; }
.fp-wo-detail .text-center { text-align: center; }
.fp-wo-detail hr.heavy { border: 0; border-top: 2px solid #000; margin: 8px 0; }
.fp-wo-detail .fp-spec { font-size: 10pt; font-weight: bold; margin: 8px 0 4px 0; }
.fp-wo-detail .fp-step-block { page-break-inside: avoid; margin-bottom: 6px; }
.fp-wo-detail hr.heavy { border: 0; border-top: 2px solid #000; margin: 12px 0; }
.fp-wo-detail .fp-spec { font-size: 10pt; font-weight: bold; margin: 10px 0 6px 0; }
.fp-wo-detail .fp-step-block { page-break-inside: avoid; margin-bottom: 14px; }
.fp-wo-detail .fp-prepared { margin-bottom: 14px; }
</style>
<h1>Work Order Detail</h1>
<!-- ===== HEADER — Prepared For + summary table ===== -->
<div style="margin-bottom: 8px;">
<div class="fp-prepared">
<strong>Prepared For:</strong>
<span style="font-size: 11pt;"
t-esc="(job.partner_id and job.partner_id.name) or '—'"/>
@@ -77,35 +112,41 @@
<table class="bordered">
<tr>
<th style="width: 20%;">Part Number</th>
<th style="width: 18%;">Part Number</th>
<th style="width: 30%;">Description</th>
<th style="width: 8%;">Quantity</th>
<th style="width: 10%;">Work Order</th>
<th style="width: 14%;">PO Number</th>
<th style="width: 8%;">Packing List No</th>
<th style="width: 7%;">Quantity</th>
<th style="width: 11%;">Work Order</th>
<th style="width: 12%;">PO Number</th>
<th style="width: 12%;">Serial No</th>
<th style="width: 10%;">Date</th>
</tr>
<tr>
<td>
<t t-if="'part_catalog_id' in job._fields and job.part_catalog_id">
<span t-esc="job.part_catalog_id.part_number or '—'"/>
<t t-if="'revision' in job.part_catalog_id._fields and job.part_catalog_id.revision">
<br/>
<span style="font-size: 7.5pt;">Rev <span t-esc="job.part_catalog_id.revision"/></span>
</t>
</t>
<t t-else="">
<span t-esc="(job.product_id and job.product_id.default_code) or '—'"/>
</t>
</td>
<td style="white-space: pre-wrap;">
<t t-if="'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.product_id and job.product_id.name) or '—'"/>
</t>
<t t-if="'special_requirements' in job._fields and job.special_requirements">
<br/>
<span style="font-size: 7.5pt;"
t-esc="job.special_requirements"/>
</t>
<td style="vertical-align: top;">
<!-- Customer-facing description. The
pre-line wrapper lives on an
INNER div, not the <td>: keeping
pre-line on the cell rendered
the indentation between <td>
and <t t-if> as literal blank
lines, pushing the description
halfway down the cell. The div
only sees the t-esc'd text, so
pre-line preserves the operator's
intentional \n\n paragraph
breaks but nothing else. -->
<div style="white-space: pre-line;"><t t-if="customer_desc"><span t-esc="customer_desc.strip()"/></t><t t-elif="'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.product_id and job.product_id.name) or '—'"/></t></div>
</td>
<td class="text-center">
<span t-esc="job.qty"/>
@@ -114,11 +155,15 @@
<span t-esc="job.name"/>
</td>
<td>
<span t-esc="(job.sale_order_id and job.sale_order_id.client_order_ref) or '—'"/>
<span t-esc="po_number or '—'"/>
</td>
<td/>
<td>
<span t-esc="(job.date_finished or job.date_started or job.create_date) and (job.date_finished or job.date_started or job.create_date).strftime('%Y-%m-%d') or ''"/>
<span t-esc="serial_names or '—'"/>
</td>
<td>
<t t-set="_hdr_dt"
t-value="job.date_finished or job.date_started or job.create_date"/>
<span t-esc="job.fp_format_local(_hdr_dt, '%Y-%m-%d')"/>
</td>
</tr>
</table>
@@ -130,15 +175,39 @@
<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 ''"/>
<!-- ===== STEPS WALK ===== -->
<t t-foreach="all_steps" t-as="step">
<!-- Aggregate captured input values from any
move that touches this step (incoming or
outgoing — the Record Inputs wizard
creates a self-loop move with from=to=step). -->
<t t-set="step_moves"
t-value="job.move_ids.filtered(
lambda m: m.from_step_id == step or m.to_step_id == step
).sorted('move_datetime')"/>
<t t-set="step_values"
t-value="step_moves.mapped('transition_input_value_ids')"/>
<!-- Pick a representative "Moved By" / Time:
prefer the step's own date_finished, fall
back to first move on the step, fall back
to date_started. Same for the user. -->
<t t-set="display_dt"
t-value="step.date_finished or (step_moves and step_moves[-1].move_datetime) or step.date_started or False"/>
<t t-set="display_user"
t-value="(step.finished_by_user_id and step.finished_by_user_id.name)
or (step_moves and step_moves[-1].moved_by_user_id and step_moves[-1].moved_by_user_id.name)
or (step.started_by_user_id and step.started_by_user_id.name)
or ''"/>
<div class="fp-step-block">
<h3>
<span t-esc="(dest and dest.name) or '—'"/>
<t t-if="tank_code"> (<span t-esc="tank_code"/>)</t>
<span t-esc="step.name or '—'"/>
<t t-if="step.tank_id and step.tank_id.code">
(<span t-esc="step.tank_id.code"/>)
</t>
<t t-if="step.state == 'skipped'">
<span style="font-size: 9pt; color: #888; font-weight: normal;">— SKIPPED</span>
</t>
</h3>
<div class="fp-meta">
<strong>Part Number:</strong>
@@ -151,69 +220,72 @@
<t t-else="">
<span t-esc="(job.product_id and (job.product_id.default_code or job.product_id.name)) or ''"/>
</t>
<br/>
<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="display_user or display_dt">
<br/>
<strong>Moved By:</strong>
<span t-esc="display_user or '—'"/>
<span> </span>
<strong>Time:</strong>
<span t-esc="job.fp_format_local(display_dt, '%b %d, %Y %I:%M:%S %p') or '—'"/>
</t>
</div>
<!-- Captured input values for this move -->
<t t-set="captured_values_by_input"
t-value="{v.node_input_id.id: v for v in mv.transition_input_value_ids}"/>
<t t-set="prompts" t-value="False"/>
<t t-if="dest and dest.recipe_node_id">
<t t-set="prompts"
t-value="dest.recipe_node_id.input_ids.filtered(lambda i: (i.kind or 'step_input') == 'step_input').sorted('sequence')"/>
</t>
<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 and mv.transition_input_value_ids">
<!-- Captured inputs table — only rendered
when this step has at least one
value recorded across all its moves. -->
<t t-if="step_values">
<table class="bordered">
<thead>
<tr>
<th style="width: 24%;">Name</th>
<th style="width: 30%;">Description</th>
<th style="width: 32%;">Description</th>
<th style="width: 18%;">Value</th>
<th style="width: 28%;">Recorded By</th>
<th style="width: 26%;">Recorded By</th>
</tr>
</thead>
<tbody>
<t t-foreach="prompts" t-as="inp">
<t t-set="cv" t-value="captured_values_by_input.get(inp.id)"/>
<t t-if="cv">
<t t-set="actual_str" t-value="''"/>
<t t-if="cv.value_text">
<t t-set="actual_str" t-value="cv.value_text"/>
<t t-foreach="step_values" t-as="cv">
<t t-set="inp" t-value="cv.node_input_id"/>
<t t-set="prompt_name"
t-value="(inp and inp.name) or (cv.value_text and cv.value_text.split(':')[0]) or 'Measurement'"/>
<t t-set="prompt_hint"
t-value="(inp and 'hint' in inp._fields and inp.hint) or ''"/>
<t t-set="actual_str" t-value="''"/>
<t t-if="cv.value_text">
<t t-set="actual_str" t-value="cv.value_text"/>
<!-- Strip the leading "Prompt:" prefix that
ad-hoc rows store so the Value cell
shows just the value, not the prompt
twice. -->
<t t-if="inp and inp.name and actual_str.startswith(inp.name + ':')">
<t t-set="actual_str" t-value="actual_str[len(inp.name)+1:].strip()"/>
</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>
<tr>
<td><span t-esc="inp.name"/></td>
<td>
<t t-if="'hint' in inp._fields and inp.hint">
<span t-esc="inp.hint"/>
</t>
</td>
<td>
<strong t-esc="actual_str"/>
</td>
<td>
<span t-esc="(mv.moved_by_user_id and mv.moved_by_user_id.name) or ''"/>
</td>
</tr>
</t>
<t t-elif="cv.value_number">
<t t-set="_unit" t-value="(inp and 'target_unit' in inp._fields and inp.target_unit) or ''"/>
<t t-set="actual_str" t-value="('%s %s' % (cv.value_number, _unit)).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="job.fp_format_local(cv.value_date, '%Y-%m-%d %H:%M')"/>
</t>
<tr>
<td><span t-esc="prompt_name"/></td>
<td>
<t t-if="prompt_hint">
<span t-esc="prompt_hint"/>
</t>
</td>
<td>
<strong t-esc="actual_str"/>
</td>
<td>
<span t-esc="(cv.move_id.moved_by_user_id and cv.move_id.moved_by_user_id.name) or ''"/>
</td>
</tr>
</t>
</tbody>
</table>
@@ -221,16 +293,22 @@
</div>
</t>
<t t-if="not moves">
<t t-if="not all_steps">
<p style="color: #888; font-style: italic;">
No move log entries yet — this job hasn't progressed
through any steps. Operators move the job forward
via the tablet or the backend Move wizard.
No steps on this job yet — operators progress the
job via Start / Finish &amp; Next on the form, or
via the tablet.
</p>
</t>
<!-- ===== CERTIFIED BY + CERT STATEMENT ===== -->
<p style="page-break-before: always;"/>
<!-- page-break-before is honoured by wkhtmltopdf
but the new page starts flush against the
header_spacing band; the spacer div below
gives the cert table breathing room so it
doesn't sit under the company logo. -->
<div style="page-break-before: always;"/>
<div style="height: 8mm;"/>
<t t-set="owner_sig" t-value="False"/>
<t t-if="'x_fc_owner_user_id' in company._fields and company.x_fc_owner_user_id">