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

View File

@@ -46,10 +46,13 @@
.fp-report .status-ok { color: #2e7d32; font-weight: bold; } .fp-report .status-ok { color: #2e7d32; font-weight: bold; }
.fp-report .status-warning { color: #f57f17; font-weight: bold; } .fp-report .status-warning { color: #f57f17; font-weight: bold; }
.fp-report .status-fail { color: #c62828; 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 .sig-line { border-bottom: 1px solid #000; min-height: 60px; }
.fp-report .small-muted { font-size: 8pt; color: #666; } .fp-report .small-muted { font-size: 8pt; color: #666; }
.fp-report .fp-cell-mid { vertical-align: middle !important; } .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> </style>
</template> </template>
@@ -83,10 +86,13 @@
.fp-landscape .status-ok { color: #2e7d32; font-weight: bold; } .fp-landscape .status-ok { color: #2e7d32; font-weight: bold; }
.fp-landscape .status-warning { color: #f57f17; font-weight: bold; } .fp-landscape .status-warning { color: #f57f17; font-weight: bold; }
.fp-landscape .status-fail { color: #c62828; 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 .sig-line { border-bottom: 1px solid #000; min-height: 70px; }
.fp-landscape .small-muted { font-size: 9pt; color: #666; } .fp-landscape .small-muted { font-size: 9pt; color: #666; }
.fp-landscape .fp-cell-mid { vertical-align: middle !important; } .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> </style>
</template> </template>
</odoo> </odoo>

View File

@@ -174,14 +174,15 @@
</tbody> </tbody>
</table> </table>
<!-- Certification statement --> <!-- 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;"> <div class="highlight-box" style="margin-top: 10px;">
This is to certify that the above-named materials are properly classified, This is to certify that the above-named materials are properly classified,
packaged, marked, and labelled, and are in proper condition for transportation packaged, marked, and labelled, and are in proper condition for transportation
according to the applicable regulations of the Department of Transportation. according to the applicable regulations of the Department of Transportation.
</div> </div>
<!-- Sign off -->
<div class="row" style="margin-top: 20px;"> <div class="row" style="margin-top: 20px;">
<div class="col-4"> <div class="col-4">
<div class="sig-box"> <div class="sig-box">
@@ -202,6 +203,7 @@
</div> </div>
</div> </div>
</div> </div>
</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')