fix(bol): bigger title, shipper info, uniform headers, cargo qty, taller signatures

Five fixes applied to the Bill of Lading and (where relevant) all
report templates:

1. **Bigger title + BoL #** — portrait now uses h2 24pt (was h4 16pt),
   landscape h2 26pt; BoL # ticker is 13/14pt instead of body size.

2. **Shipper info missing** — root cause: `_fp_build_delivery_vals`
   was creating deliveries without `company_id`, so the BoL's
   `<span t-field="doc.company_id.name"/>` rendered empty. Two fixes:
   - Hook now sets `company_id = mo.company_id.id or env.company.id`.
   - Template falls back defensively to `env.company` when
     `doc.company_id` is empty (covers any legacy delivery that
     somehow slips through without it).
   - Backfilled 14 existing deliveries via SQL on entech.

3. **Uniform header backgrounds** — replaced mixed `info-header`
   (gray) + default-th (brand black) headers with a single
   `fp-header-primary` (brand black) across all sub-tables for a
   consistent look.

4. **Cargo description alignment + missing column** — added a QTY
   column (matches landscape variant), pulled from the linked MO
   via job_ref → mrp.production.product_qty. Added `.fp-cell-mid`
   utility class with `vertical-align: middle !important;` and
   applied it to every cargo + info cell so values sit centred
   instead of jammed against the top border.

5. **Signature box too short** — bumped `.sig-box` from 70 → 110 px
   (portrait) / 130 px (landscape), `.sig-line` from 28 → 60/70 px,
   added flex layout so the label sits at the bottom and signers
   have a real space to write in. Lives in the shared
   `report_base_styles.xml` so EVERY FP template benefits, not just
   the BoL.

Verified: BoL portrait renders cleanly at 140 KB with full shipper
block + uniform headers + middle-aligned cargo cells.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 07:29:28 -04:00
parent 82a2091914
commit 7ad7481195
8 changed files with 102 additions and 47 deletions

View File

