feat(fusion_plating): internal sticker = external layout w/ internal notes + Receiving print buttons

- Internal Job Sticker is no longer a separate Layout A: it's now a COPY of the
  External sticker (Layout B, one per box, logo + WO + BOX + QR + rail fields +
  prominent PLATING THICKNESS banner) but feeds the INTERNAL description and
  labels its notes "INTERNAL NOTES" so the shop copy can't be confused with the
  customer copy. The old Layout-A body template is deleted.
- Removed the Internal sticker from the fp.job Print menu (binding_model_id ->
  False); it now prints from the Receiving screen instead.
- Added "External Sticker" + "Internal Sticker" print buttons to the fp.receiving
  form header (shown once a WO exists). Each renders one label per tracked box
  for the receiving's work order (passes a single WO so the SO-scoped box loop
  doesn't reprint each label per job).
Verified on entech (WO-30094 / RCV-30096): internal renders Layout B with the
internal description + INTERNAL NOTES; external unchanged; both receiving buttons
return the right report actions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 13:16:14 -04:00
parent 97880765b5
commit c80ffa1b2c
3 changed files with 71 additions and 74 deletions

View File

@@ -8,6 +8,7 @@
# reaches state='done'.
from odoo import _, fields, models
from odoo.exceptions import UserError
class FpReceiving(models.Model):
@@ -65,3 +66,34 @@ class FpReceiving(models.Model):
if len(jobs) == 1:
action.update({'view_mode': 'form', 'res_id': jobs.id})
return action
# ---- Sticker printing from the Receiving screen (2026-06-04) ----------
# Both stickers loop the SO's boxes (one label per box). Pass a SINGLE
# work order: the box loop is sale-order-scoped, so feeding every job
# would reprint each box label once per job. One job → exactly one label
# per box. Falls back to a single 1/1 label when no boxes exist yet.
def _fp_sticker_jobs(self):
self.ensure_one()
if not self.sale_order_id:
return self.env['fp.job']
return self.env['fp.job'].sudo().search(
[('sale_order_id', '=', self.sale_order_id.id)], order='id', limit=1)
def _fp_print_sticker(self, xmlid):
self.ensure_one()
jobs = self._fp_sticker_jobs()
if not jobs:
raise UserError(_(
'No work order exists for this receiving yet — create the '
'Work Order before printing stickers.'))
return self.env.ref(xmlid).report_action(jobs)
def action_print_external_sticker(self):
"""Customer (external) box sticker(s) for this receiving's WO."""
return self._fp_print_sticker(
'fusion_plating_jobs.action_report_fp_job_sticker')
def action_print_internal_sticker(self):
"""Shop (internal) box sticker(s) — same layout, internal notes."""
return self._fp_print_sticker(
'fusion_plating_jobs.action_report_fp_job_sticker_internal')

View File

