From cb9baa03ad95e7d4dc778eaebe27ce5936cb5fef Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 08:14:07 -0400 Subject: [PATCH] =?UTF-8?q?fix(reports):=20collapse=20sig-row=20to=20one?= =?UTF-8?q?=20bordered=20table=20=E2=80=94=20kill=20duplicate=20borders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User reported "multiple unwanted vertical lines in the boxes" on the portrait BoL. Pixel analysis confirmed it: previous design had 3 separate `
` each with its own 1px border, with a 4-8px gap between adjacent boxes — visually those adjacent borders read as a doubled / "duplicate" line between cells. Fix: replace 3-box layout with a single `` containing 3 td cells. With border-collapse: collapse, adjacent cells share their border — so the row now shows 4 vertical lines (1 outer left + 2 internal dividers + 1 outer right) instead of 6 close-together border lines. - Dropped `.sig-box` class entirely (no per-box border anymore) - Added `.sig-table` + `.sig-cell` with explicit 1px borders so the layout works without depending on `.bordered` class inheritance - Applied to both portrait + landscape variants - Landscape sig-row was still using the OLD Bootstrap row+col-4 layout (never got replaced earlier) — also migrated to the new table layout Verified: page count unchanged (portrait 1, landscape 1), all labels and content present, structure clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_reports/__manifest__.py | 2 +- .../report/report_base_styles.xml | 10 ++-- .../report/report_fp_bol.xml | 50 ++++++++----------- .../scripts/fp_bol_portrait_inspect.py | 25 ++++++++++ .../scripts/fp_bol_portrait_save.py | 8 +++ fusion_plating/scripts/fp_bol_stress.py | 33 ++++++++++++ fusion_plating/scripts/fp_grep.py | 22 ++++++++ 7 files changed, 113 insertions(+), 37 deletions(-) create mode 100644 fusion_plating/scripts/fp_bol_portrait_inspect.py create mode 100644 fusion_plating/scripts/fp_bol_portrait_save.py create mode 100644 fusion_plating/scripts/fp_bol_stress.py create mode 100644 fusion_plating/scripts/fp_grep.py diff --git a/fusion_plating/fusion_plating_reports/__manifest__.py b/fusion_plating/fusion_plating_reports/__manifest__.py index 6404bdc7..a93caaa3 100644 --- a/fusion_plating/fusion_plating_reports/__manifest__.py +++ b/fusion_plating/fusion_plating_reports/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating — Reports', - 'version': '19.0.4.5.0', + 'version': '19.0.4.7.0', 'category': 'Manufacturing/Plating', 'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.', 'depends': [ diff --git a/fusion_plating/fusion_plating_reports/report/report_base_styles.xml b/fusion_plating/fusion_plating_reports/report/report_base_styles.xml index ca784006..369dfc59 100644 --- a/fusion_plating/fusion_plating_reports/report/report_base_styles.xml +++ b/fusion_plating/fusion_plating_reports/report/report_base_styles.xml @@ -46,10 +46,9 @@ .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; page-break-inside: avoid; break-inside: avoid; } .fp-report .sig-line { border-bottom: 1px solid #000; height: 60px; margin-bottom: 4px; } - .fp-report .sig-table { width: 100%; border-collapse: separate; border-spacing: 8px 0; margin-top: 16px; page-break-inside: avoid; break-inside: avoid; } - .fp-report .sig-table td { padding: 0; vertical-align: top; page-break-inside: avoid; break-inside: avoid; } + .fp-report .sig-table { width: 100%; border-collapse: collapse; margin-top: 16px; border: 1px solid #000; page-break-inside: avoid; break-inside: avoid; } + .fp-report .sig-table .sig-cell { padding: 14px 12px 8px 12px; vertical-align: top; border: 1px solid #000; page-break-inside: avoid; break-inside: avoid; } .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; } @@ -88,10 +87,9 @@ .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: 8px 10px; page-break-inside: avoid; break-inside: avoid; } .fp-landscape .sig-line { border-bottom: 1px solid #000; height: 45px; margin-bottom: 3px; } - .fp-landscape .sig-table { width: 100%; border-collapse: separate; border-spacing: 6px 0; margin-top: 6px; page-break-inside: avoid; break-inside: avoid; } - .fp-landscape .sig-table td { padding: 0; vertical-align: top; page-break-inside: avoid; break-inside: avoid; } + .fp-landscape .sig-table { width: 100%; border-collapse: collapse; margin-top: 6px; border: 1px solid #000; page-break-inside: avoid; break-inside: avoid; } + .fp-landscape .sig-table .sig-cell { padding: 10px 10px 6px 10px; vertical-align: top; border: 1px solid #000; page-break-inside: avoid; break-inside: avoid; } .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; } diff --git a/fusion_plating/fusion_plating_reports/report/report_fp_bol.xml b/fusion_plating/fusion_plating_reports/report/report_fp_bol.xml index 43e1ff69..ab1665cf 100644 --- a/fusion_plating/fusion_plating_reports/report/report_fp_bol.xml +++ b/fusion_plating/fusion_plating_reports/report/report_fp_bol.xml @@ -183,25 +183,19 @@ according to the applicable regulations of the Department of Transportation. -
+
- - -
-
-
-
Shipper (Signature / Date)
-
+
+
+
Shipper (Signature / Date)
-
-
-
Carrier / Driver (Signature / Date)
-
+
+
+
Carrier / Driver (Signature / Date)
-
-
-
Consignee (Signature / Date)
-
+
+
+
Consignee (Signature / Date)
@@ -374,26 +368,22 @@
-
-
-
+ + + + + + +
Shipper (Signature / Date)
-
- -
-
+
Carrier / Driver (Signature / Date)
-
- -
-
+
Consignee (Signature / Date)
-
- - +
diff --git a/fusion_plating/scripts/fp_bol_portrait_inspect.py b/fusion_plating/scripts/fp_bol_portrait_inspect.py new file mode 100644 index 00000000..488fd5b1 --- /dev/null +++ b/fusion_plating/scripts/fp_bol_portrait_inspect.py @@ -0,0 +1,25 @@ +env = env # noqa +import re +rep = env.ref('fusion_plating_reports.action_report_fp_bol_portrait') +dlv = env['fusion.plating.delivery'].search([], order='id desc', limit=1) +html, _ = rep.with_context(force_report_rendering=True + )._render_qweb_html(rep.report_name, [dlv.id]) +out = html.decode() if isinstance(html, bytes) else str(html) + +# Pull the sig-table block + a bit before +m = re.search(r'(
)\s*
\s*
\s*', + out, re.S) +if m: + print('=== fp-keep-together block ===') + print(m.group(1)[:3000]) +else: + # Fallback — just find the sig-table + m2 = re.search(r'()', out, re.S) + if m2: + print('=== sig-table block ===') + print(m2.group(1)) + +# Also dump the relevant CSS rules +print('\n=== relevant css ===') +for rule in re.findall(r'\.fp-report\s+\.(?:sig-|fp-keep)[^{]*\{[^}]*\}', out): + print(rule) diff --git a/fusion_plating/scripts/fp_bol_portrait_save.py b/fusion_plating/scripts/fp_bol_portrait_save.py new file mode 100644 index 00000000..41d89f7b --- /dev/null +++ b/fusion_plating/scripts/fp_bol_portrait_save.py @@ -0,0 +1,8 @@ +env = env # noqa +rep = env.ref('fusion_plating_reports.action_report_fp_bol_portrait') +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]) +with open('/tmp/bol_portrait.pdf', 'wb') as f: + f.write(pdf) +print(f'wrote {len(pdf)/1024:.1f} KB') diff --git a/fusion_plating/scripts/fp_bol_stress.py b/fusion_plating/scripts/fp_bol_stress.py new file mode 100644 index 00000000..60e8f80e --- /dev/null +++ b/fusion_plating/scripts/fp_bol_stress.py @@ -0,0 +1,33 @@ +# Stress-test the BoL with progressively longer notes +env = env # noqa +import re +rep = env.ref('fusion_plating_reports.action_report_fp_bol_landscape') +dlv = env['fusion.plating.delivery'].search([], order='id desc', limit=1) +orig_notes = dlv.notes +print(f'baseline (current notes={len(orig_notes or "") } chars)') + +scenarios = [ + ('empty notes', '', 1), + ('one short line', '

