fix(reports): keep BoL signature row intact across page breaks

Landscape BoL was splitting the signature row down the middle —
boxes half on page 1, half on page 2. Two complementary fixes:

1. **Per-element rule**: added `page-break-inside: avoid` +
   `break-inside: avoid` to `.sig-box` (both portrait + landscape
   styles) so an individual signature box can never split across
   pages.

2. **Wrapper rule**: introduced `.fp-keep-together` utility +
   wrapped the BoL's certification statement + signature row in
   it, so the whole "sign here" block moves to the next page as
   one unit if it doesn't fit. Also applied
   `page-break-inside: avoid` to `table tr` so cargo lines don't
   split mid-row either.

Lives in shared `report_base_styles.xml` so any FP template that
opts into `.fp-keep-together` benefits automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 07:35:55 -04:00
parent 7ad7481195
commit b16486f66b
5 changed files with 74 additions and 25 deletions

View File

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

View File

@@ -46,10 +46,13 @@
.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: 14px 12px 8px 12px; min-height: 110px; display: flex; flex-direction: column; justify-content: flex-end; }
.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; page-break-inside: avoid; break-inside: avoid; }
.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; }
.fp-report .fp-keep-together { page-break-inside: avoid; break-inside: avoid; }
.fp-report .fp-keep-together .row, .fp-report .fp-keep-together .col-4 { page-break-inside: avoid; break-inside: avoid; }
.fp-report table tr { page-break-inside: avoid; break-inside: avoid; }
</style>
</template>
@@ -83,10 +86,13 @@
.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: 14px 12px 8px 12px; min-height: 130px; display: flex; flex-direction: column; justify-content: flex-end; }
.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; page-break-inside: avoid; break-inside: avoid; }
.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; }
.fp-landscape .fp-keep-together { page-break-inside: avoid; break-inside: avoid; }
.fp-landscape .fp-keep-together .row, .fp-landscape .fp-keep-together .col-4 { page-break-inside: avoid; break-inside: avoid; }
.fp-landscape table tr { page-break-inside: avoid; break-inside: avoid; }
</style>
</template>
</odoo>

View File

@@ -174,31 +174,33 @@
</tbody>
</table>
<!-- Certification statement -->
<div class="highlight-box" style="margin-top: 10px;">
This is to certify that the above-named materials are properly classified,
packaged, marked, and labelled, and are in proper condition for transportation
according to the applicable regulations of the Department of Transportation.
</div>
<!-- Cert statement + signatures held together so the
BoL doesn't split the signature row across pages. -->
<div class="fp-keep-together">
<div class="highlight-box" style="margin-top: 10px;">
This is to certify that the above-named materials are properly classified,
packaged, marked, and labelled, and are in proper condition for transportation
according to the applicable regulations of the Department of Transportation.
</div>
<!-- Sign off -->
<div class="row" style="margin-top: 20px;">
<div class="col-4">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Shipper (Signature / Date)</div>
<div class="row" style="margin-top: 20px;">
<div class="col-4">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Shipper (Signature / Date)</div>
</div>
</div>
</div>
<div class="col-4">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Carrier / Driver (Signature / Date)</div>
<div class="col-4">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Carrier / Driver (Signature / Date)</div>
</div>
</div>
</div>
<div class="col-4">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Consignee (Signature / Date)</div>
<div class="col-4">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Consignee (Signature / Date)</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,10 @@
env = env # noqa
import re
for variant in ('portrait', 'landscape'):
rep = env.ref(f'fusion_plating_reports.action_report_fp_bol_{variant}')
dlv = env['fusion.plating.delivery'].search([], order='id desc', limit=1)
pdf, _ = rep.with_context(force_report_rendering=True
)._render_qweb_pdf(rep.report_name, [dlv.id])
# Count pages by looking at the /Type /Page (not /Pages) markers
n_pages = len(re.findall(rb'/Type\s*/Page[^s]', pdf))
print(f'{variant:10s} {len(pdf)/1024:6.1f} KB pages={n_pages}')

View File

@@ -0,0 +1,31 @@
env = env # noqa
import subprocess, os
rep = env.ref('fusion_plating_reports.action_report_fp_bol_landscape')
dlv = env['fusion.plating.delivery'].search([], order='id desc', limit=1)
pdf, _ = rep.with_context(force_report_rendering=True
)._render_qweb_pdf(rep.report_name, [dlv.id])
path = '/tmp/bol_landscape.pdf'
with open(path, 'wb') as f:
f.write(pdf)
print(f'wrote {len(pdf)/1024:.1f} KB to {path}')
# Extract text per page using pdftotext (poppler-utils)
try:
for p in (1, 2, 3):
out = subprocess.run(
['pdftotext', '-layout', '-f', str(p), '-l', str(p), path, '-'],
capture_output=True, text=True, timeout=10
)
if out.returncode != 0 or not out.stdout.strip():
continue
text = out.stdout
sig_labels = [
'Shipper (Signature' in text,
'Carrier / Driver' in text,
'Consignee (Signature' in text,
]
cert_present = 'is to certify' in text
print(f'PAGE {p}: cert={cert_present} sigs={sig_labels} '
f'(all-3-sigs-together={all(sig_labels)})')
except FileNotFoundError:
print('pdftotext not installed — skipping per-page text check')