changes
This commit is contained in:
@@ -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 (the CoC pattern — proven to work):** use a TINY `margin_top` (≤8mm) + `header_spacing=0` + `header_line=False`, then put a generous `padding-top: 20mm` on the body wrapper class (`.fp-coc`, `.fp-sale`, etc.). The header HTML is allowed to overflow the reserved zone INTO the body area; the body's wrapper padding is what actually clears it. This is more robust than trying to size `margin_top` to the rendered header height — any future header change (extra phone line, taller logo) immediately causes overlap with the "sized exactly" approach, whereas the overflow+padding approach has slack built in. Reference setup: `paperformat_fp_coc` and `paperformat_fp_a4_portrait` both use `margin_top=8`, `header_spacing=0`, `header_line=False`; CoC's `.fp-coc` and SO's `.fp-sale` both use `padding-top: 20mm` on the wrapper. 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); SO portrait uses "Fusion Plating A4 Portrait (Compact)". 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` |
|
||||
| **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 (the CoC pattern — proven to work):** use a TINY `margin_top` (≤8mm) + `header_spacing=0` + `header_line=False`, then put a generous `padding-top: 20mm` on the body wrapper class (`.fp-coc`, `.fp-sale`, etc.). The header HTML is allowed to overflow the reserved zone INTO the body area; the body's wrapper padding is what actually clears it. This is more robust than trying to size `margin_top` to the rendered header height — any future header change (extra phone line, taller logo) immediately causes overlap with the "sized exactly" approach, whereas the overflow+padding approach has slack built in. Reference setup: `paperformat_fp_coc` and `paperformat_fp_a4_portrait` both use `margin_top=8`, `header_spacing=0`, `header_line=False`; CoC's `.fp-coc` and SO's `.fp-sale` both use `padding-top: 20mm` on the wrapper. **For landscape custom-header reports, reuse `paperformat_fp_a4_landscape_compact`** (same shape, just rotated) — invoice landscape uses this; don't create yet-another-landscape-compact. 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); SO portrait uses "Fusion Plating A4 Portrait (Compact)". 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>"`. (4) **XML comments cannot contain `--` (double-hyphen)** per the XML spec — `<!-- needs wkhtmltopdf --footer-html -->` fails with `XMLSyntaxError: Comment must not contain '--' (double-hyphen)`. Rewrite without the double-hyphen: `<!-- needs a wkhtmltopdf footer-html arg -->`. Bites when documenting CLI flags or option names in QWeb comments. | `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 |
|
||||
@@ -44,6 +44,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 |
|
||||
| **Custom-header reports need `.article` wrapper for UTF-8 — use `fp_external_layout_clean`, not raw `html_container`** | Pattern that bit us: building a custom-header QWeb report (logo + address LEFT, title + barcode RIGHT in one row, no Odoo company band) by dropping `<t t-call="web.external_layout">` and using only `<t t-call="web.html_container">`. **Result:** every accented French character (é, è, °, em-dash) rendered as Latin-1 mojibake in the PDF (`Adresse d'expédition` → `Adresse d'expédition`, `N° de pièce` → `N° de pièce`, `—` → `â€"`). Root cause: Odoo's report renderer expects a `<div class="article">` wrapper to dispatch content through the proper UTF-8-aware pipeline; raw `html_container` doesn't have it. **The CSS-hide approach DOESN'T work either** (e.g. `body > .header, div.header { display: none !important; }`) — the `.header` and `.footer` divs from `external_layout_standard` get **extracted from the body and pushed into wkhtmltopdf's separate `--header-html` / `--footer-html` streams BEFORE the body's CSS gets a chance to apply**, so they render in the page margins regardless of any CSS rule. **Right pattern:** `<t t-call="fusion_plating_reports.fp_external_layout_clean">` (defined in `report_fp_sale.xml`) — this variant provides just the `.article` wrapper that Odoo's pipeline needs, with NO auto `.header` div. It DOES keep a minimal `.footer` div carrying only `Page <span class="page"/> / <span class="topage"/>` — those page-number placeholders **only get substituted with the current/total page when the `.footer` div is extracted into wkhtmltopdf's `--footer-html` stream**, so if you want page numbers in a custom-layout report, include a minimal `.footer` div with just those spans (rendering "Page X / Y") — don't try to set them from QWeb or compute the page count yourself. The layout also prints an optional **internal form code** on the footer's left side when the calling report sets `<t t-set="form_code" t-value="'FRM-XXX'"/>` BEFORE the `<t t-call="...fp_external_layout_clean">`. Sale Order Confirmation uses `FRM-006`; other reports adopt their own as they're standardized. Reports that don't set `form_code` leave the left side blank — the right side always carries `Page X / Y`. Canonical example: `report_fp_sale.xml` (SO confirmation portrait). | any custom-header PDF report on entech wkhtmltopdf |
|
||||
| **QWeb `t-field` requires a dotted path — bare variables fail at compile** | Odoo 19 enforces `assert "." in el.get('t-field')` in `_compile_directive_field`. Writing `<div t-field="partner" t-options="{'widget': 'contact', ...}"/>` (where `partner` came from a `<t t-set="partner" t-value="..."/>` in the calling template) **fails at template-compile time** with `AssertionError: t-field must have at least a dot like 'record.field_name'`. The error message points at the line, but the broader trap is that **you can't write a generic "render-a-partner-as-contact" sub-template that takes a record via t-set** — the contact-widget pattern only works on real field traversals like `doc.partner_id` baked into the template at author time. **Workarounds:** (a) Inline the partner rendering at each call site so the `t-field` has a dotted path (`<div t-field="doc.partner_invoice_id" t-options=...`). (b) Render the address parts manually in the sub-template using `t-esc` on explicit fields (`partner.street`, `partner.city`, etc.) — verbose but works with bare variables. Pattern (b) is what `fp_packing_slip_addr_block` uses now after this trap was hit. Same applies to `t-out` with `widget` options. | any QWeb sub-template trying to render a record via `t-field` |
|
||||
| **Assigning a `Date` to a `Datetime` field shifts the day in negative-UTC timezones** | When a transient/wizard `fields.Date` value is written into a target `fields.Datetime` field (e.g. wizard `customer_deadline` → SO `commitment_date`), Odoo stores midnight UTC of the picked date. Rendered back in any negative-UTC timezone (Eastern UTC-4/-5, all of CA/US), midnight UTC = 8pm the previous day — so the user picks "May 25" in the wizard and sees "May 24" on the SO header / PDF report. **Fix:** combine the date with noon before writing: `datetime.combine(self.my_date, time(12, 0))` — noon UTC stays on the same calendar date in every reasonable timezone (±12hr). Caught here on `fp.direct.order.wizard._prepare_order_vals` writing `commitment_date`. Watch for the same pattern any time a wizard/configurator with a Date field hands off to a Datetime target. The reverse (`Datetime` field read into a Date-display) is fine if `t-options="{'widget':'date'}"` is used — Odoo handles the tz-aware date extraction. | any wizard writing a Date value into a Datetime field |
|
||||
| **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 |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Configurator',
|
||||
'version': '19.0.21.5.5',
|
||||
'version': '19.0.21.5.6',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
||||
'description': """
|
||||
|
||||
@@ -67,6 +67,28 @@ class SaleOrder(models.Model):
|
||||
'Net Terms strategies.',
|
||||
)
|
||||
x_fc_rush_order = fields.Boolean(string='Rush Order', tracking=True)
|
||||
|
||||
# Lead Time (Phase D11) — promised production window in business
|
||||
# days. Operators enter a min/max range (e.g. 3-5 days or 7-10 days)
|
||||
# so we render a proper expectation on the SO confirmation instead
|
||||
# of the binary Standard/Rush we had before. Both fields default to
|
||||
# 0 — `x_fc_lead_time_display` computes the right human-readable
|
||||
# string (range / single value / Rush / Standard) for the PDF.
|
||||
x_fc_lead_time_min_days = fields.Integer(
|
||||
string='Lead Time Min (days)', tracking=True,
|
||||
help='Lower bound of the promised production lead time, in '
|
||||
'business days. Leave 0 if not committed.',
|
||||
)
|
||||
x_fc_lead_time_max_days = fields.Integer(
|
||||
string='Lead Time Max (days)', tracking=True,
|
||||
help='Upper bound of the promised production lead time, in '
|
||||
'business days. Leave 0 if not committed.',
|
||||
)
|
||||
x_fc_lead_time_display = fields.Char(
|
||||
string='Lead Time',
|
||||
compute='_compute_lead_time_display',
|
||||
help='Human-readable lead time string for the SO confirmation PDF.',
|
||||
)
|
||||
x_fc_delivery_method = fields.Selection(
|
||||
[('local_delivery', 'Local Delivery'), ('shipping_partner', 'Shipping Partner'),
|
||||
('customer_pickup', 'Customer Pickup')],
|
||||
@@ -242,6 +264,27 @@ class SaleOrder(models.Model):
|
||||
currency_field='currency_id',
|
||||
)
|
||||
|
||||
@api.depends('x_fc_lead_time_min_days', 'x_fc_lead_time_max_days', 'x_fc_rush_order')
|
||||
def _compute_lead_time_display(self):
|
||||
"""Render the lead time as a human-readable string for reports.
|
||||
|
||||
Priority order:
|
||||
- Real min/max range set → "X-Y days" or "X days"
|
||||
- Range not set, rush_order on → "Rush"
|
||||
- Otherwise → "Standard"
|
||||
"""
|
||||
for so in self:
|
||||
mn = so.x_fc_lead_time_min_days or 0
|
||||
mx = so.x_fc_lead_time_max_days or 0
|
||||
if mn and mx and mn != mx:
|
||||
so.x_fc_lead_time_display = '%d-%d days' % (min(mn, mx), max(mn, mx))
|
||||
elif mx or mn:
|
||||
so.x_fc_lead_time_display = '%d days' % (mx or mn)
|
||||
elif so.x_fc_rush_order:
|
||||
so.x_fc_lead_time_display = 'Rush'
|
||||
else:
|
||||
so.x_fc_lead_time_display = 'Standard'
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_wo_completion(self):
|
||||
"""Batched: one grouped query across all records in self.
|
||||
|
||||
@@ -201,6 +201,16 @@
|
||||
</div>
|
||||
<field name="x_fc_is_blanket_order"/>
|
||||
<field name="x_fc_block_partial_shipments"/>
|
||||
<!-- Lead Time range. Both 0 = "Standard" on
|
||||
the PDF; otherwise renders "X-Y days"
|
||||
(or "X days" if min==max or one is 0). -->
|
||||
<label for="x_fc_lead_time_min_days" string="Lead Time (days)"/>
|
||||
<div class="o_row">
|
||||
<field name="x_fc_lead_time_min_days" class="oe_inline" style="width: 4em;"/>
|
||||
<span> to </span>
|
||||
<field name="x_fc_lead_time_max_days" class="oe_inline" style="width: 4em;"/>
|
||||
</div>
|
||||
<field name="x_fc_lead_time_display" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
|
||||
@@ -86,6 +86,11 @@ class FpDirectOrderWizard(models.Model):
|
||||
)
|
||||
internal_deadline = fields.Date(string='Internal Deadline')
|
||||
customer_deadline = fields.Date(string='Customer Deadline', tracking=True)
|
||||
# Lead Time — promised production window. Mirrors directly to
|
||||
# x_fc_lead_time_min_days / x_fc_lead_time_max_days on the SO via
|
||||
# _prepare_order_vals. Leaving both 0 = Standard (no commitment).
|
||||
lead_time_min_days = fields.Integer(string='Lead Time Min (days)')
|
||||
lead_time_max_days = fields.Integer(string='Lead Time Max (days)')
|
||||
|
||||
# ---- Order flags (Phase B) ----
|
||||
is_blanket_order = fields.Boolean(
|
||||
@@ -530,6 +535,8 @@ class FpDirectOrderWizard(models.Model):
|
||||
'x_fc_customer_job_number': self.customer_job_number or False,
|
||||
'x_fc_planned_start_date': self.planned_start_date,
|
||||
'x_fc_internal_deadline': self.internal_deadline,
|
||||
'x_fc_lead_time_min_days': self.lead_time_min_days or 0,
|
||||
'x_fc_lead_time_max_days': self.lead_time_max_days or 0,
|
||||
# commitment_date is a Datetime; customer_deadline is a Date.
|
||||
# Assigning a bare Date stores midnight UTC, which renders as
|
||||
# the PREVIOUS day in any negative-UTC timezone (Eastern shifts
|
||||
|
||||
@@ -102,6 +102,14 @@
|
||||
still `customer_deadline` (wizard) →
|
||||
`commitment_date` (SO). -->
|
||||
<field name="customer_deadline" string="Delivery Date"/>
|
||||
<!-- Lead time range (min/max business days).
|
||||
Both 0 = "Standard" on the SO confirm PDF. -->
|
||||
<label for="lead_time_min_days" string="Lead Time (days)"/>
|
||||
<div class="o_row">
|
||||
<field name="lead_time_min_days" class="oe_inline" style="width: 4em;"/>
|
||||
<span> to </span>
|
||||
<field name="lead_time_max_days" class="oe_inline" style="width: 4em;"/>
|
||||
</div>
|
||||
<field name="is_blanket_order"/>
|
||||
<field name="block_partial_shipments"/>
|
||||
</group>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Reports',
|
||||
'version': '19.0.11.26.16',
|
||||
'version': '19.0.11.26.30',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
|
||||
'depends': [
|
||||
|
||||
@@ -90,7 +90,12 @@
|
||||
<t t-set="_desc"
|
||||
t-value="line.fp_customer_description() if _has_helper else line.name"/>
|
||||
<span t-esc="_desc" style="white-space: pre-line;"/>
|
||||
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id">
|
||||
<!-- Serial line is suppressed when the calling template sets
|
||||
`fp_no_serial_in_desc=True` in `line.env.context`. SO
|
||||
portrait does this because it shows the serial in its own
|
||||
column in the part-number cell — otherwise it'd be
|
||||
duplicated. Invoice / packing slip still display it here. -->
|
||||
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id and not line.env.context.get('fp_no_serial_in_desc')">
|
||||
<br/>
|
||||
<small>Serial: <span t-esc="line.x_fc_serial_id.name"/></small>
|
||||
</t>
|
||||
|
||||
@@ -73,6 +73,26 @@
|
||||
<field name="dpi">90</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- Compact A4 Landscape for customer-facing landscape reports. -->
|
||||
<!-- Same shape as `paperformat_fp_a4_portrait` (margin_top=8, -->
|
||||
<!-- header_spacing=0) just rotated. Bind alongside the portrait -->
|
||||
<!-- compact for any report that has a landscape variant. -->
|
||||
<!-- ============================================================= -->
|
||||
<record id="paperformat_fp_a4_landscape_compact" model="report.paperformat">
|
||||
<field name="name">Fusion Plating A4 Landscape (Compact)</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="format">A4</field>
|
||||
<field name="orientation">Landscape</field>
|
||||
<field name="margin_top">8</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">0</field>
|
||||
<field name="dpi">90</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- 1. Certificate of Conformance (Portal Job) — Landscape -->
|
||||
<!-- ============================================================= -->
|
||||
@@ -451,6 +471,11 @@
|
||||
<field name="binding_model_id" ref="account.model_account_move"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="is_invoice_report" eval="True"/>
|
||||
<!-- Same compact paperformat as the SO confirmation so the
|
||||
inline custom header sits at the top of the page (not 40mm
|
||||
down under Odoo's default margin). See CLAUDE.md
|
||||
"wkhtmltopdf header overlap — the CoC pattern". -->
|
||||
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
|
||||
</record>
|
||||
|
||||
<record id="action_report_fp_invoice_landscape" model="ir.actions.report">
|
||||
@@ -462,7 +487,10 @@
|
||||
<field name="print_report_name">'Invoice - %s' % (object.name or '')</field>
|
||||
<field name="binding_model_id" ref="account.model_account_move"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
|
||||
<!-- Compact landscape paperformat (same shape as the portrait
|
||||
compact, just rotated) so the inline header lands at the
|
||||
top with no auto-margin gap. -->
|
||||
<field name="paperformat_id" ref="paperformat_fp_a4_landscape_compact"/>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
|
||||
@@ -14,26 +14,92 @@
|
||||
<template id="report_fp_invoice_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">
|
||||
<div class="page">
|
||||
<t t-set="form_code" t-value="'FRM-007'"/>
|
||||
<t t-call="fusion_plating_reports.fp_external_layout_clean">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="company" t-value="doc.company_id or env.company"/>
|
||||
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
|
||||
<t t-call="fusion_plating_reports.fp_sale_bilingual_styles"/>
|
||||
|
||||
<h4>
|
||||
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice # </span>
|
||||
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice # </span>
|
||||
<span t-elif="doc.move_type == 'out_refund'">Credit Note # </span>
|
||||
<span t-elif="doc.move_type == 'in_invoice'">Vendor Bill # </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h4>
|
||||
<!-- Compute helpers -->
|
||||
<t t-set="title_en" t-value="
|
||||
'Credit Note' if doc.move_type == 'out_refund'
|
||||
else 'Vendor Bill' if doc.move_type == 'in_invoice'
|
||||
else 'Draft Invoice' if (doc.move_type == 'out_invoice' and doc.state == 'draft')
|
||||
else 'Invoice'"/>
|
||||
<t t-set="title_fr" t-value="
|
||||
'Note de crédit' if doc.move_type == 'out_refund'
|
||||
else 'Facture fournisseur' if doc.move_type == 'in_invoice'
|
||||
else 'Facture brouillon' if (doc.move_type == 'out_invoice' and doc.state == 'draft')
|
||||
else 'Facture'"/>
|
||||
<t t-set="barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', doc.name, 600, 100) if doc.name and doc.name != '/' else False"/>
|
||||
<!-- Pull FP-specific fields from the source SO when invoice_origin
|
||||
names one. Falls back to an empty recordset on manual
|
||||
invoices so the t-if guards in the markup stay clean. -->
|
||||
<t t-set="source_so" t-value="doc.env['sale.order'].search([('name', '=', doc.invoice_origin)], limit=1) if doc.invoice_origin else doc.env['sale.order'].browse()"/>
|
||||
<t t-set="logo_uri" t-value="('data:image/png;base64,%s' % company.logo.decode()) if company.logo else False"/>
|
||||
<t t-set="company_fax" t-value="company.partner_id.x_ff_fax_number if 'x_ff_fax_number' in company.partner_id._fields else False"/>
|
||||
<t t-set="po_number" t-value="(source_so.x_fc_po_number if source_so else False) or doc.invoice_origin or ''"/>
|
||||
<t t-set="spec_label" t-value="(source_so.x_fc_customer_spec_id.display_name or source_so.x_fc_customer_spec_id.name) if source_so and source_so.x_fc_customer_spec_id else ''"/>
|
||||
<t t-set="delivery_method_label" t-value="dict(source_so._fields['x_fc_delivery_method'].selection).get(source_so.x_fc_delivery_method, '') if source_so and source_so.x_fc_delivery_method else ''"/>
|
||||
|
||||
<div class="fp-report fp-sale">
|
||||
<!-- Inline 3-column header: logo+address LEFT,
|
||||
NADCAP MIDDLE, title+barcode RIGHT.
|
||||
Mirrors SO confirmation exactly. -->
|
||||
<div class="fp-sale-header-row">
|
||||
<div class="fp-sale-header-left">
|
||||
<t t-if="logo_uri">
|
||||
<img t-att-src="logo_uri" class="fp-sale-logo" alt="Logo"/>
|
||||
</t>
|
||||
<div class="fp-sale-company-addr">
|
||||
<div>
|
||||
<t t-if="company.partner_id.street"><span t-esc="company.partner_id.street"/></t>
|
||||
<t t-if="company.partner_id.city"> | <span t-esc="company.partner_id.city"/></t>
|
||||
<t t-if="company.partner_id.state_id"> | <span t-esc="company.partner_id.state_id.code or company.partner_id.state_id.name"/></t>
|
||||
<t t-if="company.partner_id.zip"> | <span t-esc="company.partner_id.zip"/></t>
|
||||
</div>
|
||||
<div t-if="company.phone or company_fax">
|
||||
<t t-if="company.phone">Tel: <span t-esc="company.phone"/></t>
|
||||
<t t-if="company.phone and company_fax">   </t>
|
||||
<t t-if="company_fax">Fax: <span t-esc="company_fax"/></t>
|
||||
</div>
|
||||
<div t-if="company.partner_id.website">
|
||||
<a t-att-href="company.partner_id.website"><span t-esc="company.partner_id.website"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp-sale-header-mid">
|
||||
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
|
||||
<img class="fp-nadcap-logo"
|
||||
t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
|
||||
alt="Nadcap Accredited"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="fp-sale-header-right">
|
||||
<span class="fp-sale-title-en"><t t-esc="title_en"/></span>
|
||||
<span class="fp-sale-title-fr"><t t-esc="title_fr"/></span>
|
||||
<t t-if="barcode_uri">
|
||||
<div class="fp-bc-wrap" style="margin-top: 4px;">
|
||||
<img t-att-src="barcode_uri" alt="Invoice Barcode"/>
|
||||
<div class="fp-bc-label"><span t-field="doc.name"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<!-- Billing / Shipping -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLING ADDRESS</th>
|
||||
<th style="width: 50%;">DELIVERY 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">Delivery Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse de livraison</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -56,53 +122,131 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Invoice info -->
|
||||
<!-- Row 1: Invoice Date | Due Date | Sales Rep | Customer PO # | Payment Ref -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="info-header" style="width: 25%;">INVOICE DATE</th>
|
||||
<th class="info-header" style="width: 25%;">DUE DATE</th>
|
||||
<th class="info-header" style="width: 25%;">SOURCE</th>
|
||||
<th class="info-header" style="width: 25%;">SALES REP</th>
|
||||
<th class="info-header" style="width: 20%;">
|
||||
<span class="fp-bl-en-stk">Invoice Date</span>
|
||||
<span class="fp-bl-fr-stk">Date de facture</span>
|
||||
</th>
|
||||
<th class="info-header" style="width: 20%;">
|
||||
<span class="fp-bl-en-stk">Due Date</span>
|
||||
<span class="fp-bl-fr-stk">Date d'échéance</span>
|
||||
</th>
|
||||
<th class="info-header" style="width: 20%;">
|
||||
<span class="fp-bl-en-stk">Sales Rep</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">Payment Ref</span>
|
||||
<span class="fp-bl-fr-stk">Réf. paiement</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center"><span t-field="doc.invoice_date"/></td>
|
||||
<td class="text-center"><span t-field="doc.invoice_date_due"/></td>
|
||||
<td class="text-center"><span t-field="doc.invoice_origin"/></td>
|
||||
<td class="text-center"><span t-field="doc.invoice_user_id"/></td>
|
||||
<td class="text-center"><span t-esc="po_number or '—'"/></td>
|
||||
<td class="text-center"><span t-esc="doc.payment_reference or '—'"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Lines -->
|
||||
<!-- Row 2: Customer Job # | Specification | Delivery Method (pulled from source SO; hidden on manual invoices). -->
|
||||
<t t-if="source_so and (source_so.x_fc_customer_job_number or spec_label or delivery_method_label)">
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<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-esc="source_so.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>
|
||||
|
||||
<!-- Lines (taxes column dropped; summarized in totals) -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start" style="width: 20%;">PART NUMBER</th>
|
||||
<th class="text-start" style="width: 32%;">DESCRIPTION</th>
|
||||
<th style="width: 8%;">QTY</th>
|
||||
<th style="width: 8%;">UOM</th>
|
||||
<th style="width: 12%;">UNIT PRICE</th>
|
||||
<th style="width: 8%;">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.invoice_line_ids" 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"/>
|
||||
<!-- Three stacked lines: Part #, Name, S/N -->
|
||||
<div>
|
||||
<strong>Part #:</strong>
|
||||
<t t-call="fusion_plating_reports.customer_line_part_number"/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Name:</strong>
|
||||
<t t-if="line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.name">
|
||||
<span t-esc="line.x_fc_part_catalog_id.name"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
<div>
|
||||
<strong>S/N:</strong>
|
||||
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id">
|
||||
<span t-esc="line.x_fc_serial_id.name"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<!-- Suppress duplicate serial in the description column -->
|
||||
<t t-set="line" t-value="line.with_context(fp_no_serial_in_desc=True)"/>
|
||||
<t t-call="fusion_plating_reports.customer_line_description"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@@ -112,9 +256,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>
|
||||
@@ -127,40 +268,47 @@
|
||||
<!-- Terms + Totals -->
|
||||
<div class="row" style="margin-top: 15px;">
|
||||
<div class="col-6">
|
||||
<t t-if="doc.invoice_payment_term_id.note">
|
||||
<strong>Payment Terms:</strong><br/>
|
||||
<span t-field="doc.invoice_payment_term_id.note"/>
|
||||
</t>
|
||||
<t t-if="doc.payment_reference">
|
||||
<div style="margin-top: 10px;">
|
||||
<strong>Payment Reference:</strong>
|
||||
<span t-field="doc.payment_reference"/>
|
||||
</div>
|
||||
<t t-if="doc.invoice_payment_term_id">
|
||||
<strong>Payment Terms / Modalités de paiement:</strong><br/>
|
||||
<t t-if="doc.invoice_payment_term_id.note">
|
||||
<span t-field="doc.invoice_payment_term_id.note"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-field="doc.invoice_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>
|
||||
</tr>
|
||||
<t t-if="doc.amount_residual and doc.amount_residual != doc.amount_total">
|
||||
<tr>
|
||||
<td><strong>Amount Due</strong></td>
|
||||
<td>
|
||||
<strong><span class="fp-bl-en">Amount Due</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Montant dû</span></strong>
|
||||
</td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_residual" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</strong></td>
|
||||
@@ -173,14 +321,14 @@
|
||||
<!-- Paid stamp -->
|
||||
<t t-if="doc.payment_state in ('paid', 'in_payment')">
|
||||
<div style="margin-top: 15px; text-align: center;">
|
||||
<span class="paid-stamp">PAID</span>
|
||||
<span class="paid-stamp">PAID / PAYÉ</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Notes -->
|
||||
<t t-if="doc.narration">
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Notes:</strong>
|
||||
<strong>Notes / Remarques:</strong>
|
||||
<div t-field="doc.narration"/>
|
||||
</div>
|
||||
</t>
|
||||
@@ -198,26 +346,87 @@
|
||||
<template id="report_fp_invoice_landscape">
|
||||
<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_landscape_styles"/>
|
||||
<div class="fp-landscape">
|
||||
<div class="page">
|
||||
<t t-set="form_code" t-value="'FRM-007'"/>
|
||||
<t t-call="fusion_plating_reports.fp_external_layout_clean">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="company" t-value="doc.company_id or env.company"/>
|
||||
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
|
||||
<t t-call="fusion_plating_reports.fp_sale_bilingual_styles"/>
|
||||
|
||||
<h2 style="text-align: left;">
|
||||
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice # </span>
|
||||
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice # </span>
|
||||
<span t-elif="doc.move_type == 'out_refund'">Credit Note # </span>
|
||||
<span t-elif="doc.move_type == 'in_invoice'">Vendor Bill # </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h2>
|
||||
<!-- Same compute helpers as portrait -->
|
||||
<t t-set="title_en" t-value="
|
||||
'Credit Note' if doc.move_type == 'out_refund'
|
||||
else 'Vendor Bill' if doc.move_type == 'in_invoice'
|
||||
else 'Draft Invoice' if (doc.move_type == 'out_invoice' and doc.state == 'draft')
|
||||
else 'Invoice'"/>
|
||||
<t t-set="title_fr" t-value="
|
||||
'Note de crédit' if doc.move_type == 'out_refund'
|
||||
else 'Facture fournisseur' if doc.move_type == 'in_invoice'
|
||||
else 'Facture brouillon' if (doc.move_type == 'out_invoice' and doc.state == 'draft')
|
||||
else 'Facture'"/>
|
||||
<t t-set="barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', doc.name, 600, 100) if doc.name and doc.name != '/' else False"/>
|
||||
<t t-set="source_so" t-value="doc.env['sale.order'].search([('name', '=', doc.invoice_origin)], limit=1) if doc.invoice_origin else doc.env['sale.order'].browse()"/>
|
||||
<t t-set="logo_uri" t-value="('data:image/png;base64,%s' % company.logo.decode()) if company.logo else False"/>
|
||||
<t t-set="company_fax" t-value="company.partner_id.x_ff_fax_number if 'x_ff_fax_number' in company.partner_id._fields else False"/>
|
||||
<t t-set="po_number" t-value="(source_so.x_fc_po_number if source_so else False) or doc.invoice_origin or ''"/>
|
||||
<t t-set="spec_label" t-value="(source_so.x_fc_customer_spec_id.display_name or source_so.x_fc_customer_spec_id.name) if source_so and source_so.x_fc_customer_spec_id else ''"/>
|
||||
<t t-set="delivery_method_label" t-value="dict(source_so._fields['x_fc_delivery_method'].selection).get(source_so.x_fc_delivery_method, '') if source_so and source_so.x_fc_delivery_method else ''"/>
|
||||
|
||||
<div class="fp-landscape fp-sale">
|
||||
<!-- 3-column inline header — same as portrait -->
|
||||
<div class="fp-sale-header-row">
|
||||
<div class="fp-sale-header-left">
|
||||
<t t-if="logo_uri">
|
||||
<img t-att-src="logo_uri" class="fp-sale-logo" alt="Logo"/>
|
||||
</t>
|
||||
<div class="fp-sale-company-addr">
|
||||
<div>
|
||||
<t t-if="company.partner_id.street"><span t-esc="company.partner_id.street"/></t>
|
||||
<t t-if="company.partner_id.city"> | <span t-esc="company.partner_id.city"/></t>
|
||||
<t t-if="company.partner_id.state_id"> | <span t-esc="company.partner_id.state_id.code or company.partner_id.state_id.name"/></t>
|
||||
<t t-if="company.partner_id.zip"> | <span t-esc="company.partner_id.zip"/></t>
|
||||
</div>
|
||||
<div t-if="company.phone or company_fax">
|
||||
<t t-if="company.phone">Tel: <span t-esc="company.phone"/></t>
|
||||
<t t-if="company.phone and company_fax">   </t>
|
||||
<t t-if="company_fax">Fax: <span t-esc="company_fax"/></t>
|
||||
</div>
|
||||
<div t-if="company.partner_id.website">
|
||||
<a t-att-href="company.partner_id.website"><span t-esc="company.partner_id.website"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp-sale-header-mid">
|
||||
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
|
||||
<img class="fp-nadcap-logo"
|
||||
t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
|
||||
alt="Nadcap Accredited"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="fp-sale-header-right">
|
||||
<span class="fp-sale-title-en"><t t-esc="title_en"/></span>
|
||||
<span class="fp-sale-title-fr"><t t-esc="title_fr"/></span>
|
||||
<t t-if="barcode_uri">
|
||||
<div class="fp-bc-wrap" style="margin-top: 4px;">
|
||||
<img t-att-src="barcode_uri" alt="Invoice Barcode"/>
|
||||
<div class="fp-bc-label"><span t-field="doc.name"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<!-- Billing / Shipping -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLING ADDRESS</th>
|
||||
<th style="width: 50%;">DELIVERY 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">Delivery Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse de livraison</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -240,44 +449,106 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Invoice info (wide) -->
|
||||
<!-- Invoice info row (wide, 6 cols on landscape) -->
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>INVOICE DATE</th>
|
||||
<th>DUE DATE</th>
|
||||
<th>SOURCE</th>
|
||||
<th>SALES REP</th>
|
||||
<th>PAYMENT REF</th>
|
||||
<th>CURRENCY</th>
|
||||
<th>
|
||||
<span class="fp-bl-en-stk">Invoice Date</span>
|
||||
<span class="fp-bl-fr-stk">Date de facture</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="fp-bl-en-stk">Due Date</span>
|
||||
<span class="fp-bl-fr-stk">Date d'échéance</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="fp-bl-en-stk">Sales Rep</span>
|
||||
<span class="fp-bl-fr-stk">Vendeur</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="fp-bl-en-stk">Customer PO #</span>
|
||||
<span class="fp-bl-fr-stk">N° de B/C client</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="fp-bl-en-stk">Payment Ref</span>
|
||||
<span class="fp-bl-fr-stk">Réf. paiement</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="fp-bl-en-stk">Currency</span>
|
||||
<span class="fp-bl-fr-stk">Devise</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center"><span t-field="doc.invoice_date"/></td>
|
||||
<td class="text-center"><span t-field="doc.invoice_date_due"/></td>
|
||||
<td class="text-center"><span t-field="doc.invoice_origin"/></td>
|
||||
<td class="text-center"><span t-field="doc.invoice_user_id"/></td>
|
||||
<td class="text-center"><span t-esc="doc.payment_reference or '-'"/></td>
|
||||
<td class="text-center"><span t-esc="po_number or '—'"/></td>
|
||||
<td class="text-center"><span t-esc="doc.payment_reference or '—'"/></td>
|
||||
<td class="text-center"><span t-field="doc.currency_id.name"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Lines — hide discount column unless at least one line has a discount -->
|
||||
<!-- Source SO row (wider, 3 cols, inline). Hidden on manual invoices. -->
|
||||
<t t-if="source_so and (source_so.x_fc_customer_job_number or spec_label or delivery_method_label)">
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<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>
|
||||
<span class="fp-bl-en">Specification</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Spécification</span>
|
||||
</th>
|
||||
<th>
|
||||
<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-esc="source_so.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>
|
||||
|
||||
<!-- Lines (taxes column dropped; discount column conditional) -->
|
||||
<t t-set="has_discount" t-value="any(l.discount for l in doc.invoice_line_ids)"/>
|
||||
<t t-set="col_count" t-value="8 if has_discount else 7"/>
|
||||
<t t-set="col_count" t-value="7 if has_discount else 6"/>
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start" style="width: 18%;">PART NUMBER</th>
|
||||
<th class="text-start" style="width: 24%;">DESCRIPTION</th>
|
||||
<th style="width: 8%;">QTY</th>
|
||||
<th style="width: 8%;">UOM</th>
|
||||
<th style="width: 12%;">UNIT PRICE</th>
|
||||
<th t-if="has_discount" style="width: 10%;">DISCOUNT</th>
|
||||
<th style="width: 10%;">TAXES</th>
|
||||
<th style="width: 10%;">AMOUNT</th>
|
||||
<th class="text-start" style="width: 22%;">
|
||||
<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: 30%;">
|
||||
<span class="fp-bl-en">Description</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Description</span>
|
||||
</th>
|
||||
<th style="width: 7%;">
|
||||
<span class="fp-bl-en-stk">Qty</span>
|
||||
<span class="fp-bl-fr-stk">Qté</span>
|
||||
</th>
|
||||
<th style="width: 7%;">
|
||||
<span class="fp-bl-en-stk">UOM</span>
|
||||
<span class="fp-bl-fr-stk">UDM</span>
|
||||
</th>
|
||||
<th style="width: 12%;">
|
||||
<span class="fp-bl-en-stk">Unit Price</span>
|
||||
<span class="fp-bl-fr-stk">Prix unitaire</span>
|
||||
</th>
|
||||
<th t-if="has_discount" style="width: 10%;">
|
||||
<span class="fp-bl-en-stk">Discount</span>
|
||||
<span class="fp-bl-fr-stk">Remise</span>
|
||||
</th>
|
||||
<th style="width: 12%;">
|
||||
<span class="fp-bl-en-stk">Amount</span>
|
||||
<span class="fp-bl-fr-stk">Montant</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -291,9 +562,28 @@
|
||||
<t t-elif="not line.display_type or line.display_type == 'product'">
|
||||
<tr>
|
||||
<td>
|
||||
<t t-call="fusion_plating_reports.customer_line_part_number"/>
|
||||
<!-- Three stacked lines: Part #, Name, S/N -->
|
||||
<div>
|
||||
<strong>Part #:</strong>
|
||||
<t t-call="fusion_plating_reports.customer_line_part_number"/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Name:</strong>
|
||||
<t t-if="line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.name">
|
||||
<span t-esc="line.x_fc_part_catalog_id.name"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
<div>
|
||||
<strong>S/N:</strong>
|
||||
<t t-if="'x_fc_serial_id' in line._fields and line.x_fc_serial_id">
|
||||
<span t-esc="line.x_fc_serial_id.name"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<t t-set="line" t-value="line.with_context(fp_no_serial_in_desc=True)"/>
|
||||
<t t-call="fusion_plating_reports.customer_line_description"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@@ -305,10 +595,7 @@
|
||||
</td>
|
||||
<td t-if="has_discount" class="text-center">
|
||||
<t t-if="line.discount"><span t-esc="line.discount"/>%</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or '-'"/>
|
||||
<t t-else="">—</t>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
@@ -322,34 +609,47 @@
|
||||
<!-- Terms + Totals -->
|
||||
<div class="row" style="margin-top: 15px;">
|
||||
<div class="col-7">
|
||||
<t t-if="doc.invoice_payment_term_id.note">
|
||||
<strong>Payment Terms:</strong><br/>
|
||||
<span t-field="doc.invoice_payment_term_id.note"/>
|
||||
<t t-if="doc.invoice_payment_term_id">
|
||||
<strong>Payment Terms / Modalités de paiement:</strong><br/>
|
||||
<t t-if="doc.invoice_payment_term_id.note">
|
||||
<span t-field="doc.invoice_payment_term_id.note"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-field="doc.invoice_payment_term_id.name"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-5" style="text-align: right;">
|
||||
<table class="totals-table" style="width: auto; margin-left: auto;">
|
||||
<tr>
|
||||
<td style="min-width: 200px;">Subtotal</td>
|
||||
<td style="min-width: 200px;">
|
||||
<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: 150px;">
|
||||
<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>
|
||||
</tr>
|
||||
<t t-if="doc.amount_residual and doc.amount_residual != doc.amount_total">
|
||||
<tr>
|
||||
<td><strong>Amount Due</strong></td>
|
||||
<td>
|
||||
<strong><span class="fp-bl-en">Amount Due</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Montant dû</span></strong>
|
||||
</td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_residual" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</strong></td>
|
||||
@@ -362,7 +662,7 @@
|
||||
<!-- Paid stamp -->
|
||||
<t t-if="doc.payment_state in ('paid', 'in_payment')">
|
||||
<div style="margin-top: 15px; text-align: center;">
|
||||
<span class="paid-stamp">PAID</span>
|
||||
<span class="paid-stamp">PAID / PAYÉ</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -22,6 +22,46 @@
|
||||
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. -->
|
||||
<!-- Custom minimal layout — same .article wrapper that Odoo's
|
||||
report pipeline expects (so UTF-8 charset handling works
|
||||
correctly via the standard processing path), but with NO
|
||||
auto company .header div. Includes a minimal .footer div
|
||||
that ONLY carries the wkhtmltopdf page-number placeholders
|
||||
(`<span class="page"/> / <span class="topage"/>`) — those
|
||||
only get filled in when the .footer div is extracted into
|
||||
wkhtmltopdf's footer-html stream. The .footer is otherwise
|
||||
empty so no boilerplate company info shows. -->
|
||||
<template id="fp_external_layout_clean">
|
||||
<t t-if="not o" t-set="o" t-value="doc"/>
|
||||
<t t-if="not company">
|
||||
<t t-if="company_id">
|
||||
<t t-set="company" t-value="company_id"/>
|
||||
</t>
|
||||
<t t-elif="o and 'company_id' in o and o.company_id.sudo()">
|
||||
<t t-set="company" t-value="o.company_id.sudo()"/>
|
||||
</t>
|
||||
<t t-else="else">
|
||||
<t t-set="company" t-value="res_company"/>
|
||||
</t>
|
||||
</t>
|
||||
<div class="article o_report_layout_standard"
|
||||
t-att-data-oe-model="o and o._name"
|
||||
t-att-data-oe-id="o and o.id"
|
||||
t-att-data-oe-lang="o and o.env.context.get('lang')">
|
||||
<t t-out="0"/>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div style="font-size: 9pt; color: #666; overflow: hidden;">
|
||||
<!-- Internal form code (e.g. "FRM-006") on the left.
|
||||
Each report sets `form_code` via t-set BEFORE the
|
||||
t-call to this layout; reports that don't set it
|
||||
leave the left side blank. -->
|
||||
<span style="float: left;" t-if="form_code"><t t-esc="form_code"/></span>
|
||||
<span style="float: right; white-space: nowrap; padding-left: 10px;" t-if="report_type == 'pdf'">Page <span class="page"/> / <span class="topage"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="fp_sale_bilingual_styles">
|
||||
<style>
|
||||
/* Inline bilingual: English bold, then a faint slash, then
|
||||
@@ -38,13 +78,58 @@
|
||||
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; }
|
||||
/* Match the CoC pattern exactly: tiny paperformat margin_top
|
||||
(8mm) lets the header HTML overflow into the body area,
|
||||
and this 20mm padding-top on the wrapper clears it. See
|
||||
CLAUDE.md "wkhtmltopdf header overlap" — the alternative
|
||||
"size margin_top to the header height" approach has zero
|
||||
slack and breaks any time the header HTML grows. */
|
||||
.fp-report.fp-sale { padding-top: 20mm; }
|
||||
/* This template uses fp_external_layout_clean (sibling
|
||||
template in this file) instead of web.external_layout.
|
||||
That gives us the `.article` wrapper Odoo's report
|
||||
renderer needs for proper UTF-8 dispatch, WITHOUT the
|
||||
`.header` / `.footer` divs that wkhtmltopdf would
|
||||
extract into page-margin streams. So the body owns the
|
||||
entire visible header (logo + address LEFT, title +
|
||||
barcode RIGHT) and no auto company band shows up. See
|
||||
CLAUDE.md "Custom-header reports need .article wrapper". */
|
||||
.fp-report.fp-sale { padding-top: 0; }
|
||||
|
||||
/* Custom inline header: 2-column flex-via-floats. Left has
|
||||
the company logo + address + phone + URL; right has the
|
||||
document title (bilingual stack) + Code128 barcode. */
|
||||
.fp-sale-header-row { overflow: hidden; margin-bottom: 14px; padding-bottom: 6px; }
|
||||
.fp-sale-header-left { float: left; width: 38%; }
|
||||
/* Middle column: NADCAP accreditation logo (pulled from
|
||||
company settings) above a small "25 Years in Business"
|
||||
medallion. Conditional on company.x_fc_nadcap_active so
|
||||
non-Nadcap shops only see the badge. */
|
||||
.fp-sale-header-mid { float: left; width: 24%; text-align: center; padding-top: 4px; }
|
||||
.fp-nadcap-logo { max-height: 45px; max-width: 115px; display: inline-block; }
|
||||
/* 25-Years-in-Business medallion: tasteful gold-on-cream
|
||||
bordered badge. Picks a dark muted gold (#8a6d2c) so it
|
||||
reads as "anniversary keepsake" rather than gaudy. */
|
||||
.fp-25-badge {
|
||||
display: inline-block;
|
||||
border: 1.5px solid #c8a55b;
|
||||
background-color: #faf5e8;
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
line-height: 1.05;
|
||||
}
|
||||
.fp-25-badge .fp-25-num { font-size: 22pt; font-weight: bold; color: #8a6d2c; }
|
||||
.fp-25-badge .fp-25-lbl { font-size: 7pt; font-weight: bold; color: #8a6d2c; letter-spacing: 1.2px; margin-top: 1px; }
|
||||
.fp-25-badge .fp-25-sub { font-size: 6.5pt; color: #8a6d2c; font-style: italic; margin-top: 1px; }
|
||||
/* Right column: title (English bold + French italic) + barcode
|
||||
+ SO number all centered as a stacked block. Width reduced
|
||||
to 38% to share row with the new middle NADCAP+25Years cell. */
|
||||
.fp-sale-header-right { float: right; width: 38%; text-align: center; }
|
||||
.fp-sale-logo { max-height: 50px; max-width: 280px; display: block; margin-bottom: 4px; }
|
||||
.fp-sale-company-addr { font-size: 8.5pt; color: #222; line-height: 1.35; }
|
||||
.fp-sale-company-addr div { margin: 0; }
|
||||
.fp-sale-company-addr a { color: #2e6da4; text-decoration: none; }
|
||||
|
||||
/* Inline footer line — phone | email | website | tax id.
|
||||
One-time render at the bottom of page 1 (multi-page SO
|
||||
reports are rare; if we ever need it, switch to
|
||||
wkhtmltopdf --footer-html). */
|
||||
.fp-sale-customfooter { text-align: center; font-size: 8pt; color: #666; margin-top: 24px; padding-top: 8px; border-top: 1px solid #ccc; }
|
||||
/* 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",
|
||||
@@ -65,48 +150,83 @@
|
||||
.fp-sale-barcode { float: right; margin-left: 12px; }
|
||||
.fp-bc-wrap { display: inline-block; text-align: center; }
|
||||
.fp-bc-wrap img { height: 48px; max-width: 240px; border: 0 !important; padding: 0; display: block; }
|
||||
.fp-bc-wrap .fp-bc-label { font-size: 10pt; color: #333; margin-top: 6px; letter-spacing: 0.5px; }
|
||||
.fp-bc-wrap .fp-bc-label { font-size: 16pt; font-weight: bold; color: #000; margin-top: 6px; letter-spacing: 1.5px; }
|
||||
</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"/>
|
||||
<t t-call="fusion_plating_reports.fp_sale_bilingual_styles"/>
|
||||
<!-- Internal form code rendered on the footer left side
|
||||
by fp_external_layout_clean (see that template). -->
|
||||
<t t-set="form_code" t-value="'FRM-006'"/>
|
||||
<t t-call="fusion_plating_reports.fp_external_layout_clean">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="company" t-value="doc.company_id or env.company"/>
|
||||
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
|
||||
<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 ''"/>
|
||||
<!-- 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 ''"/>
|
||||
<t t-set="logo_uri" t-value="('data:image/png;base64,%s' % company.logo.decode()) if company.logo else False"/>
|
||||
<t t-set="company_fax" t-value="company.partner_id.x_ff_fax_number if 'x_ff_fax_number' in company.partner_id._fields else False"/>
|
||||
|
||||
<div class="fp-report fp-sale">
|
||||
<div class="page">
|
||||
|
||||
<!-- Title bar: stacked English/French title
|
||||
on the left, Code128 barcode floated
|
||||
right. NO HTML table — see CLAUDE.md
|
||||
"wkhtmltopdf header overlap" §2 for why
|
||||
a table here leaks borders. -->
|
||||
<div class="fp-sale-titlebar">
|
||||
<t t-if="barcode_uri">
|
||||
<div class="fp-sale-barcode">
|
||||
<div class="fp-bc-wrap">
|
||||
<img t-att-src="barcode_uri" alt="Order Barcode"/>
|
||||
<div class="fp-bc-label"><span t-field="doc.name"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<span class="fp-sale-title-en">
|
||||
<t t-esc="title_en"/><span class="fp-sale-title-num"># <span t-field="doc.name"/></span>
|
||||
</span>
|
||||
<span class="fp-sale-title-fr"><t t-esc="title_fr"/></span>
|
||||
<div class="fp-report fp-sale">
|
||||
<!-- Inline header (drops web.external_layout for this
|
||||
report — see CSS comment for context). Left: logo
|
||||
+ address + tel/fax + URL. Right: bilingual title
|
||||
+ Code128 barcode of the order number. -->
|
||||
<div class="fp-sale-header-row">
|
||||
<div class="fp-sale-header-left">
|
||||
<t t-if="logo_uri">
|
||||
<img t-att-src="logo_uri" class="fp-sale-logo" alt="Logo"/>
|
||||
</t>
|
||||
<div class="fp-sale-company-addr">
|
||||
<div>
|
||||
<t t-if="company.partner_id.street"><span t-esc="company.partner_id.street"/></t>
|
||||
<t t-if="company.partner_id.city"> | <span t-esc="company.partner_id.city"/></t>
|
||||
<t t-if="company.partner_id.state_id"> | <span t-esc="company.partner_id.state_id.code or company.partner_id.state_id.name"/></t>
|
||||
<t t-if="company.partner_id.zip"> | <span t-esc="company.partner_id.zip"/></t>
|
||||
</div>
|
||||
<div t-if="company.phone or company_fax">
|
||||
<t t-if="company.phone">Tel: <span t-esc="company.phone"/></t>
|
||||
<t t-if="company.phone and company_fax">   </t>
|
||||
<t t-if="company_fax">Fax: <span t-esc="company_fax"/></t>
|
||||
</div>
|
||||
<div t-if="company.partner_id.website">
|
||||
<a t-att-href="company.partner_id.website"><span t-esc="company.partner_id.website"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Middle column: NADCAP logo only. Base64-inlined
|
||||
from `company.x_fc_nadcap_logo` so wkhtmltopdf
|
||||
doesn't have to fetch over HTTP (network calls
|
||||
fail on entech). -->
|
||||
<div class="fp-sale-header-mid">
|
||||
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
|
||||
<img class="fp-nadcap-logo"
|
||||
t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
|
||||
alt="Nadcap Accredited"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="fp-sale-header-right">
|
||||
<span class="fp-sale-title-en"><t t-esc="title_en"/></span>
|
||||
<span class="fp-sale-title-fr"><t t-esc="title_fr"/></span>
|
||||
<t t-if="barcode_uri">
|
||||
<div class="fp-bc-wrap" style="margin-top: 4px;">
|
||||
<img t-att-src="barcode_uri" alt="Order Barcode"/>
|
||||
<div class="fp-bc-label"><span t-field="doc.name"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<!-- Billing / Shipping (wide cells — inline) -->
|
||||
<table class="bordered">
|
||||
@@ -174,10 +294,18 @@
|
||||
<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>
|
||||
<!-- Lead Time renders from the
|
||||
computed display string on
|
||||
sale.order. Rush stays
|
||||
highlighted; everything
|
||||
else (range / single value
|
||||
/ Standard) is plain text. -->
|
||||
<t t-if="doc.x_fc_lead_time_display == 'Rush'">
|
||||
<span class="status-warning">Rush</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-esc="doc.x_fc_lead_time_display"/>
|
||||
</t>
|
||||
<t t-else="">Standard</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -267,13 +395,34 @@
|
||||
<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>
|
||||
<!-- Three stacked lines:
|
||||
1. Part Number (catalog part_number + revision)
|
||||
2. Name (catalog name, falls back to em-dash)
|
||||
3. Serial Number (m2m serials joined, or em-dash) -->
|
||||
<div>
|
||||
<strong>Part #:</strong>
|
||||
<t t-call="fusion_plating_reports.customer_line_part_number"/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Name:</strong>
|
||||
<t t-if="line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.name">
|
||||
<span t-esc="line.x_fc_part_catalog_id.name"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
<div>
|
||||
<strong>S/N:</strong>
|
||||
<t t-if="line.x_fc_serial_ids">
|
||||
<span t-esc="', '.join(line.x_fc_serial_ids.mapped('name'))"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<!-- Rebind `line` with fp_no_serial_in_desc=True so the
|
||||
shared description macro skips its Serial line — we
|
||||
already render S/N in the part-number cell above. -->
|
||||
<t t-set="line" t-value="line.with_context(fp_no_serial_in_desc=True)"/>
|
||||
<t t-call="fusion_plating_reports.customer_line_description"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
|
||||
Reference in New Issue
Block a user