Handle with care.

', 1), + ('three lines', '

Line 1

Line 2

Line 3

', 1), + ('ten lines', ''.join(f'

Special instruction line {i}: handle with care, fragile, do not stack.

' for i in range(10)), 1), + ('paragraph block', '

' + ('Long instructions filling the cargo description box. ' * 30) + '

', 1), + ('huge block', '

' + ('Very long instructions. ' * 80) + '

', 1), +] +print(f'\n{"scenario":<22} {"chars":<8} {"pages":<6} signature row intact?') +print('-' * 70) +for label, notes, _ in scenarios: + dlv.write({'notes': notes}) + pdf, _e = rep.with_context(force_report_rendering=True + )._render_qweb_pdf(rep.report_name, [dlv.id]) + n_pages = len(re.findall(rb'/Type\s*/Page[^s]', pdf)) + # Save last for inspection + with open(f'/tmp/bol_stress_{label.replace(" ","_")}.pdf', 'wb') as f: + f.write(pdf) + # Quick "intact" heuristic: if it's >1 page and the size is small, + # likely overflowed. Real check is in pypdf locally. + print(f'{label:<22} {len(notes):<8} {n_pages:<6} (PDF saved)') + +# Restore +dlv.write({'notes': orig_notes or False}) +print('\noriginal notes restored') diff --git a/fusion_plating/scripts/fp_grep.py b/fusion_plating/scripts/fp_grep.py new file mode 100644 index 00000000..d6bb956a --- /dev/null +++ b/fusion_plating/scripts/fp_grep.py @@ -0,0 +1,22 @@ +env = env # noqa +import re +rep = env.ref('fusion_plating_reports.action_report_fp_bol_portrait') +dlv = env['fusion.plating.delivery'].search([], order='id desc', limit=1) +html, _ = rep.with_context(force_report_rendering=True + )._render_qweb_html(rep.report_name, [dlv.id]) +out = html.decode() if isinstance(html, bytes) else str(html) +# Extract the sig-table block + 200 chars before and after +m = re.search(r'(.{0,400})(
)(.{0,200})', out, re.S) +if m: + print('=== before ===') + print(m.group(1)[-300:]) + print('=== sig-table ===') + print(m.group(2)) +else: + print('NOT FOUND. Looking for any sig-table:') + for m in re.finditer(r']*sig[^>]*>', out): + print(' ', m.group(0)) + # Also search for the labels + for label in ['Shipper (Signature', 'sig-cell', 'sig-table', 'sig-box']: + i = out.find(label) + print(f' {label!r}: pos={i}')