This commit is contained in:
gsinghpal
2026-05-21 04:47:45 -04:00
parent 3440e4b7c6
commit d6d6249857
10 changed files with 610 additions and 301 deletions

View File

@@ -29,7 +29,7 @@ Fusion Plating is a multi-module Odoo 19 ERP for electroless nickel plating and
| **Report border rendering** | After two failed attempts (px→mm conversion + dpi bump; then `border-collapse: separate` single-side-per-cell), settled on **`border-collapse: collapse` + longhand borders + `background-clip: padding-box`**. Verticals are a hair softer than horizontals on entech wkhtmltopdf — accepted as the lesser evil vs misaligned tables. See rule 14a, last paragraph. **Don't retry the single-side pattern.** | `fusion_plating_reports` |
| **Page-break-inside: avoid placement** | When a long QWeb report dumps content into multi-page PDFs via wkhtmltopdf, the company header (rendered as `--header-html`) can overlap body content if a page break lands mid-row in a table. **Apply `page-break-inside: avoid` to `<tr>` elements** (and to wrapper `<div>`s that wrap whole logical sections like signature blocks), not to `<table>`. On entech wkhtmltopdf, `<table>`-level `page-break-inside` is unreliable when the table is long enough to definitely break; per-row is honoured. Pattern: keep individual readings/rows together so the wkhtmltopdf header zone never overlaps mid-row content. Wrap the larger logical block (cert thickness section, signature + certification statement) in `<div style="page-break-inside: avoid;">` to keep it together when it fits and naturally wrap to a fresh page when it doesn't. | `fusion_plating_reports/report/report_coc.xml` |
| **`opacity` + `italic` muted text renders jagged on entech wkhtmltopdf** | The obvious pattern for a subtle footnote — `font-style: italic; opacity: 0.7;` (used by `.fp-coc .small-label`) — produces washed-out, jagged characters that look "broken" or "messed up" on the printed PDF. Visually it reads as garbled text even though the source is clean. **Use solid grey (`color: #555`) at normal weight instead** for muted secondary text. Same workaround applies to any `opacity`-driven greyed-out element bound for wkhtmltopdf. The existing `.small-label` class still exists for legacy callers but new code should prefer an explicit `color:` style. | `fusion_plating_reports` |
| **wkhtmltopdf header overlap — paperformat.margin_top, NOT body padding-top** | The wkhtmltopdf header zone is sized by `report.paperformat.margin_top` (and `header_spacing`). If the `web.external_layout` header (logo + address etc.) renders ~28mm tall but paperformat reserves only 8mm, page 2+ has the header bleeding over body content (the overlap shows up as the company logo printed *on top of* the signature, readings table, etc.). The anti-pattern is "fix" it by adding `padding-top: 50mm` to the body wrapper — this only pads page 1 (single one-shot padding) and does nothing for subsequent pages, while also wasting 50mm of usable space on page 1. **Right fix:** size `paperformat.margin_top` to the actual rendered header height, then drop body `padding-top` to a tiny visual gap (~5mm). Each report can have its own paperformat — `report_coc_en` / `report_coc_fr` use "Fusion Plating CoC" (id 13); the legacy `report_coc` uses "A4 Landscape (Fusion Plating)" (id 12). Update the right one and don't bleed changes across reports. | `fusion_plating_reports`, `report.paperformat` |
| **wkhtmltopdf header overlap — paperformat.margin_top, NOT body padding-top** | The wkhtmltopdf header zone is sized by `report.paperformat.margin_top` (and `header_spacing`). If the `web.external_layout` header (logo + address etc.) renders ~28mm tall but paperformat reserves only 8mm, page 2+ has the header bleeding over body content (the overlap shows up as the company logo printed *on top of* the signature, readings table, etc.). The anti-pattern is "fix" it by adding `padding-top: 50mm` to the body wrapper — this only pads page 1 (single one-shot padding) and does nothing for subsequent pages, while also wasting 50mm of usable space on page 1. **Right fix:** size `paperformat.margin_top` to the actual rendered header height, then drop body `padding-top` to a tiny visual gap (~5mm). Each report can have its own paperformat — `report_coc_en` / `report_coc_fr` use "Fusion Plating CoC" (id 13); the legacy `report_coc` uses "A4 Landscape (Fusion Plating)" (id 12). Update the right one and don't bleed changes across reports. **Corollary — don't use negative `margin-top` to "tighten" the gap** (e.g. `.my-page { margin-top: -10px; }` to pull the H1 up under the header). The body wrapper sits at the bottom edge of the reserved margin_top zone; any negative margin pushes content INTO the header band, where wkhtmltopdf clips the top of glyphs (looks like the title is half-eaten). If the gap really feels too big, shrink the title font instead, or reduce `paperformat.margin_top` so the entire header zone is shorter. **For customer-facing portrait reports** (SO confirmation, quote, invoice, packing slip, BoL) the canonical compact paperformat is `fusion_plating_reports.paperformat_fp_a4_portrait` (margin_top=22mm, header_spacing=3mm, keeps the standard header band). Bind it via `<field name="paperformat_id" ref="fusion_plating_reports.paperformat_fp_a4_portrait"/>` rather than creating yet-another-one. **Two compounding-padding traps to be aware of:** (1) Odoo's `.page` class has `padding: 1cm` baked in (Bootstrap-derived). If you wrap your body in `<div class="page">` AND add a body `padding-top: 15mm`, you get the paperformat margin_top + 10mm Odoo + 15mm yours = ~65mm of dead space above the title. To remove the .page contribution without losing its left/right padding, override only the top: `.fp-report.fp-sale .page { padding-top: 0 !important; }`. CoC sidesteps this by NOT using an inner `.page` div — it wraps directly in `<div class="fp-coc">` and puts padding on that. (2) The base `.fp-report table.bordered th, .fp-report table.bordered td` rule applies borders explicitly, BUT a separate cascade still bleeds borders onto NESTED `<table>` elements even when the inner table has no `.bordered` class — `border: 0 !important` on the cells does NOT reliably override it (some wkhtmltopdf rendering paths still draw the lines). **Don't use a `<table>` for non-bordered layouts** like a title/barcode strip; use `<div>` + `float: right` / flexbox instead. Saves an hour of CSS specificity arguments with wkhtmltopdf. (3) **CSS comments inside QWeb `<style>` blocks are XML-parsed** — writing `/* don't use a <table> here */` makes lxml see a literal `<table>` opening tag and the file fails to load with `XMLSyntaxError: Opening and ending tag mismatch`. Strip the angle brackets from any HTML-like literals in CSS comments: write `/* don't use a table here */` or quote it as `"<table>"`. | `fusion_plating_reports`, `report.paperformat` |
| **CoC + thickness = ONE cert (page 2 merge OR inline body)** | When a customer has both `x_fc_send_coc` and `x_fc_send_thickness_report` on (or part has `certificate_requirement='coc_thickness'`), `_resolve_required_cert_types` returns **`{'coc'}` only**. Standalone `thickness_report` certs are only created when CoC is OFF and thickness is ON (rare). The earlier "two certs" behavior was a bug — don't restore it. **Two rendering paths exist for the thickness data in the CoC PDF:** (1) **Page-2 PDF merge** via `_fp_merge_thickness_into_pdf` — used when there's a real PDF source (operator uploaded a Fischerscope PDF, or QC has `thickness_report_pdf_id`). (2) **Inline readings table in the CoC body** — used when `thickness_reading_ids` is populated but there's no PDF source (e.g. RTF upload parsed to readings, manually typed readings). Lives in `report_coc.xml` between the parts table and the signature block, gated on `doc.thickness_reading_ids`. Both can coexist on a cert — PDF merges as page 2, readings render inline; usually only one path has data per cert. | `fusion_plating_jobs`, `fusion_plating_certificates`, `fusion_plating_reports` |
| **Smart-button "create or view" pattern** | For a smart button that toggles between "create" and "view" states, use **one** idempotent button with `widget="statinfo"`, not two sibling buttons gated by mutually-exclusive `invisible` expressions. Custom `<div class="o_stat_info">` without `<span class="o_stat_value">` renders awkwardly in Odoo 19 (numbers + label expected); `statinfo` handles the standard structure automatically. The action method itself should branch on whether the linked record exists (create-then-open or just open). | any module with smart buttons |
| **stock.move.name removed** | Odoo 19 dropped the `name` field on `stock.move`. Passing `name` in a create dict raises `ValueError: Invalid field 'name' on model 'stock.move'`. Use `description_picking` instead (the operator-facing line label on the picking). The DB column is gone too — `name` doesn't exist as a stored field. | any code that builds stock.move records |
@@ -43,6 +43,7 @@ Fusion Plating is a multi-module Odoo 19 ERP for electroless nickel plating and
| **entech apt is broken — install new packages via `dpkg -i` bypass** | LXC 111's apt state has pre-existing breakage that blocks ANY `apt install`: `python3-lxml-html-clean` not installable on Bookworm but odoo's deb depends on it, `postgresql-15-pgvector` Breaks `postgresql-15-jit-llvm (< 19)`, `libglu1-mesa`/`libglx-mesa0` installed without their Mesa sub-deps (libopengl0, libdrm2, libxfixes3…), `postgresql-15` itself in `iF` half-configured state. Apt's global resolver refuses ALL installs until these are fixed. Workaround that worked for ImageMagick + libwmf: `apt-get download` the target debs into a tmp dir, then `dpkg -i *.deb` — dpkg only checks the direct deps of what you're installing, not the system-wide health. Use this pattern when entech needs new system packages; **don't try `apt --fix-broken install`** without coordinating with whoever owns the box — fixing pgvector/lxml-html-clean could cascade into Odoo or PostgreSQL changes. Installed this way: `imagemagick`, `imagemagick-6-common`, `imagemagick-6.q16`, `libmagickcore-6.q16-6`, `libmagickwand-6.q16-6`, `libwmf-0.2-7`, `libwmflite-0.2-7`, `libwmf-bin`, `libfftw3-double3`, `liblqr-1-0`, `hicolor-icon-theme` (2026-05-21, ~4 MB total). WMF→raster path: `wmf2svg input.wmf -o out.svg` writes a thin SVG referencing `out-N.png` side-files (libwmf unpacks raster blocks inside the metafile). ImageMagick's `convert` lacks the WMF delegate on Debian Bookworm — use wmf2svg for raster extraction, not `convert input.wmf out.png`. | any new system package install on entech LXC 111 |
| **Fischerscope XDAL 600 `.doc` files are actually RTF** | Helmut Fischer's XDAL 600 XRF software exports thickness reports with a `.doc` extension but the file contents are **RTF** (`{\\rtf1\\ansi…`), not Microsoft Word binary `.doc`. `file(1)` confirms: `Rich Text Format data, version 1`. python-docx will refuse to open it, and the filename-based dispatch (`endswith('.docx')`) silently skips parsing. **Don't reach for libreoffice/antiword.** Detect by **magic bytes** (`raw_bytes[:5] == b'{\\\\rtf'`) and route through `_fp_parse_fischerscope_rtf` instead — it strips RTF control words with regex and runs the same Fischerscope reading regex as the .docx path. The image data embedded as hex inside `{\\pict ...}` blocks must be stripped FIRST or the reading regex will choke on multi-MB image hex. | `fusion_plating_jobs/wizards/fp_cert_issue_wizard.py` |
| **entech apt — which conversion tools are available** | The host has pre-existing broken deps (`python3-lxml-html-clean` missing, `postgresql-15-pgvector` vs `postgresql-15-jit-llvm` conflict, various Mesa packages) that make new `apt install` calls fragile — they often abort partway through dep resolution. **Currently installed and usable:** `convert` (ImageMagick 6), `wmf2svg`, `wmf2eps` (libwmf-bin). **Not installed:** `libreoffice`, `unoconv`, `pandoc`, `wmf2png`. Don't assume the next `apt install` will go through — always run `which <tool>` first and design the feature to soft-fail if the tool isn't there (see `_fp_extract_rtf_images` for the pattern: shell out, catch `FileNotFoundError`/`TimeoutExpired`, fall back to "no image" instead of crashing the cert flow). For WMF → PNG specifically: `wmf2svg` writes both SVG and a side-file `*-N.png` per embedded raster — use that, not `convert input.wmf` (no WMF delegate). For new tools: check pure-Python alternatives first (Pillow without backends, pypdf, openpyxl) before reaching for apt. | any feature wanting to convert docs/images server-side |
| **Customer-facing reports use bilingual EN/FR labels** | Every customer-facing report label (column titles, section banners, totals, document title) renders English first and French second. **Default to inline slash format** ("English / French" on one line) — easier to scan and saves vertical space. **Use the stacked variant only for cells too narrow** for the French word to fit on the same line (QTY, UOM, narrow column headers in dense tables). CSS classes live in the `fp_sale_bilingual_styles` template in `report_fp_sale.xml`. **Inline (default):** `.fp-bl-en { font-weight:bold; }` + `.fp-bl-sep { color:#999; margin:0 3px; }` + `.fp-bl-fr { font-weight:normal; font-style:italic; color:#555; }`. Pattern: `<span class="fp-bl-en">English</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">French</span>`. **Stacked (narrow cells):** `.fp-bl-en-stk` + `.fp-bl-fr-stk` (each `display:block`). **Always render both spans even when EN and FR are the same word** (e.g. "Description / Description", "Taxes / Taxes") — visual consistency across the row matters more than the redundancy; dropping the FR span on identical-word labels leaves an obvious gap when scanning down a column of headers. When a report has a barcode block, encode `doc.name` via `ir.actions.report.barcode_data_uri('Code128', doc.name, 600, 100)` (the helper inlines a data URI — don't `/report/barcode/...` over HTTP, wkhtmltopdf network fetches fail on entech). Apply to ALL outward-facing reports (SO confirmation, quote, invoice, CoC, packing slip, BoL); internal-only reports (job traveller, WO sticker) can stay English. | `fusion_plating_reports/report/report_fp_sale.xml` (canonical), every customer-facing report |
### Pending — IN PROGRESS when this session ended

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Configurator',
'version': '19.0.21.5.1',
'version': '19.0.21.5.2',
'category': 'Manufacturing/Plating',
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
'description': """

View File

@@ -144,11 +144,9 @@
string="Job #"/>
</list>
</field>
<!-- Row 1: RFQ/PO (left) + Scheduling (right) — pairs the two
tallest groups so neither column dangles empty. -->
<group>
<group string="Configurator (legacy)" invisible="not x_fc_configurator_id">
<field name="x_fc_configurator_id" readonly="1"/>
<field name="x_fc_process_summary" readonly="1"/>
</group>
<group string="RFQ / PO">
<field name="x_fc_po_number"/>
<field name="upload_rfq_file"
@@ -174,29 +172,6 @@
<field name="x_fc_po_override_reason"
invisible="not x_fc_po_override"/>
</group>
</group>
<group>
<group string="Invoicing">
<field name="x_fc_invoice_strategy"/>
<field name="x_fc_deposit_percent"
invisible="x_fc_invoice_strategy != 'deposit'"/>
<field name="x_fc_progress_initial_percent"
invisible="x_fc_invoice_strategy != 'progress'"/>
<field name="x_fc_final_invoice_id" readonly="1"
invisible="not x_fc_final_invoice_id"/>
</group>
<group string="Delivery">
<field name="x_fc_rush_order"/>
<field name="x_fc_delivery_method"/>
<field name="x_fc_receiving_status"/><!-- Will become computed when fusion_plating_receiving is installed -->
</group>
</group>
<group>
<group string="Customer Reference">
<field name="x_fc_customer_job_number"/>
<field name="x_fc_contact_phone"/>
<field name="x_fc_ship_via"/>
</group>
<group string="Scheduling">
<field name="x_fc_planned_start_date"/>
<field name="x_fc_internal_deadline"/>
@@ -216,7 +191,33 @@
<field name="x_fc_block_partial_shipments"/>
</group>
</group>
<!-- Row 2: Invoicing + Delivery (unchanged pairing). -->
<group>
<group string="Invoicing">
<field name="x_fc_invoice_strategy"/>
<field name="x_fc_deposit_percent"
invisible="x_fc_invoice_strategy != 'deposit'"/>
<field name="x_fc_progress_initial_percent"
invisible="x_fc_invoice_strategy != 'progress'"/>
<field name="x_fc_final_invoice_id" readonly="1"
invisible="not x_fc_final_invoice_id"/>
</group>
<group string="Delivery">
<field name="x_fc_rush_order"/>
<field name="x_fc_delivery_method"/>
<field name="x_fc_receiving_status"/><!-- Will become computed when fusion_plating_receiving is installed -->
</group>
</group>
<!-- Row 3: Customer Reference + Margin — both short groups, so
pairing them keeps the right column from going blank. -->
<group>
<group string="Customer Reference">
<field name="x_fc_customer_job_number"/>
<field name="x_fc_contact_phone"/>
<field name="x_fc_ship_via"/>
</group>
<group string="Margin">
<div colspan="2"
invisible="x_fc_margin_available"
@@ -235,14 +236,29 @@
<field name="x_fc_margin_available" invisible="1"/>
</group>
</group>
<!-- Row 4: Notes — two side-by-side textareas instead of the
previous broken separator-in-group layout. -->
<group>
<group string="Internal Notes">
<field name="x_fc_internal_note" nolabel="1"
placeholder="Internal notes for estimator / planner / shop floor..."/>
</group>
<separator string="External Notes (customer-visible)"/>
<field name="x_fc_external_note"
<group string="External Notes (customer-visible)">
<field name="x_fc_external_note" nolabel="1"
placeholder="Notes that appear on the acknowledgement and portal..."/>
</group>
</group>
<!-- Legacy configurator block — invisible on new SOs (only
the handful that came through the old quote configurator
flow have x_fc_configurator_id set). Kept at the bottom
so it doesn't waste vertical space on the common case. -->
<group invisible="not x_fc_configurator_id">
<group string="Configurator (legacy)">
<field name="x_fc_configurator_id" readonly="1"/>
<field name="x_fc_process_summary" readonly="1"/>
</group>
</group>
</page>
</xpath>

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Logistics',
'version': '19.0.3.10.0',
'version': '19.0.3.11.0',
'category': 'Manufacturing/Plating',
'summary': (
'Pickup & delivery for plating shops: vehicle master, driver '

View File

@@ -260,6 +260,58 @@ class FpDelivery(models.Model):
def _fp_parent_counter_field(self):
return 'x_fc_pn_delivery_count'
def action_view_coc(self):
"""Open the certificate record this delivery's CoC PDF came
from. The attachment carries res_model + res_id, so we
navigate to that record (operator gets all cert info — issue
date, void wizard, reset, etc.) rather than just opening the
raw PDF. Falls back to opening the attachment directly if
someone manually attached a PDF that isn't a cert.
"""
self.ensure_one()
att = self.coc_attachment_id
if not att:
raise UserError(_('No CoC linked to this delivery.'))
if att.res_model == 'fp.certificate' and att.res_id:
return {
'type': 'ir.actions.act_window',
'name': _('Certificate of Conformance'),
'res_model': 'fp.certificate',
'res_id': att.res_id,
'view_mode': 'form',
'target': 'current',
}
# Plain attachment — open via PDF preview helper if available.
if hasattr(att, 'action_fusion_preview'):
return att.action_fusion_preview(title=att.name or 'CoC')
return {
'type': 'ir.actions.act_url',
'url': '/web/content/%d?download=false' % att.id,
'target': 'new',
}
def action_view_packing_list(self):
"""Open the packing-list PDF via fusion_pdf_preview (or fall
back to a new tab when the preview helper isn't installed).
Packing lists don't have a backing model — they're attachments
only — so we don't navigate to a record.
"""
self.ensure_one()
att = self.packing_list_attachment_id
if not att:
raise UserError(_('No packing list attached to this delivery.'))
if hasattr(att, 'action_fusion_preview'):
return att.action_fusion_preview(
title=att.name or 'Packing List',
model_name=self._name,
record_ids=self.id,
)
return {
'type': 'ir.actions.act_url',
'url': '/web/content/%d?download=false' % att.id,
'target': 'new',
}
def action_refresh_from_source(self):
"""Re-pull delivery address / contact / scheduled date / source
facility / carrier / CoC from the linked job → SO → receiving →

View File

@@ -79,6 +79,30 @@
widget="statinfo"
string="Outbound Shipment"/>
</button>
<!-- CoC smart button → cert record (not the
raw PDF — operator can print/reset/void
from the cert form). -->
<button name="action_view_coc"
type="object"
class="oe_stat_button"
icon="fa-certificate"
invisible="not coc_attachment_id">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">CoC</span>
</div>
</button>
<!-- Packing list smart button → PDF preview
dialog (packing lists are attachments
only, no backing model to navigate to). -->
<button name="action_view_packing_list"
type="object"
class="oe_stat_button"
icon="fa-list-alt"
invisible="not packing_list_attachment_id">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Packing List</span>
</div>
</button>
</div>
<div class="oe_title">
<label for="name"/>

View File

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

View File

@@ -41,6 +41,34 @@
<field name="dpi">90</field>
</record>
<!-- ============================================================= -->
<!-- Compact A4 Portrait for customer-facing reports -->
<!-- (SO confirmation, quotation, invoice, packing slip, BoL). -->
<!-- Keeps the external_layout header band (logo + company addr) -->
<!-- but shrinks the reserved zone from Odoo's default ~40mm to -->
<!-- 22mm so the document title sits ~5mm under the logo instead -->
<!-- of 30mm. header_spacing kept at 3mm so the header HTML never -->
<!-- bleeds into body content on a page break. See CLAUDE.md row -->
<!-- "wkhtmltopdf header overlap" for the underlying mechanic. -->
<!-- ============================================================= -->
<record id="paperformat_fp_a4_portrait" model="report.paperformat">
<field name="name">Fusion Plating A4 Portrait (Compact)</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<!-- margin_top sized for the standard FP header (ENTECH logo +
2-line company address). Earlier 22mm clipped it — the logo
+ name + address actually need ~28mm. 32mm leaves a small
clean gap before the title. Tighter than Odoo's 40mm default. -->
<field name="margin_top">32</field>
<field name="margin_bottom">15</field>
<field name="margin_left">10</field>
<field name="margin_right">10</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">3</field>
<field name="dpi">90</field>
</record>
<!-- ============================================================= -->
<!-- 1. Certificate of Conformance (Portal Job) — Landscape -->
<!-- ============================================================= -->
@@ -266,6 +294,14 @@
<field name="print_report_name">(object.state in ('draft', 'sent') and 'Quotation - %s' % object.name) or 'Order - %s' % object.name</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
<!-- Uses Odoo's default paperformat so web.external_layout's
header/footer band gets its reserved space correctly (same
approach as report_coc_en / report_coc_fr). Title spacing
below the header is controlled by `padding-top` on the body
wrapper in report_fp_sale.xml — NOT by a custom paperformat,
since trimming the paperformat margin makes the header HTML
bleed into the body. See CLAUDE.md "wkhtmltopdf header
overlap" for the underlying mechanic. -->
</record>
<record id="action_report_fp_sale_landscape" model="ir.actions.report">

View File

@@ -3,11 +3,150 @@
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Fusion Plating — Packing Slip / Shipping Confirmation (Portrait + Landscape).
Binds to stock.picking. Shows parts, quantities, lot/serial tracking,
and a receiver sign-off.
Binds to stock.picking. Bill-To / Ship-To boxes, bilingual column
headers, Received-By signature block and a QR code for scan-to-sign.
-->
<odoo>
<!-- ============================================================= -->
<!-- Shared bits -->
<!-- ============================================================= -->
<template id="fp_packing_slip_styles">
<style>
.fp-ps-addrtable td { vertical-align: top; padding: 8px 10px; font-size: 10pt; }
.fp-ps-addrtable .fp-ps-addr-label { font-weight: bold; font-size: 9pt; color: #333; text-transform: uppercase; margin-bottom: 4px; }
.fp-ps-info-table th { background-color: #eaeaea; }
.fp-ps-info-table td { text-align: center; font-size: 11pt; padding: 8px; }
.fp-ps-items-table th { font-size: 8.5pt; line-height: 1.1; padding: 4px 4px; }
.fp-ps-items-table th .fp-fr { display: block; font-weight: normal; color: #555; font-size: 7.5pt; }
.fp-ps-items-table td { font-size: 9.5pt; padding: 5px 5px; }
.fp-ps-num { text-align: center; }
.fp-ps-sig-table td { padding: 10px 12px; vertical-align: top; }
.fp-ps-sig-line { border-bottom: 1px solid #000; min-height: 38px; margin-top: 4px; }
.fp-ps-sig-label { font-weight: bold; font-size: 9pt; text-transform: uppercase; color: #333; }
.fp-ps-sig-sub { font-size: 8pt; color: #666; }
.fp-ps-qr-box { text-align: center; padding: 6px; }
.fp-ps-qr-box img { width: 110px; height: 110px; display: inline-block; }
.fp-ps-qr-caption { font-size: 9pt; color: #333; margin-top: 4px; line-height: 1.2; }
.fp-ps-qr-caption .fp-fr { display: block; color: #666; font-size: 8pt; }
</style>
</template>
<!-- Address box content (shared by portrait + landscape) -->
<template id="fp_packing_slip_addr_block">
<div class="fp-ps-addr-label" t-esc="label"/>
<strong><span t-esc="partner.name or ''"/></strong>
<div t-field="partner"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
</template>
<!-- Items table (shared markup; only widths change between layouts) -->
<template id="fp_packing_slip_items">
<table class="bordered fp-ps-items-table">
<thead>
<tr>
<th t-att-style="w_ordered or 'width: 8%;'">
Ordered<span class="fp-fr">Comm.</span>
</th>
<th t-att-style="w_shipped or 'width: 8%;'">
Shipped<span class="fp-fr">EXP</span>
</th>
<th t-att-style="w_bo or 'width: 8%;'">
B/O<span class="fp-fr">À venir</span>
</th>
<th class="text-start" t-att-style="w_part or 'width: 17%;'">
Part Number<span class="fp-fr">N° de pièce</span>
</th>
<th t-att-style="w_po or 'width: 11%;'">
PO<span class="fp-fr">B/C</span>
</th>
<th t-att-style="w_wo or 'width: 11%;'">
WO<span class="fp-fr">B/T</span>
</th>
<th t-att-style="w_process or 'width: 14%;'">
Process<span class="fp-fr">Procédé</span>
</th>
<th class="text-start" t-att-style="w_desc or 'width: 23%;'">
Description
</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.move_ids_without_package" t-as="move">
<t t-set="line" t-value="move.sale_line_id or move"/>
<t t-set="ordered_qty" t-value="move.product_uom_qty or 0.0"/>
<t t-set="done_qty" t-value="move.quantity or 0.0"/>
<t t-set="bo_qty" t-value="ordered_qty - done_qty if ordered_qty &gt; done_qty else 0.0"/>
<t t-set="wo_job" t-value="doc.env['fp.job'].search([('sale_order_line_ids', 'in', move.sale_line_id.ids)], limit=1) if move.sale_line_id else doc.env['fp.job']"/>
<t t-set="proc_variant" t-value="(move.sale_line_id.x_fc_process_variant_id if move.sale_line_id and 'x_fc_process_variant_id' in move.sale_line_id._fields else False)"/>
<t t-set="proc_label" t-value="(proc_variant.variant_label or proc_variant.name) if proc_variant else ((move.sale_line_id.x_fc_part_catalog_id.default_process_id.variant_label or move.sale_line_id.x_fc_part_catalog_id.default_process_id.name) if move.sale_line_id and move.sale_line_id.x_fc_part_catalog_id and move.sale_line_id.x_fc_part_catalog_id.default_process_id else '')"/>
<tr>
<td class="fp-ps-num">
<span t-esc="int(ordered_qty) if ordered_qty == int(ordered_qty) else ordered_qty"/>
</td>
<td class="fp-ps-num">
<span t-esc="int(done_qty) if done_qty == int(done_qty) else done_qty"/>
</td>
<td class="fp-ps-num">
<span t-esc="int(bo_qty) if bo_qty == int(bo_qty) else bo_qty"/>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
</td>
<td class="fp-ps-num">
<span t-esc="po_number or '-'"/>
</td>
<td class="fp-ps-num">
<t t-if="wo_job"><span t-esc="wo_job.name"/></t>
<t t-else="">-</t>
</td>
<td class="fp-ps-num">
<span t-esc="proc_label or '-'"/>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_description"/>
</td>
</tr>
</t>
</tbody>
</table>
</template>
<!-- Signature + QR strip (shared) -->
<template id="fp_packing_slip_signoff">
<table class="bordered fp-ps-sig-table" style="margin-top: 14px;">
<tbody>
<tr>
<td style="width: 38%;">
<div class="fp-ps-sig-label">
Received By
<span style="font-weight: normal; color: #666; font-size: 8pt;"> / Reçu par</span>
</div>
<div class="fp-ps-sig-line"/>
<div class="fp-ps-sig-sub">Print name &amp; signature</div>
</td>
<td style="width: 32%;">
<div class="fp-ps-sig-label">
Received Date
<span style="font-weight: normal; color: #666; font-size: 8pt;"> / Date de réception</span>
</div>
<div class="fp-ps-sig-line"/>
<div class="fp-ps-sig-sub">YYYY-MM-DD</div>
</td>
<td style="width: 30%;" class="fp-ps-qr-box">
<t t-if="qr_uri">
<img t-att-src="qr_uri" alt="QR Code"/>
</t>
<div class="fp-ps-qr-caption">
Scan the QR Code to Sign
<span class="fp-fr">Scannez le code QR pour signer</span>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<!-- ============================================================= -->
<!-- PORTRAIT -->
<!-- ============================================================= -->
@@ -16,6 +155,25 @@
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<t t-call="fusion_plating_reports.fp_packing_slip_styles"/>
<!-- =========================================
Pre-compute fields from the picking chain.
doc → stock.picking, doc.sale_id → SO,
partner_invoice_id → bill-to (falls back
to commercial_partner). carrier presence
decides "Ready for pick up" vs tracking ref.
========================================= -->
<t t-set="bill_partner" t-value="(doc.sale_id.partner_invoice_id if doc.sale_id and doc.sale_id.partner_invoice_id else (doc.partner_id.commercial_partner_id or doc.partner_id))"/>
<t t-set="ship_partner" t-value="doc.partner_id"/>
<t t-set="has_carrier" t-value="'carrier_id' in doc._fields and doc.carrier_id"/>
<t t-set="ship_via" t-value="(doc.carrier_id.name if has_carrier else (doc.sale_id.x_fc_ship_via if doc.sale_id and 'x_fc_ship_via' in doc.sale_id._fields and doc.sale_id.x_fc_ship_via else 'CUSTOMER PICKUP'))"/>
<t t-set="tracking_ref" t-value="doc.carrier_tracking_ref if 'carrier_tracking_ref' in doc._fields and doc.carrier_tracking_ref else False"/>
<t t-set="tracking_text" t-value="tracking_ref if tracking_ref else ('Ready for pick up' if not has_carrier else '—')"/>
<t t-set="po_number" t-value="(doc.sale_id.client_order_ref if doc.sale_id and doc.sale_id.client_order_ref else '')"/>
<t t-set="qr_payload" t-value="doc.name or ''"/>
<t t-set="qr_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('QR', qr_payload, 220, 220) if qr_payload else False"/>
<div class="fp-report">
<div class="page">
@@ -24,92 +182,67 @@
<span t-field="doc.name"/>
</h4>
<!-- From / To -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">FROM</th>
<th style="width: 50%;">SHIP TO</th>
</tr>
</thead>
<!-- Bill To / Ship To -->
<table class="bordered fp-ps-addrtable">
<tbody>
<tr>
<td style="height: 80px;">
<strong><span t-field="doc.company_id.name"/></strong><br/>
<div t-field="doc.company_id.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 80px;">
<strong><span t-field="doc.partner_id.name"/></strong><br/>
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone'], 'no_marker': True}"/>
</td>
</tr>
</tbody>
</table>
<!-- Shipment info -->
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 25%;">SHIP DATE</th>
<th class="info-header" style="width: 25%;">SOURCE</th>
<th class="info-header" style="width: 25%;">OPERATION</th>
<th class="info-header" style="width: 25%;">CARRIER</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-esc="doc.origin or '-'"/></td>
<td class="text-center"><span t-field="doc.picking_type_id"/></td>
<td class="text-center">
<t t-if="'carrier_id' in doc._fields and doc.carrier_id">
<span t-field="doc.carrier_id"/>
<td style="width: 50%;">
<t t-call="fusion_plating_reports.fp_packing_slip_addr_block">
<t t-set="label" t-value="'Bill To:'"/>
<t t-set="partner" t-value="bill_partner"/>
</t>
</td>
<td style="width: 50%;">
<t t-call="fusion_plating_reports.fp_packing_slip_addr_block">
<t t-set="label" t-value="'Ship To:'"/>
<t t-set="partner" t-value="ship_partner"/>
</t>
<t t-else="">-</t>
</td>
</tr>
</tbody>
</table>
<!-- Products -->
<table class="bordered">
<!-- Ship details -->
<table class="bordered fp-ps-info-table">
<thead>
<tr>
<th class="text-start" style="width: 22%;">PART NUMBER</th>
<th class="text-start" style="width: 34%;">DESCRIPTION</th>
<th style="width: 12%;">QTY</th>
<th style="width: 10%;">UOM</th>
<th style="width: 22%;">LOT / SERIAL</th>
<th style="width: 33%;">
Ship Via<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">Mode d'expédition</span>
</th>
<th style="width: 33%;">
Shipping Date<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">Date d'expédition</span>
</th>
<th style="width: 34%;">
Tracking #<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">N° de suivi</span>
</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.move_ids_without_package" t-as="move">
<tr>
<t t-set="line" t-value="move.sale_line_id or move"/>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_description"/>
</td>
<td class="text-center">
<span t-esc="int(move.quantity) if move.quantity == int(move.quantity) else move.quantity"/>
</td>
<td class="text-center"><span t-field="move.product_uom"/></td>
<td>
<t t-foreach="move.move_line_ids" t-as="ml">
<t t-if="ml.lot_id">
<span t-field="ml.lot_id.name"/><br/>
</t>
</t>
</td>
</tr>
</t>
<tr>
<td><span t-esc="ship_via"/></td>
<td>
<t t-if="doc.scheduled_date">
<span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/>
</t>
<t t-else=""></t>
</td>
<td><span t-esc="tracking_text"/></td>
</tr>
</tbody>
</table>
<!-- Items -->
<t t-call="fusion_plating_reports.fp_packing_slip_items">
<t t-set="w_ordered" t-value="'width: 8%;'"/>
<t t-set="w_shipped" t-value="'width: 8%;'"/>
<t t-set="w_bo" t-value="'width: 8%;'"/>
<t t-set="w_part" t-value="'width: 17%;'"/>
<t t-set="w_po" t-value="'width: 11%;'"/>
<t t-set="w_wo" t-value="'width: 11%;'"/>
<t t-set="w_process" t-value="'width: 14%;'"/>
<t t-set="w_desc" t-value="'width: 23%;'"/>
</t>
<!-- Notes -->
<t t-if="doc.note">
<div style="margin-top: 10px;">
@@ -118,21 +251,8 @@
</div>
</t>
<!-- Sign off -->
<div class="row" style="margin-top: 30px;">
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Shipper (Signature / Date)</div>
</div>
</div>
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Receiver (Signature / Date)</div>
</div>
</div>
</div>
<!-- Sign-off + QR -->
<t t-call="fusion_plating_reports.fp_packing_slip_signoff"/>
</div>
</div>
@@ -149,6 +269,18 @@
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<t t-call="fusion_plating_reports.fp_packing_slip_styles"/>
<t t-set="bill_partner" t-value="(doc.sale_id.partner_invoice_id if doc.sale_id and doc.sale_id.partner_invoice_id else (doc.partner_id.commercial_partner_id or doc.partner_id))"/>
<t t-set="ship_partner" t-value="doc.partner_id"/>
<t t-set="has_carrier" t-value="'carrier_id' in doc._fields and doc.carrier_id"/>
<t t-set="ship_via" t-value="(doc.carrier_id.name if has_carrier else (doc.sale_id.x_fc_ship_via if doc.sale_id and 'x_fc_ship_via' in doc.sale_id._fields and doc.sale_id.x_fc_ship_via else 'CUSTOMER PICKUP'))"/>
<t t-set="tracking_ref" t-value="doc.carrier_tracking_ref if 'carrier_tracking_ref' in doc._fields and doc.carrier_tracking_ref else False"/>
<t t-set="tracking_text" t-value="tracking_ref if tracking_ref else ('Ready for pick up' if not has_carrier else '—')"/>
<t t-set="po_number" t-value="(doc.sale_id.client_order_ref if doc.sale_id and doc.sale_id.client_order_ref else '')"/>
<t t-set="qr_payload" t-value="doc.name or ''"/>
<t t-set="qr_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('QR', qr_payload, 220, 220) if qr_payload else False"/>
<div class="fp-landscape">
<div class="page">
@@ -157,104 +289,67 @@
<span t-field="doc.name"/>
</h2>
<!-- From / To -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">FROM</th>
<th style="width: 50%;">SHIP TO</th>
</tr>
</thead>
<!-- Bill To / Ship To -->
<table class="bordered fp-ps-addrtable">
<tbody>
<tr>
<td style="height: 80px; font-size: 12pt;">
<strong><span t-field="doc.company_id.name"/></strong><br/>
<div t-field="doc.company_id.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
<td style="width: 50%;">
<t t-call="fusion_plating_reports.fp_packing_slip_addr_block">
<t t-set="label" t-value="'Bill To:'"/>
<t t-set="partner" t-value="bill_partner"/>
</t>
</td>
<td style="height: 80px; font-size: 12pt;">
<strong><span t-field="doc.partner_id.name"/></strong><br/>
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone'], 'no_marker': True}"/>
<td style="width: 50%;">
<t t-call="fusion_plating_reports.fp_packing_slip_addr_block">
<t t-set="label" t-value="'Ship To:'"/>
<t t-set="partner" t-value="ship_partner"/>
</t>
</td>
</tr>
</tbody>
</table>
<!-- Shipment info -->
<table class="bordered info-table">
<!-- Ship details -->
<table class="bordered fp-ps-info-table">
<thead>
<tr>
<th>SHIP DATE</th>
<th>SOURCE</th>
<th>OPERATION</th>
<th>CARRIER</th>
<th>TRACKING REF</th>
<th style="width: 33%;">
Ship Via<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">Mode d'expédition</span>
</th>
<th style="width: 33%;">
Shipping Date<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">Date d'expédition</span>
</th>
<th style="width: 34%;">
Tracking #<span class="fp-fr" style="display:block; font-weight:normal; color:#555; font-size:8pt;">N° de suivi</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-esc="doc.origin or '-'"/></td>
<td class="text-center"><span t-field="doc.picking_type_id"/></td>
<td class="text-center">
<t t-if="'carrier_id' in doc._fields and doc.carrier_id">
<span t-field="doc.carrier_id"/>
<td><span t-esc="ship_via"/></td>
<td>
<t t-if="doc.scheduled_date">
<span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/>
</t>
<t t-else="">-</t>
</td>
<td class="text-center">
<t t-if="'carrier_tracking_ref' in doc._fields">
<span t-esc="doc.carrier_tracking_ref or '-'"/>
</t>
<t t-else="">-</t>
<t t-else=""></t>
</td>
<td><span t-esc="tracking_text"/></td>
</tr>
</tbody>
</table>
<!-- Products -->
<table class="bordered">
<thead>
<tr>
<th class="text-start" style="width: 18%;">PART NUMBER</th>
<th class="text-start" style="width: 26%;">DESCRIPTION</th>
<th style="width: 10%;">ORDERED</th>
<th style="width: 10%;">DONE</th>
<th style="width: 8%;">UOM</th>
<th style="width: 14%;">LOT / SERIAL</th>
<th style="width: 14%;">NOTES</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.move_ids_without_package" t-as="move">
<tr>
<t t-set="line" t-value="move.sale_line_id or move"/>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_description"/>
</td>
<td class="text-center">
<span t-esc="int(move.product_uom_qty) if move.product_uom_qty == int(move.product_uom_qty) else move.product_uom_qty"/>
</td>
<td class="text-center">
<span t-esc="int(move.quantity) if move.quantity == int(move.quantity) else move.quantity"/>
</td>
<td class="text-center"><span t-field="move.product_uom"/></td>
<td>
<t t-foreach="move.move_line_ids" t-as="ml">
<t t-if="ml.lot_id">
<span t-field="ml.lot_id.name"/><br/>
</t>
</t>
</td>
<td/>
</tr>
</t>
</tbody>
</table>
<!-- Items: landscape gets a touch more breathing room on
the description / part columns. -->
<t t-call="fusion_plating_reports.fp_packing_slip_items">
<t t-set="w_ordered" t-value="'width: 7%;'"/>
<t t-set="w_shipped" t-value="'width: 7%;'"/>
<t t-set="w_bo" t-value="'width: 7%;'"/>
<t t-set="w_part" t-value="'width: 16%;'"/>
<t t-set="w_po" t-value="'width: 10%;'"/>
<t t-set="w_wo" t-value="'width: 10%;'"/>
<t t-set="w_process" t-value="'width: 13%;'"/>
<t t-set="w_desc" t-value="'width: 30%;'"/>
</t>
<!-- Notes -->
<t t-if="doc.note">
@@ -264,21 +359,8 @@
</div>
</t>
<!-- Sign off -->
<div class="row" style="margin-top: 30px;">
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Shipper (Signature / Date)</div>
</div>
</div>
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Receiver (Signature / Date)</div>
</div>
</div>
</div>
<!-- Sign-off + QR -->
<t t-call="fusion_plating_reports.fp_packing_slip_signoff"/>
</div>
</div>

View File

@@ -11,28 +11,99 @@
<!-- ============================================================= -->
<!-- PORTRAIT -->
<!-- ============================================================= -->
<!-- Shared bilingual-label snippet. CSS class `.fp-bl` does the
two-line render: English on top, French underneath in a lighter
italic. Stored next to the report's own scss-style block so it
doesn't drift when the same idiom propagates to other reports.
Title sizing: the previous attempt at "compact" (negative
margin-top) pushed the title up INTO the wkhtmltopdf header zone
(the company logo band) and clipped the top of the H1 glyphs.
External_layout already places the page body at the bottom of
the reserved margin-top — don't fight that. Use a small positive
gap and shrink the title text instead. -->
<template id="fp_sale_bilingual_styles">
<style>
/* Inline bilingual: English bold, then a faint slash, then
French italic-grey. Sits on one line where room allows
and wraps to two naturally if the cell is narrow. Apply
this everywhere except super-narrow cells (QTY, UOM)
where the cell is physically too tight even for the
shortest French word — those use the stacked variant
below. */
.fp-bl-en { font-weight: bold; }
.fp-bl-sep { color: #999; margin: 0 3px; font-weight: normal; }
.fp-bl-fr { font-weight: normal; font-style: italic; color: #555; }
/* Stacked variant for narrow cells — EN on top line, FR
below in italic-grey. */
.fp-bl-en-stk { display: block; font-weight: bold; }
.fp-bl-fr-stk { display: block; font-weight: normal; font-style: italic; color: #555; font-size: 80%; margin-top: 1px; }
/* Kill the extra top padding Odoo's `.page` class adds
(1cm by default). The paperformat already reserves
header room — `.page` padding compounds on top of it
and was the source of the giant gap. Keep left/right/
bottom at 1cm so the content isn't flush to the edges. */
.fp-report.fp-sale .page { padding-top: 0 !important; }
/* Title bar uses float-based div layout, NOT an HTML table —
the global ".fp-report table" rule was applying borders
to every nested table even with "border: 0 !important",
so the only reliable fix is to avoid the table element. */
.fp-sale-titlebar { margin: 0 0 8px 0; padding: 0; overflow: hidden; }
.fp-sale-title { font-size: 14pt; line-height: 1.2; color: #2e2e2e; font-weight: bold; }
.fp-sale-title .fp-bl-fr { font-size: 10pt; }
.fp-sale-barcode { float: right; text-align: right; margin-left: 12px; }
.fp-sale-barcode img { height: 34px; max-width: 220px; }
.fp-sale-barcode .fp-bc-label { font-size: 8pt; color: #555; margin-top: 2px; }
</style>
</template>
<template id="report_fp_sale_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<t t-call="fusion_plating_reports.fp_sale_bilingual_styles"/>
<!-- Compute helpers -->
<t t-set="is_quote" t-value="doc.state in ('draft', 'sent')"/>
<t t-set="title_en" t-value="'Quotation' if is_quote else 'Order Confirmation'"/>
<t t-set="title_fr" t-value="'Devis' if is_quote else 'Confirmation de commande'"/>
<t t-set="barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', doc.name, 600, 100) if doc.name else False"/>
<t t-set="spec_label" t-value="(doc.x_fc_customer_spec_id.display_name or doc.x_fc_customer_spec_id.name) if doc.x_fc_customer_spec_id else ''"/>
<t t-set="delivery_method_label" t-value="dict(doc._fields['x_fc_delivery_method'].selection).get(doc.x_fc_delivery_method, '') if 'x_fc_delivery_method' in doc._fields and doc.x_fc_delivery_method else ''"/>
<div class="fp-report fp-sale">
<div class="page">
<!-- Title -->
<h4>
<span t-if="doc.state in ['draft','sent']">Quotation # </span>
<span t-else="">Sales Order # </span>
<span t-field="doc.name"/>
</h4>
<!-- Title bar: bilingual title on the left,
Code128 barcode floated right. NO <table>
— see CLAUDE.md "wkhtmltopdf header
overlap" §2 for why a table here leaks
borders even with `border:0 !important`. -->
<div class="fp-sale-titlebar">
<t t-if="barcode_uri">
<div class="fp-sale-barcode">
<img t-att-src="barcode_uri" alt="Order Barcode"/>
<div class="fp-bc-label"><span t-field="doc.name"/></div>
</div>
</t>
<div class="fp-sale-title">
<span class="fp-bl-en"><t t-esc="title_en"/></span><span class="fp-bl-sep">/</span><span class="fp-bl-fr"><t t-esc="title_fr"/></span>
<span> # </span><span t-field="doc.name"/>
</div>
</div>
<!-- Billing / Shipping -->
<!-- Billing / Shipping (wide cells — inline) -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">BILLING ADDRESS</th>
<th style="width: 50%;">SHIPPING ADDRESS</th>
<th style="width: 50%;">
<span class="fp-bl-en">Billing Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse de facturation</span>
</th>
<th style="width: 50%;">
<span class="fp-bl-en">Shipping Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse d'expédition</span>
</th>
</tr>
</thead>
<tbody>
@@ -49,115 +120,144 @@
</tbody>
</table>
<!-- Order info -->
<!-- Row 1: 5 narrow cells (20% each) — stacked
so the French label doesn't overflow into
the next column. -->
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 20%;">ORDER DATE</th>
<th class="info-header" style="width: 20%;">EXPIRATION</th>
<th class="info-header" style="width: 20%;">SALESPERSON</th>
<th class="info-header" style="width: 20%;">CUSTOMER PO #</th>
<th class="info-header" style="width: 20%;">RUSH</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Order Date</span>
<span class="fp-bl-fr-stk">Date de commande</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Delivery Date</span>
<span class="fp-bl-fr-stk">Date de livraison</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Salesperson</span>
<span class="fp-bl-fr-stk">Vendeur</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Customer PO #</span>
<span class="fp-bl-fr-stk">N° de B/C client</span>
</th>
<th class="info-header" style="width: 20%;">
<span class="fp-bl-en-stk">Lead Time</span>
<span class="fp-bl-fr-stk">Délai</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.date_order" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.validity_date"/></td>
<td class="text-center"><span t-field="doc.user_id"/></td>
<td class="text-center"><span t-esc="doc.x_fc_po_number or '-'"/></td>
<td class="text-center">
<span t-if="doc.x_fc_rush_order" class="status-warning">RUSH</span>
<span t-else="">Standard</span>
<t t-if="doc.commitment_date">
<span t-field="doc.commitment_date" t-options="{'widget': 'date'}"/>
</t>
<t t-else=""></t>
</td>
<td class="text-center"><span t-field="doc.user_id"/></td>
<td class="text-center"><span t-esc="doc.x_fc_po_number or '—'"/></td>
<td class="text-center">
<t t-if="doc.x_fc_rush_order">
<span class="status-warning">Rush / Urgent</span>
</t>
<t t-else="">Standard</t>
</td>
</tr>
</tbody>
</table>
<!-- Plating info -->
<t t-if="doc.x_fc_part_catalog_id or doc.x_fc_customer_spec_id or doc.x_fc_delivery_method">
<!-- Row 2: 3 wider cells (33% each) — inline. -->
<t t-if="doc.x_fc_customer_job_number or spec_label or delivery_method_label">
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 34%;">PART</th>
<th class="info-header" style="width: 33%;">SPECIFICATION</th>
<th class="info-header" style="width: 33%;">DELIVERY METHOD</th>
<th class="info-header" style="width: 34%;">
<span class="fp-bl-en">Customer Job #</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">N° de travail client</span>
</th>
<th class="info-header" style="width: 33%;">
<span class="fp-bl-en">Specification</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Spécification</span>
</th>
<th class="info-header" style="width: 33%;">
<span class="fp-bl-en">Delivery Method</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Méthode de livraison</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.x_fc_part_catalog_id"/></td>
<td class="text-center"><span t-field="doc.x_fc_customer_spec_id"/></td>
<td class="text-center">
<t t-set="dm" t-value="dict(doc._fields['x_fc_delivery_method'].selection).get(doc.x_fc_delivery_method, '-')"/>
<span t-esc="dm"/>
</td>
<td class="text-center"><span t-esc="doc.x_fc_customer_job_number or '—'"/></td>
<td class="text-center"><span t-esc="spec_label or '—'"/></td>
<td class="text-center"><span t-esc="delivery_method_label or '—'"/></td>
</tr>
</tbody>
</table>
</t>
<!-- Scheduling + customer job reference -->
<t t-if="doc.x_fc_customer_job_number or doc.x_fc_planned_start_date or doc.commitment_date or doc.x_fc_ship_via">
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 25%;">CUSTOMER JOB #</th>
<th class="info-header" style="width: 25%;">PLANNED START</th>
<th class="info-header" style="width: 25%;">CUSTOMER DEADLINE</th>
<th class="info-header" style="width: 25%;">SHIP VIA</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-esc="doc.x_fc_customer_job_number or '-'"/></td>
<td class="text-center"><span t-field="doc.x_fc_planned_start_date"/></td>
<td class="text-center"><span t-field="doc.commitment_date"/></td>
<td class="text-center"><span t-esc="doc.x_fc_ship_via or '-'"/></td>
</tr>
</tbody>
</table>
</t>
<!-- Blanket / block-partial callout (confirmed-order shipping flags) -->
<!-- Blanket / block-partial callout -->
<t t-if="doc.x_fc_is_blanket_order or doc.x_fc_block_partial_shipments">
<div class="highlight-box">
<t t-if="doc.x_fc_is_blanket_order">
<strong>Blanket Order.</strong>
<strong>Blanket Order / Commande ouverte.</strong>
Parts will be released in quantities over time.
</t>
<t t-if="doc.x_fc_block_partial_shipments">
<strong>Partial shipments blocked.</strong>
<strong>Partial shipments blocked / Expéditions partielles bloquées.</strong>
The order ships as one complete batch.
</t>
</div>
</t>
<!-- Order lines -->
<!-- Order lines. Taxes column dropped — taxes
summarized in the totals block below; per-line
tax labels were noise on a single-tax-region
plating order. The part-number cell appends
the catalog `name` (Part Name) after the
revision so customers see PN + Rev + Name. -->
<table class="bordered">
<thead>
<tr>
<th class="text-start" style="width: 20%;">PART NUMBER</th>
<th class="text-start" style="width: 30%;">DESCRIPTION</th>
<th style="width: 8%;">QTY</th>
<th style="width: 8%;">UOM</th>
<th style="width: 12%;">UNIT PRICE</th>
<th style="width: 10%;">TAXES</th>
<th style="width: 12%;">AMOUNT</th>
<th class="text-start" style="width: 24%;">
<span class="fp-bl-en">Part Number</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">N° de pièce</span>
</th>
<th class="text-start" style="width: 38%;">
<span class="fp-bl-en">Description</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Description</span>
</th>
<th style="width: 8%;">
<span class="fp-bl-en-stk">Qty</span>
<span class="fp-bl-fr-stk">Qté</span>
</th>
<th style="width: 8%;">
<span class="fp-bl-en-stk">UOM</span>
<span class="fp-bl-fr-stk">UDM</span>
</th>
<th style="width: 11%;">
<span class="fp-bl-en-stk">Unit Price</span>
<span class="fp-bl-fr-stk">Prix unitaire</span>
</th>
<th style="width: 11%;">
<span class="fp-bl-en-stk">Amount</span>
<span class="fp-bl-fr-stk">Montant</span>
</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.order_line" t-as="line">
<t t-if="line.display_type == 'line_section'">
<tr class="section-row"><td colspan="7"><strong t-field="line.name"/></td></tr>
<tr class="section-row"><td colspan="6"><strong t-field="line.name"/></td></tr>
</t>
<t t-elif="line.display_type == 'line_note'">
<tr class="note-row"><td colspan="7"><span t-field="line.name"/></td></tr>
<tr class="note-row"><td colspan="6"><span t-field="line.name"/></td></tr>
</t>
<t t-elif="not line.display_type or line.display_type == 'product'">
<tr>
<td>
<t t-call="fusion_plating_reports.customer_line_part_number"/>
<t t-if="line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.name">
<span> - </span>
<span t-esc="line.x_fc_part_catalog_id.name"/>
</t>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_description"/>
@@ -169,9 +269,6 @@
<td class="text-end">
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
<td class="text-center">
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or '-'"/>
</td>
<td class="text-end">
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
@@ -184,37 +281,38 @@
<!-- Terms + Totals -->
<div class="row" style="margin-top: 15px;">
<div class="col-6">
<t t-if="doc.payment_term_id.note">
<strong>Payment Terms:</strong><br/>
<span t-field="doc.payment_term_id.note"/>
</t>
<t t-if="doc.x_fc_invoice_strategy">
<div style="margin-top: 10px;">
<strong>Invoice Strategy: </strong>
<t t-set="inv_strat" t-value="dict(doc._fields['x_fc_invoice_strategy'].selection).get(doc.x_fc_invoice_strategy, '-')"/>
<span t-esc="inv_strat"/>
<t t-if="doc.x_fc_invoice_strategy == 'deposit' and doc.x_fc_deposit_percent">
(<span t-esc="doc.x_fc_deposit_percent"/>%)
</t>
</div>
<t t-if="doc.payment_term_id">
<strong>Payment Terms / Modalités de paiement:</strong><br/>
<t t-if="doc.payment_term_id.note">
<span t-field="doc.payment_term_id.note"/>
</t>
<t t-else="">
<span t-field="doc.payment_term_id.name"/>
</t>
</t>
</div>
<div class="col-6" style="text-align: right;">
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<td style="min-width: 150px;">Subtotal</td>
<td style="min-width: 150px;">
<span class="fp-bl-en">Subtotal</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Sous-total</span>
</td>
<td class="text-end" style="min-width: 110px;">
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<tr>
<td>Taxes</td>
<td>
<span class="fp-bl-en">Taxes</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Taxes</span>
</td>
<td class="text-end">
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<tr style="background-color: #c1c1c1;">
<td><strong>Grand Total</strong></td>
<td>
<span class="fp-bl-en">Grand Total</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Total général</span>
</td>
<td class="text-end"><strong>
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</strong></td>
@@ -226,7 +324,7 @@
<!-- External (customer-visible) notes -->
<t t-if="doc.x_fc_external_note">
<div style="margin-top: 15px;">
<strong>Notes:</strong>
<strong>Notes / Remarques:</strong>
<div t-field="doc.x_fc_external_note"/>
</div>
</t>
@@ -234,7 +332,7 @@
<!-- Terms and Conditions -->
<t t-if="doc.note">
<div style="margin-top: 15px;">
<strong>Terms and Conditions:</strong>
<strong>Terms and Conditions / Conditions générales:</strong>
<div t-field="doc.note"/>
</div>
</t>