@@ -57,8 +57,11 @@
<field name="report_name">fusion_plating_jobs.report_fp_job_sticker_internal_template</field>
<field name="report_file">fusion_plating_jobs.report_fp_job_sticker_internal_template</field>
<field name="print_report_name">'Internal Job Sticker - %s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="fusion_plating.model_fp_job"/>
<field name="binding_type">report</field>
<!-- NOT bound to the fp.job Print menu (removed from the list per
request). Printed via the Receiving screen button
(fp.receiving.action_print_internal_sticker). eval=False clears
any binding a prior install left in the DB. -->
<field name="binding_model_id" eval="False"/>
<field name="paperformat_id" ref="paperformat_fp_job_sticker"/>
</record>
@@ -121,74 +124,6 @@
</style>
</template>
<!-- ===================== Internal body — Layout A ===================== -->
<template id="fp_job_internal_body">
<div class="label-page"><div class="label">
<table class="fpt">
<tr style="height:20mm" class="rule">
<td class="band pad">
<span style="float:right;text-align:right">
<t t-if="_logo"><span style="display:inline-block;background:#fff;padding:0.8mm 1.6mm;border-radius:1mm"><img t-att-src="image_data_uri(_logo)" style="max-height:8mm;max-width:38mm;vertical-align:middle"/></span></t>
<div style="margin-top:1.6mm"><span class="tag">INTERNAL</span></div>
</span>
<span class="lbl" style="color:#fff">Work Order</span>
<div style="font-size:30pt;font-weight:900;line-height:0.95"><t t-esc="d['wo']"/></div>
</td>
<td style="width:31mm;border-left:0.9mm solid #000;text-align:center;vertical-align:middle;padding:1mm">
<span class="qfwrap-int"><img t-att-src="_qr"/></span>
</td>
</tr>
<!-- Part# (dominant) + Customer moved up here. Customer is short +
consistent (name stripped for privacy) so it needs little room;
Part# gets the bulk. MASK/BAKE flags float at the Part# edge. -->
<tr style="height:13mm" class="rule">
<td style="padding:0" colspan="2"><table class="fpt"><tr>
<td class="pad vrule" style="width:72%">
<span style="float:right">
<t t-if="d['mask']"><span class="badge">MASK</span></t>
<t t-if="d['bake']"><span class="badge">BAKE</span></t>
</span>
<span class="lbl">Part#</span>
<span style="font-size:18pt;font-weight:900"> <t t-esc="d['part'] or '-'"/></span>
<t t-if="d['rev']"><span style="font-size:11pt;font-weight:bold"> Rev <t t-esc="d['rev']"/></span></t>
</td>
<td class="pad" style="width:28%">
<span class="lbl">Customer</span>
<span style="font-size:13pt;font-weight:900;display:block;line-height:1.05"><t t-esc="d['customer']"/></span>
</td>
</tr></table></td>
</tr>
<!-- PO# / Qty / Due / Thk. Label stacks TIGHT on the value: no <br/>
(.lbl is display:block, so the value sits directly beneath it —
the old <br/> added a blank line = the label/data gap). Thk gets
the widest column + nowrap so a thickness RANGE stays on one line;
PO#/Qty/Due trimmed to feed it. -->
<tr style="height:12mm" class="rule">
<td style="padding:0" colspan="2"><table class="fpt"><tr>
<td class="pad vrule" style="width:25%"><span class="lbl">PO#</span><span style="font-size:13pt;font-weight:900;display:block"><t t-esc="d['po'] or '-'"/></span></td>
<td class="pad vrule" style="width:13%"><span class="lbl">Qty</span><span style="font-size:14pt;font-weight:900;display:block"><t t-esc="d['qty']"/></span></td>
<td class="pad vrule" style="width:26%"><span class="lbl">Due</span><span style="font-size:12pt;font-weight:bold;display:block"><t t-esc="d['due'] or '-'"/></span></td>
<td class="pad" style="width:36%"><span class="lbl">Thk</span><span style="font-size:11pt;font-weight:bold;display:block;white-space:nowrap"><t t-esc="d['thk'] or '-'"/></span></td>
</tr></table></td>
</tr>
<t t-if="d['bake']">
<tr style="height:13mm" class="rule">
<td class="pad" colspan="2" style="vertical-align:top;padding-top:1.5mm">
<span class="inshead">BAKE</span>
<span class="instext" style="font-size:10pt;line-height:1.18"> <t t-esc="d['bake']"/></span>
</td>
</tr>
</t>
<tr>
<td class="pad" colspan="2" style="vertical-align:top;padding:1.5mm 2.5mm 3.5mm 2.5mm;overflow:hidden">
<span class="inshead">NOTES</span>
<div class="instext" t-att-style="'font-size:%spt;line-height:1.18;margin-top:1.5mm' % _note_pt"><t t-esc="_note or '-'"/></div>
</td>
</tr>
</table>
</div></div>
</template>
<!-- ===================== External body — Layout B ===================== -->
<template id="fp_job_external_body">
<div class="label-page"><div class="label">
@@ -251,7 +186,7 @@
<div class="instext" style="font-size:10pt;line-height:1.22;margin-top:1mm"><t t-esc="d['bake']"/></div>
</div>
</t>
<div class="m-notes"><span class="inshead">NOTES</span>
<div class="m-notes"><span class="inshead"><t t-esc="_notes_label or 'NOTES'"/></span>
<div class="instext" t-att-style="'font-size:%spt;line-height:1.25;margin-top:1.3mm' % _note_pt"><t t-esc="_note or '-'"/></div>
</div>
</div>
@@ -259,17 +194,35 @@
</template>
<!-- ===================== Internal outer (per job) ===================== -->
<!-- Internal sticker = a COPY of the External (Layout B, one per box) but
showing the INTERNAL description instead of the customer-facing notes,
and a "INTERNAL NOTES" header so the shop copy can't be confused with
the customer copy. Identical rail/QR/logo/box otherwise. -->
<template id="report_fp_job_sticker_internal_template">
<t t-call="web.html_container">
<t t-call="fusion_plating_jobs.fp_job_sticker_styles"/>
<t t-foreach="docs" t-as="job">
<t t-set="d" t-value="job._fp_sticker_data()"/>
<t t-set="_note" t-value="d['internal_notes']"/>
<t t-set="_notes_label" t-value="'INTERNAL NOTES'"/>
<t t-set="_note_pt" t-value="job._fp_note_pt(_note)"/>
<t t-set="_base" t-value="job.env['ir.config_parameter'].sudo().get_param('web.base.url', '')"/>
<t t-set="_qr" t-value="job.env['ir.actions.report'].sudo().barcode_data_uri('QR', _base + '/fp/job/' + str(job.id), width=1000, height=1000)"/>
<t t-set="_logo" t-value="job.env.company.logo or job.env.company.logo_web or job.env.company.partner_id.image_1920 or False"/>
<t t-call="fusion_plating_jobs.fp_job_internal_body"/>
<t t-set="_base" t-value="job.env['ir.config_parameter'].sudo().get_param('web.base.url', '')"/>
<t t-set="boxes" t-value="job._fp_sticker_boxes()"/>
<t t-if="boxes">
<t t-foreach="boxes" t-as="box">
<t t-set="_box_num" t-value="box.box_number"/>
<t t-set="_box_cnt" t-value="box.box_count or len(boxes)"/>
<t t-set="_qr" t-value="job.env['ir.actions.report'].sudo().barcode_data_uri('QR', _base + '/fp/box/' + str(box.id), width=1000, height=1000)"/>
<t t-call="fusion_plating_jobs.fp_job_external_body"/>
</t>
</t>
<t t-else="">
<t t-set="_box_num" t-value="1"/>
<t t-set="_box_cnt" t-value="1"/>
<t t-set="_qr" t-value="job.env['ir.actions.report'].sudo().barcode_data_uri('QR', _base + '/fp/job/' + str(job.id), width=1000, height=1000)"/>
<t t-call="fusion_plating_jobs.fp_job_external_body"/>
</t>
</t>
</t>
</template>
@@ -281,6 +234,7 @@
<t t-foreach="docs" t-as="job">
<t t-set="d" t-value="job._fp_sticker_data()"/>
<t t-set="_note" t-value="d['customer_notes']"/>
<t t-set="_notes_label" t-value="'NOTES'"/>
<t t-set="_note_pt" t-value="job._fp_note_pt(_note)"/>
<t t-set="_logo" t-value="job.env.company.logo or job.env.company.logo_web or job.env.company.partner_id.image_1920 or False"/>
<t t-set="_base" t-value="job.env['ir.config_parameter'].sudo().get_param('web.base.url', '')"/>

View File

@@ -25,6 +25,17 @@
class="btn-primary" icon="fa-cogs"
invisible="not x_fc_show_work_order_btn"
help="Open the Work Order(s) for this receiving. Hidden automatically once every linked WO is marked Done."/>
<!-- Print box stickers for this receiving's work order — one
label per tracked box (external = customer copy, internal
= shop copy with internal notes). Shown once a WO exists. -->
<button name="action_print_external_sticker"
string="External Sticker" type="object" icon="fa-print"
invisible="x_fc_fp_job_count == 0"
help="Print the customer (external) box sticker(s) — one per box."/>
<button name="action_print_internal_sticker"
string="Internal Sticker" type="object" icon="fa-print"
invisible="x_fc_fp_job_count == 0"
help="Print the shop (internal) box sticker(s) — same layout, internal notes."/>
</xpath>
<!-- Work Order smart button on the button_box (mirrors the