@@ -5,7 +5,7 @@
{
"name": "Fusion Plating — MRP Bridge",
'version': '19.0.6.0.0',
'version': '19.0.6.1.0',
'category': 'Manufacturing/Plating',
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
'description': """

View File

@@ -674,6 +674,7 @@ class MrpProduction(models.Model):
], order='id', limit=1)
return {
'company_id': mo.company_id.id or self.env.company.id,
'partner_id': job.partner_id.id,
'job_ref': job.name,
'source_facility_id': (

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Reports',
'version': '19.0.4.1.0',
'version': '19.0.4.2.0',
'category': 'Manufacturing/Plating',
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
'depends': [

View File

@@ -46,9 +46,10 @@
.fp-report .status-ok { color: #2e7d32; font-weight: bold; }
.fp-report .status-warning { color: #f57f17; font-weight: bold; }
.fp-report .status-fail { color: #c62828; font-weight: bold; }
.fp-report .sig-box { border: 1px solid #000; padding: 12px; min-height: 70px; }
.fp-report .sig-line { border-bottom: 1px solid #000; min-height: 28px; }
.fp-report .sig-box { border: 1px solid #000; padding: 14px 12px 8px 12px; min-height: 110px; display: flex; flex-direction: column; justify-content: flex-end; }
.fp-report .sig-line { border-bottom: 1px solid #000; min-height: 60px; }
.fp-report .small-muted { font-size: 8pt; color: #666; }
.fp-report .fp-cell-mid { vertical-align: middle !important; }
</style>
</template>
@@ -82,9 +83,10 @@
.fp-landscape .status-ok { color: #2e7d32; font-weight: bold; }
.fp-landscape .status-warning { color: #f57f17; font-weight: bold; }
.fp-landscape .status-fail { color: #c62828; font-weight: bold; }
.fp-landscape .sig-box { border: 1px solid #000; padding: 12px; min-height: 70px; }
.fp-landscape .sig-line { border-bottom: 1px solid #000; min-height: 28px; }
.fp-landscape .sig-box { border: 1px solid #000; padding: 14px 12px 8px 12px; min-height: 130px; display: flex; flex-direction: column; justify-content: flex-end; }
.fp-landscape .sig-line { border-bottom: 1px solid #000; min-height: 70px; }
.fp-landscape .small-muted { font-size: 9pt; color: #666; }
.fp-landscape .fp-cell-mid { vertical-align: middle !important; }
</style>
</template>
</odoo>

View File

@@ -19,10 +19,14 @@
<div class="fp-report">
<div class="page">
<h4 class="text-center" style="text-align: center;">
<!-- Resolve shipper company defensively — fall back to env.company
when delivery.company_id is missing on legacy records. -->
<t t-set="ship_co" t-value="doc.company_id or env.company"/>
<h2 class="text-center" style="text-align: center; font-size: 24pt; margin: 0 0 6px 0;">
BILL OF LADING
</h4>
<div class="text-center" style="text-align: center; margin-bottom: 10px;">
</h2>
<div class="text-center" style="text-align: center; margin-bottom: 14px; font-size: 13pt;">
<strong>BoL #: <span t-field="doc.name"/></strong>
</div>
@@ -30,21 +34,21 @@
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">SHIPPER</th>
<th style="width: 50%;">CONSIGNEE</th>
<th class="fp-header-primary" style="width: 50%;">SHIPPER</th>
<th class="fp-header-primary" style="width: 50%;">CONSIGNEE</th>
</tr>
</thead>
<tbody>
<tr>
<td style="height: 90px;">
<strong><span t-field="doc.company_id.name"/></strong><br/>
<td style="height: 110px;">
<strong><span t-esc="ship_co.name"/></strong><br/>
<t t-if="doc.source_facility_id">
<em t-field="doc.source_facility_id.name"/><br/>
</t>
<div t-field="doc.company_id.partner_id"
<div t-field="ship_co.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 90px;">
<td style="height: 110px;">
<strong><span t-field="doc.partner_id.name"/></strong><br/>
<t t-if="doc.delivery_address_id">
<div t-field="doc.delivery_address_id"
@@ -69,21 +73,21 @@
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 33%;">SHIP DATE</th>
<th class="info-header" style="width: 33%;">DRIVER</th>
<th class="info-header" style="width: 34%;">VEHICLE</th>
<th class="fp-header-primary" style="width: 33%;">SHIP DATE</th>
<th class="fp-header-primary" style="width: 33%;">DRIVER</th>
<th class="fp-header-primary" style="width: 34%;">VEHICLE</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center">
<td class="text-center fp-cell-mid"><span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center fp-cell-mid">
<t t-if="doc.assigned_driver_id">
<span t-field="doc.assigned_driver_id.name"/>
</t>
<t t-else="">-</t>
</td>
<td class="text-center">
<td class="text-center fp-cell-mid">
<t t-if="doc.vehicle_id">
<span t-field="doc.vehicle_id"/>
</t>
@@ -97,14 +101,14 @@
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 50%;">JOB REFERENCE</th>
<th class="info-header" style="width: 50%;">DANGEROUS GOODS (TDG)</th>
<th class="fp-header-primary" style="width: 50%;">JOB REFERENCE</th>
<th class="fp-header-primary" style="width: 50%;">DANGEROUS GOODS (TDG)</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-esc="doc.job_ref or '-'"/></td>
<td class="text-center">
<td class="text-center fp-cell-mid"><span t-esc="doc.job_ref or '-'"/></td>
<td class="text-center fp-cell-mid">
<span t-if="doc.tdg_required" class="status-warning">TDG REQUIRED</span>
<span t-else="" class="status-ok">No TDG</span>
</td>
@@ -112,30 +116,35 @@
</tbody>
</table>
<!-- Cargo description -->
<!-- Cargo description — added QTY column to match landscape -->
<table class="bordered">
<thead>
<tr>
<th colspan="4" class="fp-header-primary">CARGO DESCRIPTION</th>
<th colspan="5" class="fp-header-primary">CARGO DESCRIPTION</th>
</tr>
<tr>
<th style="width: 12%;">PACKAGES</th>
<th class="text-start" style="width: 58%;">DESCRIPTION OF GOODS</th>
<th style="width: 15%;">WEIGHT</th>
<th style="width: 15%;">CLASS</th>
<th class="fp-header-primary" style="width: 12%;">PACKAGES</th>
<th class="fp-header-primary text-start" style="width: 48%;">DESCRIPTION OF GOODS</th>
<th class="fp-header-primary" style="width: 12%;">QTY</th>
<th class="fp-header-primary" style="width: 14%;">WEIGHT</th>
<th class="fp-header-primary" style="width: 14%;">CLASS</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">1</td>
<td>
<td class="text-center fp-cell-mid">1</td>
<td class="fp-cell-mid">
Plated parts — Job <span t-esc="doc.job_ref or doc.name"/>
<t t-if="doc.notes">
<br/><span t-field="doc.notes"/>
</t>
</td>
<td class="text-center"></td>
<td class="text-center">
<td class="text-center fp-cell-mid">
<t t-set="_mo" t-value="env['mrp.production'].sudo().search([('name', '=', doc.job_ref)], limit=1) if doc.job_ref else False"/>
<span t-esc="int(_mo.product_qty) if _mo else '—'"/>
</td>
<td class="text-center fp-cell-mid"></td>
<td class="text-center fp-cell-mid">
<span t-if="doc.tdg_required">TDG</span>
<span t-else="">NON-HAZ</span>
</td>
@@ -147,17 +156,17 @@
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 50%;">CoC ATTACHED</th>
<th class="info-header" style="width: 50%;">PACKING LIST</th>
<th class="fp-header-primary" style="width: 50%;">CoC ATTACHED</th>
<th class="fp-header-primary" style="width: 50%;">PACKING LIST</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">
<td class="text-center fp-cell-mid">
<span t-if="doc.coc_attachment_id" class="status-ok">✓ Attached</span>
<span t-else=""></span>
</td>
<td class="text-center">
<td class="text-center fp-cell-mid">
<span t-if="doc.packing_list_attachment_id" class="status-ok">✓ Attached</span>
<span t-else=""></span>
</td>
@@ -212,8 +221,10 @@
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: center;">BILL OF LADING</h2>
<div class="text-center" style="text-align: center; margin-bottom: 10px;">
<t t-set="ship_co" t-value="doc.company_id or env.company"/>
<h2 style="text-align: center; font-size: 26pt; margin: 0 0 6px 0;">BILL OF LADING</h2>
<div class="text-center" style="text-align: center; margin-bottom: 14px; font-size: 14pt;">
<strong>BoL #: <span t-field="doc.name"/></strong>
</div>
@@ -221,21 +232,21 @@
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">SHIPPER</th>
<th style="width: 50%;">CONSIGNEE</th>
<th class="fp-header-primary" style="width: 50%;">SHIPPER</th>
<th class="fp-header-primary" style="width: 50%;">CONSIGNEE</th>
</tr>
</thead>
<tbody>
<tr>
<td style="height: 100px; font-size: 12pt;">
<strong><span t-field="doc.company_id.name"/></strong><br/>
<td style="height: 120px; font-size: 12pt;">
<strong><span t-esc="ship_co.name"/></strong><br/>
<t t-if="doc.source_facility_id">
<em t-field="doc.source_facility_id.name"/><br/>
</t>
<div t-field="doc.company_id.partner_id"
<div t-field="ship_co.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 100px; font-size: 12pt;">
<td style="height: 120px; font-size: 12pt;">
<strong><span t-field="doc.partner_id.name"/></strong><br/>
<t t-if="doc.delivery_address_id">
<div t-field="doc.delivery_address_id"

View File

@@ -0,0 +1,15 @@
env = env # noqa
import re
report = env.ref('fusion_plating_reports.action_report_fp_bol_portrait')
dlv = env['fusion.plating.delivery'].search([], order='id desc', limit=1)
html, _ = report.with_context(force_report_rendering=True
)._render_qweb_html(report.report_name, [dlv.id])
out = html.decode() if isinstance(html, bytes) else str(html)
# Pull just the SHIPPER td
m = re.search(r'>SHIPPER<.*?</thead>(.*?)</table>', out, re.S)
if m:
print('--- SHIPPER/CONSIGNEE table body ---')
print(m.group(1)[:2500])
else:
print('SHIPPER section not found, dumping first 2000 chars:')
print(out[:2000])

View File

@@ -0,0 +1,19 @@
env = env # noqa
co = env['res.company'].search([], limit=1)
p = co.partner_id
print('company:', co.name)
print(' partner_id:', p.id, p.name)
print(' street:', repr(p.street))
print(' street2:', repr(p.street2))
print(' city:', repr(p.city), 'zip:', repr(p.zip))
print(' state:', p.state_id.name if p.state_id else None)
print(' country:', p.country_id.name if p.country_id else None)
print(' phone:', repr(p.phone), 'email:', repr(p.email))
print()
# Also check if delivery has a source_facility_id with address
dlv = env['fusion.plating.delivery'].search([], order='id desc', limit=1)
if dlv.source_facility_id:
f = dlv.source_facility_id
print('facility:', f.name)
print(' address:', getattr(f, 'address', None) or getattr(f, 'street', None) or '(no address field)')
print(' fields:', [k for k in f._fields if 'addr' in k or 'street' in k or 'city' in k])

View File

@@ -0,0 +1,7 @@
env = env # noqa
dlv = env['fusion.plating.delivery'].search([], order='id desc', limit=1)
print('delivery:', dlv.name)
print(' company_id:', dlv.company_id, '/', dlv.company_id.name if dlv.company_id else None)
print(' source_facility_id:', dlv.source_facility_id, '/', dlv.source_facility_id.name if dlv.source_facility_id else None)
print(' has company_id field?', 'company_id' in dlv._fields)
print(' field def:', dlv._fields.get('company_id'))