Split 49 modules/suites into independent git repos; untrack from monorepo
Some checks failed
fusion_accounting CI / test (fusion_accounting_ai) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_core) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_migration) (push) Has been cancelled

Each top-level module/suite folder is now its own private repo on GitHub
(gsinghpal/<name>) and gitea (admin/<name>), with a fresh single initial
commit. The monorepo no longer tracks them (added to .gitignore + git rm
--cached); working-tree files are retained on disk and managed in their
own repos. The monorepo keeps shared root files (CLAUDE.md, docs/, scripts/,
tools/, AGENTS.md, WIP/obsolete dirs) and full history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-07 01:54:34 -04:00
parent 2a7b315e98
commit a66cdefc01
6740 changed files with 51 additions and 1277207 deletions

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import models
from . import controllers

View File

@@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating - Reports',
'version': '19.0.11.36.0',
'category': 'Manufacturing/Plating',
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
'depends': [
'sale',
'sale_pdf_quote_builder',
'account',
'stock',
# 'mrp' dep dropped post-Sub 11 (MRP cutout). Plating uses fp.job
# exclusively now. Re-introducing this dep silently pulls mrp +
# cascade back to `installed` on any -u base rescan.
'fusion_plating',
'fusion_plating_quality',
'fusion_plating_compliance',
'fusion_plating_safety',
'fusion_plating_portal',
'fusion_plating_configurator',
# NB: fusion_plating_jobs intentionally NOT depended on. Jobs depends
# on us (uses report_fp_wo_sticker_inner). Adding the reverse dep
# creates a cycle. Our only fp.job touchpoint is wo_scan.py which
# uses runtime env.get('fp.job') - safe without the manifest dep.
'fusion_plating_logistics',
# Needed for the packing-slip Print binding on fp.receiving
# (binding_model_id ref). Already a transitive dep via logistics;
# declared explicitly so the ref is robust. No cycle - receiving
# does not depend on reports.
'fusion_plating_receiving',
],
'data': [
'security/ir.model.access.csv',
'report/report_base_styles.xml',
'report/report_actions.xml',
# Quality + compliance reports
'report/report_coc.xml',
'report/report_coc_chronological.xml',
'report/report_fp_rack_travel.xml',
'report/report_ncr.xml',
'report/report_capa.xml',
'report/report_bath_chemistry_log.xml',
'report/report_calibration_cert.xml',
'report/report_fair.xml',
'report/report_audit.xml',
'report/report_incident.xml',
'report/report_spill.xml',
'report/report_waste_manifest.xml',
'report/report_discharge_sample.xml',
'report/report_wo_margin.xml',
# Quote-to-cash reports (portrait + landscape)
'report/customer_line_header.xml',
'report/report_fp_sale.xml',
'report/report_fp_work_order.xml',
'report/report_fp_wo_sticker.xml',
'report/report_fp_job_traveller.xml',
'report/report_fp_packing_slip.xml',
'report/report_fp_bol.xml',
'report/report_fp_invoice.xml',
'report/report_fp_receipt.xml',
# Sub 12 Phase E - quality/RMA reports.
'report/report_fp_rma_authorisation.xml',
'report/report_fp_8d.xml',
'report/report_fp_quality_monthly.xml',
# Hide Odoo's default reports from the Print menu wherever FP
# ships an equivalent (loaded last so it overrides any earlier
# binding declarations from base modules).
'data/fp_hide_default_reports.xml',
],
'installable': True,
'application': False,
'auto_install': False,
'license': 'OPL-1',
}

View File

@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import wo_scan

View File

@@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# /fp/wo/<id> - scan-redirect endpoint.
#
# The WO box sticker embeds a QR code that encodes this URL. When
# warehouse staff scan the sticker with their phone / tablet /
# handheld scanner, the device opens the URL; this controller then
# redirects them to the work-order form inside Odoo's backend.
# Logged-out users land on the standard Odoo login page and bounce
# back after authenticating (Odoo's redirect handles the round-trip).
from odoo import http
from odoo.http import request
class FpWoScanController(http.Controller):
@http.route('/fp/wo/<int:wo_id>', type='http', auth='user', website=False)
def wo_scan_redirect(self, wo_id, **kwargs):
"""Redirect a scanned sticker to the right backend form.
Resolution order:
1. fp.job mapped from this MO id via legacy_mrp_production_id
(post-migration: physical stickers still encode the old MO
id, but the canonical record is now an fp.job)
2. mrp.production with this id (pre-migration callers, or if
the legacy mapping wasn't run)
3. mrp.workorder with this id (older stickers that encoded
the WO id rather than the MO id)
4. fall back to the jobs list so staff can search manually.
"""
env = request.env
# 1) New native model - preferred when migration has run.
if 'fp.job' in env and 'legacy_mrp_production_id' in env['fp.job']._fields:
job = env['fp.job'].sudo().search(
[('legacy_mrp_production_id', '=', wo_id)], limit=1)
if job:
return request.redirect(
'/odoo/action-fusion_plating.action_fp_job/%d' % job.id
)
# 2) Legacy MO/WO fallbacks (only if MRP is still installed).
if 'mrp.production' in env:
mo = env['mrp.production'].sudo().browse(wo_id).exists()
if mo:
return request.redirect(
'/odoo/action-mrp.mrp_production_action/%d' % mo.id
)
if 'mrp.workorder' in env:
wo = env['mrp.workorder'].sudo().browse(wo_id).exists()
if wo:
return request.redirect(
'/odoo/action-mrp.action_mrp_workorder/%d' % wo.id
)
# 3) Fall back to native jobs list.
return request.redirect('/odoo/plating-jobs')

View File

@@ -1,155 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Hide Odoo's default PDF reports from the Print dropdown wherever
Fusion Plating ships a branded equivalent. This prevents users from
accidentally sending the wrong (unbranded, missing-fields) PDF to
customers when both options are visible side by side.
Mechanism: setting `binding_model_id` to False (and `binding_type`
to 'action') removes the report from the model's Print dropdown but
leaves the underlying report record + template intact. An admin can
re-enable any of these from Settings → Technical → Actions → Reports
if needed (no schema change, fully reversible).
Reports we intentionally leave alone:
- sale.action_report_pro_forma_invoice (no FP pro-forma yet)
- account.action_account_original_vendor_bill
- stock.action_report_picking_packages (internal warehouse ops)
- stock.action_report_picking (internal warehouse ops)
- stock.return_label_report (internal returns)
- mrp.action_report_finished_product (production label, ZPL)
- mrp.label_manufacture_template (ZPL label)
- sale_timesheet.* (timesheet integration)
-->
<odoo noupdate="0">
<!-- ================================================================
sale.order - hide Odoo's PDF Quote + raw Quotation
FP ships fp_sale (portrait + landscape) with full plating layout
================================================================ -->
<record id="sale.action_report_saleorder" model="ir.actions.report">
<field name="binding_model_id" eval="False"/>
<field name="binding_type">action</field>
</record>
<record id="sale_pdf_quote_builder.action_report_saleorder_raw" model="ir.actions.report">
<field name="binding_model_id" eval="False"/>
<field name="binding_type">action</field>
</record>
<!-- ================================================================
account.move - hide Odoo's stock invoice PDFs
FP ships fp_invoice (portrait + landscape) with PO#, plating job
refs, deposit / progress / net-terms strategies built in
================================================================ -->
<record id="account.account_invoices" model="ir.actions.report">
<field name="binding_model_id" eval="False"/>
<field name="binding_type">action</field>
</record>
<record id="account.account_invoices_without_payment" model="ir.actions.report">
<field name="binding_model_id" eval="False"/>
<field name="binding_type">action</field>
</record>
<!-- ================================================================
stock.picking - hide Odoo's Delivery Slip
FP ships fp_packing_slip + fp_bol covering the customer-facing
shipping documents
================================================================ -->
<record id="stock.action_report_delivery" model="ir.actions.report">
<field name="binding_model_id" eval="False"/>
<field name="binding_type">action</field>
</record>
<!-- mrp.production hide-block removed post-Sub 11 (MRP module
uninstalled; xmlid is no longer resolvable). -->
<!-- ================================================================
account.payment - hide Odoo's Payment Receipt
FP ships fp_receipt with PO# and plating job context
================================================================ -->
<record id="account.action_report_payment_receipt" model="ir.actions.report">
<field name="binding_model_id" eval="False"/>
<field name="binding_type">action</field>
</record>
<!-- ================================================================
Print-menu sequencing - pin FP reports to the TOP of each
dropdown so customer-facing reports appear before internal
Odoo defaults (timesheets, picking ops, finished-product
labels, etc.) which now sit at sequence 100 by default.
Convention: Portrait = primary (10) → Landscape = secondary (15)
================================================================ -->
<!-- sale.order: Quotation/Sales Order is the primary -->
<record id="fusion_plating_reports.action_report_fp_sale_portrait" model="ir.actions.report">
<field name="sequence" eval="10"/>
</record>
<record id="fusion_plating_reports.action_report_fp_sale_landscape" model="ir.actions.report">
<field name="sequence" eval="15"/>
</record>
<record id="fusion_plating_reports.action_report_fp_job_traveller_so_portrait" model="ir.actions.report">
<field name="sequence" eval="20"/>
</record>
<record id="fusion_plating_reports.action_report_fp_job_traveller_so_landscape" model="ir.actions.report">
<field name="sequence" eval="25"/>
</record>
<!-- account.move: Invoice - Plating is the primary -->
<record id="fusion_plating_reports.action_report_fp_invoice_portrait" model="ir.actions.report">
<field name="sequence" eval="10"/>
</record>
<record id="fusion_plating_reports.action_report_fp_invoice_landscape" model="ir.actions.report">
<field name="sequence" eval="15"/>
</record>
<!-- stock.picking: Packing Slip is the primary -->
<record id="fusion_plating_reports.action_report_fp_packing_slip_portrait" model="ir.actions.report">
<field name="sequence" eval="10"/>
</record>
<record id="fusion_plating_reports.action_report_fp_packing_slip_landscape" model="ir.actions.report">
<field name="sequence" eval="15"/>
</record>
<!-- mrp.production traveller sequencing removed post-Sub 11 - those
ir.actions.report records were bound to mrp.production and got
removed when Sub 11 cascade-uninstalled mrp. The fp.job-bound
versions are sequenced in their own report files now.
action_report_wo_margin reference also removed - it was an
ir.actions.report bound to mrp.workorder that went with Sub 11. -->
<!-- account.payment: Receipt - primary -->
<record id="fusion_plating_reports.action_report_fp_receipt_portrait" model="ir.actions.report">
<field name="sequence" eval="10"/>
</record>
<record id="fusion_plating_reports.action_report_fp_receipt_landscape" model="ir.actions.report">
<field name="sequence" eval="15"/>
</record>
<!-- fusion.plating.delivery: Bill of Lading -->
<record id="fusion_plating_reports.action_report_fp_bol_portrait" model="ir.actions.report">
<field name="sequence" eval="10"/>
</record>
<record id="fusion_plating_reports.action_report_fp_bol_landscape" model="ir.actions.report">
<field name="sequence" eval="15"/>
</record>
<!-- fp.certificate: single bilingual cert (French action removed 2026-05-28) -->
<record id="fusion_plating_reports.action_report_coc_en" model="ir.actions.report">
<field name="sequence" eval="10"/>
</record>
<!-- portal job CoC -->
<record id="fusion_plating_reports.action_report_coc_portrait" model="ir.actions.report">
<field name="sequence" eval="10"/>
</record>
<record id="fusion_plating_reports.action_report_coc" model="ir.actions.report">
<field name="sequence" eval="15"/>
</record>
</odoo>

View File

@@ -1,8 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from . import ir_actions_report
from . import report_wo_margin
from . import report_fp_quality_monthly

View File

@@ -1,73 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
"""Patch ir.actions.report so the Print dropdown can be ordered.
Odoo 19 fetches print-menu bindings via `ir.actions.actions._get_bindings`
which returns reports in `ORDER BY a.id` (insertion order). Only the
`action` bindings get a sequence sort applied - `report` bindings are
returned in the raw SQL order. Result: third-party FP reports installed
after Odoo's stock ones always appear at the BOTTOM of the dropdown,
even when they're the customer-facing primary report.
Two changes:
1. Add a `sequence` Integer field to ir.actions.report.
2. Override `_get_bindings` to also sort report bindings by sequence
(then by name as a tie-breaker), matching the behaviour Odoo
already applies to action bindings.
Lower sequence = appears higher in the Print dropdown.
"""
import base64
from odoo import api, fields, models
from odoo.tools import frozendict
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
sequence = fields.Integer(
default=100,
help='Order in which this report appears in the Print menu '
'(lower = higher in the list). Default 100 leaves room '
'for both higher and lower priorities.',
)
@api.model
def barcode_data_uri(self, barcode_type, value, width=300, height=300):
"""Return a data:image/png;base64 URI for a barcode/QR.
wkhtmltopdf can't always fetch /report/barcode/ over the network
during PDF rendering (sandbox/DNS/base-url pitfalls), so reports
that embed QR codes on labels inline them as base64 instead.
"""
png = self.barcode(
barcode_type, value,
width=width, height=height, humanreadable=0,
) or b''
return 'data:image/png;base64,' + base64.b64encode(png).decode('ascii')
class IrActionsActions(models.Model):
_inherit = 'ir.actions.actions'
@api.model
def _get_bindings(self, model_name):
# super() returns a cached frozendict via @tools.ormcache; we
# re-sort the 'report' slice (Odoo already sorts 'action').
result = super()._get_bindings(model_name)
if not result.get('report'):
return result
sorted_reports = tuple(sorted(
result['report'],
key=lambda vals: (
vals.get('sequence', 100),
(vals.get('name') or '').lower(),
),
))
# frozendict is immutable - rebuild from a plain dict.
new_result = dict(result)
new_result['report'] = sorted_reports
return frozendict(new_result)

View File

@@ -1,182 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
#
# Sub 12 Phase E - backing data computation for the Monthly Quality
# Summary PDF.
from datetime import timedelta
from odoo import api, fields, models
class ReportFpQualityMonthly(models.AbstractModel):
_name = 'report.fusion_plating_reports.report_fp_quality_monthly_doc'
_description = 'Monthly Quality Summary - Backing'
@api.model
def _get_report_values(self, docids, data=None):
Company = self.env['res.company']
# Default to the user's current company when called from a menu
# action with no record selection (docids will be False/None/[]).
companies = Company.browse(docids) if docids else self.env.company
today = fields.Date.context_today(self.env.user)
period_start = today.replace(day=1)
period_label = (
f'{period_start.strftime("%B %Y")} '
f'(running through {today.strftime("%Y-%m-%d")})'
)
Hold = self.env['fusion.plating.quality.hold']
Check = self.env['fusion.plating.quality.check']
Ncr = self.env['fusion.plating.ncr']
Capa = self.env['fusion.plating.capa']
Rma = self.env['fusion.plating.rma'] \
if 'fusion.plating.rma' in self.env else None
def _bytype(model, label, opened_field, closed_field, open_dom,
overdue_dom):
if model is None:
return {
'label': label, 'opened': 0, 'closed': 0,
'open_total': 0, 'overdue': 0,
}
opened = model.search_count([
(opened_field, '>=', period_start),
])
closed = (
model.search_count([(closed_field, '>=', period_start)])
if closed_field else 0
)
return {
'label': label,
'opened': opened,
'closed': closed,
'open_total': model.search_count(open_dom),
'overdue': model.search_count(overdue_dom),
}
cutoff_3d = fields.Datetime.subtract(fields.Datetime.now(), days=3)
cutoff_7d = fields.Datetime.subtract(fields.Datetime.now(), days=7)
cutoff_5d = fields.Datetime.subtract(fields.Datetime.now(), days=5)
cutoff_14d = fields.Datetime.subtract(fields.Datetime.now(), days=14)
by_type = [
_bytype(
Hold, 'Quality Holds',
'create_date', None,
[('state', 'in', ('on_hold', 'under_review'))],
[('state', 'in', ('on_hold', 'under_review')),
('create_date', '<', cutoff_3d)],
),
_bytype(
Check, 'QC Checks',
'create_date', None,
[('state', '=', 'pending')] if 'state' in Check._fields else [],
[],
),
_bytype(
Ncr, 'Non-Conformance Reports',
'reported_date', 'closed_date',
[('state', 'in', ('open', 'containment', 'disposition'))],
[('state', 'in', ('open', 'containment', 'disposition')),
('reported_date', '<', cutoff_7d)],
),
_bytype(
Capa, 'CAPAs',
'create_date', None,
[('state', 'not in', ('effective', 'closed'))],
[('state', 'not in', ('effective', 'closed')),
('due_date', '<', today),
('due_date', '!=', False)],
),
]
if Rma is not None:
by_type.append(_bytype(
Rma, 'RMAs',
'create_date', None,
[('state', 'not in', ('closed', 'cancelled'))],
['|',
'&', ('state', '=', 'received'),
('create_date', '<', cutoff_5d),
'&', ('state', 'in', ('authorised', 'shipped_to_us')),
('create_date', '<', cutoff_14d)],
))
# NCR severity
ncr_severity = []
for sev_code, sev_label in [
('critical', 'Critical'), ('high', 'High'),
('medium', 'Medium'), ('low', 'Low'),
]:
ncr_severity.append({
'label': sev_label,
'count': Ncr.search_count([
('severity', '=', sev_code),
('reported_date', '>=', period_start),
]),
})
# CAPA effectiveness
closed_in_period = Capa.search_count([
('state', 'in', ('effective', 'closed', 'not_effective')),
('verification_date', '>=', period_start),
])
effective = Capa.search_count([
('state', '=', 'effective'),
('verification_date', '>=', period_start),
])
not_effective = Capa.search_count([
('state', '=', 'not_effective'),
('verification_date', '>=', period_start),
])
rate_pct = (
int(round(100.0 * effective / closed_in_period))
if closed_in_period else 0
)
# Repeat customers (≥3 NCRs in last 90 days)
cutoff_90d = today - timedelta(days=90)
# Odoo 19 - use _read_group with aggregates=['__count'].
groups = self.env['fusion.plating.ncr']._read_group(
domain=[('reported_date', '>=', cutoff_90d),
('customer_partner_id', '!=', False)],
groupby=['customer_partner_id'],
aggregates=['__count'],
)
repeat_customers = []
for partner, count in groups:
if count < 3:
continue
rma_count = (
Rma.search_count([
('partner_id', '=', partner.id),
('state', 'not in', ('closed', 'cancelled')),
]) if Rma else 0
)
repeat_customers.append({
'name': partner.display_name,
'ncr_count': count,
'rma_count': rma_count,
})
repeat_customers.sort(key=lambda r: r['ncr_count'], reverse=True)
return {
'doc_ids': companies.ids,
'doc_model': 'res.company',
'docs': companies,
'data': {
'period_label': period_label,
'generated_at': fields.Datetime.now().strftime('%Y-%m-%d %H:%M'),
'by_type': by_type,
'ncr_severity': ncr_severity,
'capa': {
'closed': closed_in_period,
'effective': effective,
'not_effective': not_effective,
'rate_pct': rate_pct,
},
'repeat_customers': repeat_customers,
},
}

View File

@@ -1,171 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, models
class ReportWoMargin(models.AbstractModel):
"""Work Order Margin Report Data.
Computes cost, revenue, and margin breakdowns for manufacturing orders
so the QWeb template can render a Steelhead-style margin report.
"""
_name = 'report.fusion_plating_reports.report_wo_margin'
_description = 'WO Margin Report Data'
# ------------------------------------------------------------------
# helpers
# ------------------------------------------------------------------
def _get_station_costs(self, mo):
"""Return a list of dicts with per-station cost breakdown."""
station_costs = []
for wo in mo.workorder_ids:
wc = wo.workcenter_id
labour_rate = wc.costs_hour if wc else 0.0
# Sum tracked time from productivity records (minutes)
time_minutes = sum(wo.time_ids.mapped('duration'))
time_hours = time_minutes / 60.0
labour_cost = time_hours * labour_rate
# Operation cost uses the same rate & dwell for now
operation_rate = labour_rate
dwell_minutes = time_minutes
dwell_hours = dwell_minutes / 60.0
operation_cost = dwell_hours * operation_rate
total = labour_cost + operation_cost
station_costs.append({
'station': wc.name if wc else 'Unknown',
'labour_rate': labour_rate,
'labour_time': time_minutes,
'labour_hours': time_hours,
'labour_cost': labour_cost,
'operation_rate': operation_rate,
'dwell_time': dwell_minutes,
'dwell_hours': dwell_hours,
'operation_cost': operation_cost,
'total_cost': total,
})
return station_costs
def _get_part_margins(self, mo, revenue):
"""Return margin breakdown per unique product (part number)."""
parts = {}
for wo in mo.workorder_ids:
product = wo.product_id or mo.product_id
key = product.id
if key not in parts:
parts[key] = {
'product': product,
'part_number': product.default_code or product.name,
'count': 0,
'labour_cost': 0.0,
'station_labour_cost': 0.0,
'station_operation_cost': 0.0,
'outsourcing_cost': 0.0,
}
entry = parts[key]
entry['count'] += 1
time_hours = sum(wo.time_ids.mapped('duration')) / 60.0
rate = wo.workcenter_id.costs_hour if wo.workcenter_id else 0.0
cost = time_hours * rate
entry['labour_cost'] += cost
entry['station_labour_cost'] += cost
entry['station_operation_cost'] += cost
# Distribute revenue equally across parts for per-part metrics
part_list = list(parts.values())
total_parts = sum(p['count'] for p in part_list) or 1
for p in part_list:
p['so_total'] = revenue * (p['count'] / total_parts)
p['so_per_part'] = p['so_total'] / p['count'] if p['count'] else 0
p['unit_labour'] = p['labour_cost'] / p['count'] if p['count'] else 0
total_cost = (
p['labour_cost'] + p['station_labour_cost']
+ p['station_operation_cost'] + p['outsourcing_cost']
)
p['margin_pct'] = (
(p['so_total'] - total_cost) / p['so_total'] * 100
) if p['so_total'] else 0
return part_list
# ------------------------------------------------------------------
# Report entry point
# ------------------------------------------------------------------
@api.model
def _get_report_values(self, docids, data=None):
# Sub 11 - MRP gone. The report is bound to fusion_plating_reports.action_report_wo_margin
# which itself was uninstalled. Returning empty docs keeps the
# AbstractModel safe to import (its sister fp.job report
# report_fp_job_margin owns the live margin path now).
if 'mrp.production' not in self.env:
return {'doc_ids': [], 'doc_model': 'mrp.production', 'docs': []}
productions = self.env['mrp.production'].browse(docids)
docs = []
for mo in productions:
# Revenue from linked sale order
revenue = 0.0
if hasattr(mo, 'sale_order_id') and mo.sale_order_id:
revenue = sum(
mo.sale_order_id.order_line.mapped('price_subtotal')
)
# Station costs
station_costs = self._get_station_costs(mo)
part_labour_cost = sum(s['labour_cost'] for s in station_costs)
station_labour_cost = part_labour_cost # same pool
station_operation_cost = sum(
s['operation_cost'] for s in station_costs
)
# Inventory cost (raw materials)
inventory_cost = sum(
m.product_id.standard_price * m.quantity
for m in mo.move_raw_ids
)
# Outsourcing cost placeholder
outsourcing_cost = 0.0
total_cost = (
part_labour_cost + station_labour_cost
+ station_operation_cost + inventory_cost
+ outsourcing_cost
)
gross_profit = revenue - total_cost
margin_pct = (
(gross_profit / revenue * 100) if revenue else 0.0
)
# Station cost percentages
total_station_cost = sum(s['total_cost'] for s in station_costs)
for s in station_costs:
s['percentage'] = (
(s['total_cost'] / total_station_cost * 100)
if total_station_cost else 0
)
# Part margins
part_margins = self._get_part_margins(mo, revenue)
docs.append({
'mo': mo,
'revenue': revenue,
'part_labour_cost': part_labour_cost,
'station_labour_cost': station_labour_cost,
'station_operation_cost': station_operation_cost,
'inventory_cost': inventory_cost,
'outsourcing_cost': outsourcing_cost,
'total_cost': total_cost,
'gross_profit': gross_profit,
'margin_pct': margin_pct,
'station_costs': station_costs,
'part_margins': part_margins,
})
return {
'doc_ids': docids,
'docs': docs,
}

View File

@@ -1,119 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1
Sub 2 - shared QWeb macro for customer-facing line rendering.
Called from report_fp_sale, report_fp_invoice, report_fp_packing_slip,
report_fp_bol. Prints the customer's part number + revision + the
line's customer-facing description (the `name` field, standard Odoo).
For non-part lines (rush fees, freight, expedite) where
x_fc_part_catalog_id is blank, falls back to Odoo's standard product
display - safe for fee/service lines that shouldn't look like parts.
Params expected in the calling context:
line - the sale.order.line / account.move.line / stock.picking line
Usage:
<t t-call="fusion_plating_reports.customer_line_header"/>
-->
<odoo>
<!-- ==========================================================
customer_line_header (legacy - kept for backward compat)
Prints part number + revision + description in ONE td.
Reports written before the 2026-04-23 column split still
call this macro. New reports should use the split macros
below (customer_line_part_number + customer_line_description)
which render into two separate <td> columns.
========================================================== -->
<template id="customer_line_header">
<t t-call="fusion_plating_reports.customer_line_part_number"/>
<br/>
<t t-call="fusion_plating_reports.customer_line_description"/>
</template>
<!-- ==========================================================
customer_line_part_number - just the part number + rev
Renders as a single-line "PN-1234 Rev A" block, intended
for the "Part Number" td in customer-facing tables.
========================================================== -->
<template id="customer_line_part_number">
<t t-if="line.x_fc_part_catalog_id">
<strong>
<span t-esc="line.x_fc_part_catalog_id.part_number"/>
<!-- Prefer the Sub-5 revision snapshot captured on the
line at save time; fall back to the catalog's
current revision for older lines that predate it. -->
<t t-set="_rev" t-value="(
line.x_fc_revision_snapshot
if 'x_fc_revision_snapshot' in line._fields
and line.x_fc_revision_snapshot
else line.x_fc_part_catalog_id.revision)"/>
<t t-if="_rev">
<span> (Rev <span t-esc="_rev"/>)</span>
</t>
</strong>
</t>
<t t-else="">
<!-- Fee / freight / non-part line: no part number to show -->
<span class="text-muted">-</span>
</t>
</template>
<!-- ==========================================================
customer_line_description - customer-facing description
plus serial + thickness only.
Per client request (2026-04-29): customer-facing reports
show ONLY description, serial, and thickness. Job # was
previously shown here but is internal-only - it lives on
the traveller / WO sticker / packing slip header, not on
what the customer sees. Process variant, treatment names,
and recipe codes deliberately don't render either.
========================================================== -->
<template id="customer_line_description">
<t t-if="line.x_fc_part_catalog_id">
<!-- Customer-facing description column (2026-05-27 rebuild).
Per user request:
- Only the description and thickness print
- Both rendered in CAPITAL LETTERS via CSS
The "spec - PART (xN)" header that used to prefix
line.name has been removed at the wizard side; this
macro just renders the cleaned description.
white-space: pre-line preserves multi-line breaks. -->
<t t-set="_has_helper"
t-value="line._name in ('sale.order.line', 'account.move.line')"/>
<t t-set="_desc"
t-value="line.fp_customer_description() if _has_helper else line.name"/>
<div style="text-transform: uppercase; white-space: pre-line; font-weight: 500;">
<t t-esc="_desc"/>
</div>
<!-- Serial line is suppressed when the calling template sets
`fp_no_serial_in_desc=True` (SO portrait shows S/N in its
own column). 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')">
<div style="text-transform: uppercase; margin-top: 4px;">
<small>SERIAL: <span t-esc="line.x_fc_serial_id.name"/></small>
</div>
</t>
<t t-if="'x_fc_thickness_range' in line._fields and line.x_fc_thickness_range">
<div style="text-transform: uppercase; margin-top: 4px;">
<small>THICKNESS: <span t-esc="line.x_fc_thickness_range"/></small>
</div>
</t>
</t>
<t t-else="">
<!-- Fee / freight / non-part line: standard Odoo rendering, not uppercased -->
<strong t-esc="line.product_id.display_name or ''"/>
<t t-if="line.name and line.name != line.product_id.display_name">
<br/>
<span t-esc="line.name"/>
</t>
</t>
</template>
</odoo>

View File

@@ -1,540 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Paper format + report actions for all Fusion Plating reports.
-->
<odoo>
<!-- ============================================================= -->
<!-- Landscape Paper Format -->
<!-- ============================================================= -->
<record id="paperformat_fp_a4_landscape" model="report.paperformat">
<field name="name">A4 Landscape (Fusion Plating)</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Landscape</field>
<field name="margin_top">20</field>
<field name="margin_bottom">20</field>
<field name="margin_left">7</field>
<field name="margin_right">7</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">20</field>
<field name="dpi">90</field>
</record>
<!-- ============================================================= -->
<!-- CoC paper format - zero header/footer band so the title -->
<!-- starts at the top of the page, not 35mm down. -->
<!-- ============================================================= -->
<record id="paperformat_fp_coc" model="report.paperformat">
<field name="name">Fusion Plating CoC</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<field name="margin_top">8</field>
<field name="margin_bottom">8</field>
<field name="margin_left">8</field>
<field name="margin_right">8</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<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>
<!-- Mirrors paperformat_fp_coc exactly. Tiny margin_top (8mm)
puts the rendered header band flush at the top of the page;
the body clears the header via padding-top on the .fp-sale
wrapper (same way .fp-coc { padding-top: 20mm } clears it
on the CoC). DO NOT set margin_top to "the header height"
- that forces the header to live entirely inside the
reserved zone and any header growth = body overlap.
margin_top=8 + padding-on-wrapper is the proven shape. -->
<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>
<!-- ============================================================= -->
<!-- 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 -->
<!-- ============================================================= -->
<record id="action_report_coc" model="ir.actions.report">
<field name="name">Certificate of Conformance (Landscape)</field>
<field name="model">fusion.plating.portal.job</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_coc</field>
<field name="report_file">fusion_plating_reports.report_coc</field>
<field name="print_report_name">'CoC - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_portal.model_fusion_plating_portal_job"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- Certificate of Conformance - Portrait (legacy, Portal Job) -->
<record id="action_report_coc_portrait" model="ir.actions.report">
<field name="name">Certificate of Conformance (Portrait)</field>
<field name="model">fusion.plating.portal.job</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_coc_portrait</field>
<field name="report_file">fusion_plating_reports.report_coc_portrait</field>
<field name="print_report_name">'CoC - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_portal.model_fusion_plating_portal_job"/>
<field name="binding_type">report</field>
</record>
<!-- ============================================================= -->
<!-- Certificate of Conformance - single bilingual cert. -->
<!-- The body renders English + the French translation together, so -->
<!-- the separate French print action was removed 2026-05-28 (orphan -->
<!-- DB row + report_coc_fr template unlinked on entech). Compact -->
<!-- portrait paperformat (margin_top=8) so coc_header sits at top. -->
<!-- ============================================================= -->
<record id="action_report_coc_en" model="ir.actions.report">
<field name="name">Certificate of Conformance</field>
<field name="model">fp.certificate</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_coc_en</field>
<field name="report_file">fusion_plating_reports.report_coc_en</field>
<field name="print_report_name">'CoC - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_certificates.model_fp_certificate"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
</record>
<!-- ============================================================= -->
<!-- 2. Non-Conformance Report -->
<!-- ============================================================= -->
<record id="action_report_ncr" model="ir.actions.report">
<field name="name">Non-Conformance Report</field>
<field name="model">fusion.plating.ncr</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_ncr</field>
<field name="report_file">fusion_plating_reports.report_ncr</field>
<field name="print_report_name">'NCR - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_quality.model_fusion_plating_ncr"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 3. Corrective / Preventive Action -->
<!-- ============================================================= -->
<record id="action_report_capa" model="ir.actions.report">
<field name="name">CAPA Report</field>
<field name="model">fusion.plating.capa</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_capa</field>
<field name="report_file">fusion_plating_reports.report_capa</field>
<field name="print_report_name">'CAPA - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_quality.model_fusion_plating_capa"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 4. Bath Chemistry Log -->
<!-- ============================================================= -->
<record id="action_report_bath_log" model="ir.actions.report">
<field name="name">Bath Chemistry Log</field>
<field name="model">fusion.plating.bath.log</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_bath_chemistry_log</field>
<field name="report_file">fusion_plating_reports.report_bath_chemistry_log</field>
<field name="print_report_name">'Bath Log - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating.model_fusion_plating_bath_log"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 5. Calibration Certificate -->
<!-- ============================================================= -->
<record id="action_report_calibration" model="ir.actions.report">
<field name="name">Calibration Certificate</field>
<field name="model">fusion.plating.calibration.equipment</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_calibration_cert</field>
<field name="report_file">fusion_plating_reports.report_calibration_cert</field>
<field name="print_report_name">'Calibration - %s' % object.code</field>
<field name="binding_model_id" ref="fusion_plating_quality.model_fusion_plating_calibration_equipment"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 6. First Article Inspection Report -->
<!-- ============================================================= -->
<record id="action_report_fair" model="ir.actions.report">
<field name="name">FAIR Report</field>
<field name="model">fusion.plating.fair</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fair</field>
<field name="report_file">fusion_plating_reports.report_fair</field>
<field name="print_report_name">'FAIR - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_quality.model_fusion_plating_fair"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 7. Audit Report -->
<!-- ============================================================= -->
<record id="action_report_audit" model="ir.actions.report">
<field name="name">Audit Report</field>
<field name="model">fusion.plating.audit</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_audit</field>
<field name="report_file">fusion_plating_reports.report_audit</field>
<field name="print_report_name">'Audit - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_quality.model_fusion_plating_audit"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 8. Incident Report -->
<!-- ============================================================= -->
<record id="action_report_incident" model="ir.actions.report">
<field name="name">Incident Report</field>
<field name="model">fusion.plating.incident</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_incident</field>
<field name="report_file">fusion_plating_reports.report_incident</field>
<field name="print_report_name">'Incident - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_safety.model_fusion_plating_incident"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 9. Spill Register -->
<!-- ============================================================= -->
<record id="action_report_spill" model="ir.actions.report">
<field name="name">Spill Report</field>
<field name="model">fusion.plating.spill.register</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_spill</field>
<field name="report_file">fusion_plating_reports.report_spill</field>
<field name="print_report_name">'Spill - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_compliance.model_fusion_plating_spill_register"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 10. Waste Manifest -->
<!-- ============================================================= -->
<record id="action_report_waste_manifest" model="ir.actions.report">
<field name="name">Waste Manifest</field>
<field name="model">fusion.plating.waste.manifest</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_waste_manifest</field>
<field name="report_file">fusion_plating_reports.report_waste_manifest</field>
<field name="print_report_name">'Waste Manifest - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_compliance.model_fusion_plating_waste_manifest"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 11. Discharge Sample -->
<!-- ============================================================= -->
<record id="action_report_discharge_sample" model="ir.actions.report">
<field name="name">Discharge Sample Report</field>
<field name="model">fusion.plating.discharge.sample</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_discharge_sample</field>
<field name="report_file">fusion_plating_reports.report_discharge_sample</field>
<field name="print_report_name">'Discharge - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_compliance.model_fusion_plating_discharge_sample"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- 12. Work Order Margin Report (mrp.production binding) - REMOVED.
Replaced by the fp.job-bound version in
fusion_plating_jobs/report/report_fp_job_margin.xml.
The QWeb template (report_wo_margin) remains in templates for
any out-of-band callers; only the Print menu entry is gone. -->
<!-- ============================================================= -->
<!-- 13. Quotation / Sales Order (Portrait + Landscape) -->
<!-- ============================================================= -->
<record id="action_report_fp_sale_portrait" model="ir.actions.report">
<field name="name">Quotation / Order (Portrait)</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_sale_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_sale_portrait</field>
<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>
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
</record>
<record id="action_report_fp_sale_landscape" model="ir.actions.report">
<field name="name">Quotation / Order (Landscape)</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_sale_landscape</field>
<field name="report_file">fusion_plating_reports.report_fp_sale_landscape</field>
<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>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- 14. Work Order Traveller (mrp.workorder bindings) - REMOVED.
Replaced by fp.job-bound traveller in
fusion_plating_jobs/report/report_fp_job_traveller.xml. -->
<!-- ============================================================= -->
<!-- 14b. Box Sticker - 4x3" label for parts-box identification -->
<!-- Prints an ENTECH-style sticker with a QR code that -->
<!-- warehouse staff scan to jump straight to the WO form. -->
<!-- ============================================================= -->
<!-- 102x76mm = 4x3" physical label. Orientation MUST be Portrait: a
"custom" wkhtmltopdf page already takes page_width/page_height
as literal dimensions, and adding a Landscape orientation flag on
top of that swaps the dims AND rotates content, producing a
stretched 76x102 portrait page (not what we want). -->
<record id="paperformat_fp_wo_sticker" model="report.paperformat">
<field name="name">FP WO Sticker (6x4")</field>
<field name="format">custom</field>
<field name="page_width">152</field>
<field name="page_height">102</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">0</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<field name="disable_shrinking" eval="True"/>
<!-- dpi=300 is the calibrated value for this paperformat; the
sticker inner's px-based geometry is tuned against it. Do
NOT bump (see CLAUDE.md rule 14 - 600 broke layout). -->
<field name="dpi">300</field>
</record>
<!-- WO Box Sticker (mrp.workorder + mrp.production bindings) -
REMOVED. Replaced by the fp.job-bound version in
fusion_plating_jobs/report/report_fp_job_sticker.xml.
The shared inner templates (report_fp_wo_sticker_inner /
report_fp_wo_sticker_defaults) stay registered because
fp.job + sale.order stickers both t-call them. -->
<!-- Same sticker bound to sale.order - prints one sticker per
order line that carries a part, so estimators / receiving can
hand them to the floor before fp.jobs even exist. Uses the
same paperformat (6x4") so estimators don't need to think
about page size; the output PDF is multi-page if the SO has
multiple plating lines. -->
<record id="action_report_fp_so_sticker" model="ir.actions.report">
<field name="name">External Sticker</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_so_sticker</field>
<field name="report_file">fusion_plating_reports.report_fp_so_sticker</field>
<field name="print_report_name">'External Sticker - %s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_wo_sticker"/>
</record>
<!-- SO Internal sticker - same layout, prints internal description
instead of the customer-facing line.name. Shop-floor variant. -->
<record id="action_report_fp_so_sticker_internal" model="ir.actions.report">
<field name="name">Internal Sticker</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_so_sticker_internal</field>
<field name="report_file">fusion_plating_reports.report_fp_so_sticker_internal</field>
<field name="print_report_name">'Internal Sticker - %s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_wo_sticker"/>
</record>
<!-- ============================================================= -->
<!-- 15. Packing Slip (Portrait + Landscape) -->
<!-- ============================================================= -->
<record id="action_report_fp_packing_slip_portrait" model="ir.actions.report">
<field name="name">Packing Slip (Portrait)</field>
<field name="model">stock.picking</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_packing_slip_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_packing_slip_portrait</field>
<field name="print_report_name">'Packing Slip - %s' % (object.sale_id.name.rsplit('-', 1)[-1] if object.sale_id else object.name)</field>
<field name="binding_model_id" ref="stock.model_stock_picking"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
</record>
<record id="action_report_fp_packing_slip_landscape" model="ir.actions.report">
<field name="name">Packing Slip (Landscape)</field>
<field name="model">stock.picking</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_packing_slip_landscape</field>
<field name="report_file">fusion_plating_reports.report_fp_packing_slip_landscape</field>
<field name="print_report_name">'Packing Slip - %s' % (object.sale_id.name.rsplit('-', 1)[-1] if object.sale_id else object.name)</field>
<field name="binding_model_id" ref="stock.model_stock_picking"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 16. Bill of Lading (Portrait + Landscape) -->
<!-- ============================================================= -->
<record id="action_report_fp_bol_portrait" model="ir.actions.report">
<field name="name">Bill of Lading (Portrait)</field>
<field name="model">fusion.plating.delivery</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_bol_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_bol_portrait</field>
<field name="print_report_name">'BoL - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_logistics.model_fusion_plating_delivery"/>
<field name="binding_type">report</field>
</record>
<record id="action_report_fp_bol_landscape" model="ir.actions.report">
<field name="name">Bill of Lading (Landscape)</field>
<field name="model">fusion.plating.delivery</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_bol_landscape</field>
<field name="report_file">fusion_plating_reports.report_fp_bol_landscape</field>
<field name="print_report_name">'BoL - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_logistics.model_fusion_plating_delivery"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 17. Invoice (Portrait + Landscape) -->
<!-- ============================================================= -->
<record id="action_report_fp_invoice_portrait" model="ir.actions.report">
<field name="name">Invoice - Plating (Portrait)</field>
<field name="model">account.move</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_invoice_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_invoice_portrait</field>
<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="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">
<field name="name">Invoice - Plating (Landscape)</field>
<field name="model">account.move</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_invoice_landscape</field>
<field name="report_file">fusion_plating_reports.report_fp_invoice_landscape</field>
<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>
<!-- 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>
<!-- ============================================================= -->
<!-- 18. Payment Receipt (Portrait + Landscape) -->
<!-- ============================================================= -->
<record id="action_report_fp_receipt_portrait" model="ir.actions.report">
<field name="name">Payment Receipt (Portrait)</field>
<field name="model">account.payment</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_receipt_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_receipt_portrait</field>
<field name="print_report_name">'Receipt - %s' % (object.name or '')</field>
<field name="binding_model_id" ref="account.model_account_payment"/>
<field name="binding_type">report</field>
</record>
<record id="action_report_fp_receipt_landscape" model="ir.actions.report">
<field name="name">Payment Receipt (Landscape)</field>
<field name="model">account.payment</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_receipt_landscape</field>
<field name="report_file">fusion_plating_reports.report_fp_receipt_landscape</field>
<field name="print_report_name">'Receipt - %s' % (object.name or '')</field>
<field name="binding_model_id" ref="account.model_account_payment"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- ============================================================= -->
<!-- 19. Job Traveller - Sales Order bindings only. -->
<!-- The MO-bound bindings (mrp.production -> traveller) were -->
<!-- removed because fusion_plating_jobs ships the canonical -->
<!-- fp.job-bound traveller. The SO bindings stay; estimators -->
<!-- print the traveller from the SO before fp.jobs exist. -->
<!-- ============================================================= -->
<record id="action_report_fp_job_traveller_so_landscape" model="ir.actions.report">
<field name="name">Job Traveller (Landscape)</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_job_traveller_so_landscape</field>
<field name="report_file">fusion_plating_reports.report_fp_job_traveller_so_landscape</field>
<field name="print_report_name">'Traveller - %s' % object.name</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<record id="action_report_fp_job_traveller_so_portrait" model="ir.actions.report">
<field name="name">Job Traveller (Portrait)</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_job_traveller_so_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_job_traveller_so_portrait</field>
<field name="print_report_name">'Traveller - %s' % object.name</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@@ -1,96 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Audit Report
-->
<odoo>
<template id="report_audit">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Audit Report
<span t-field="doc.name"/>
</h2>
<!-- Header Info -->
<table class="bordered info-table">
<thead><tr>
<th>AUDIT #</th>
<th>TYPE</th>
<th>SCOPE</th>
<th>FACILITY</th>
<th>AUDIT DATE</th>
<th>STATUS</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.audit_type"/></td>
<td class="text-center"><span t-field="doc.scope"/></td>
<td class="text-center"><span t-field="doc.facility_id"/></td>
<td class="text-center"><span t-field="doc.audit_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.state"/></td>
</tr></tbody>
</table>
<!-- Auditors & Stats -->
<table class="bordered info-table">
<thead><tr>
<th>AUDITORS</th>
<th># FINDINGS</th>
<th>LINKED CAPAs</th>
</tr></thead>
<tbody><tr>
<td class="text-center">
<t t-foreach="doc.auditor_ids" t-as="aud">
<span t-out="aud.name"/>
<t t-if="not aud_last">, </t>
</t>
</td>
<td class="text-center"><span t-field="doc.findings_count"/></td>
<td class="text-center"><span t-field="doc.capa_count"/></td>
</tr></tbody>
</table>
<!-- Findings -->
<t t-if="doc.findings_html">
<table class="bordered">
<tr class="section-row"><td>FINDINGS</td></tr>
<tr><td><t t-out="doc.findings_html"/></td></tr>
</table>
</t>
<!-- Linked CAPAs Table -->
<t t-if="doc.capa_ids">
<table class="bordered">
<thead><tr>
<th>CAPA #</th>
<th>TYPE</th>
<th>STATUS</th>
<th>OWNER</th>
<th>DUE DATE</th>
</tr></thead>
<tbody>
<t t-foreach="doc.capa_ids" t-as="capa">
<tr>
<td><span t-field="capa.name"/></td>
<td class="text-center"><span t-field="capa.type"/></td>
<td class="text-center"><span t-field="capa.state"/></td>
<td class="text-center"><span t-field="capa.owner_id"/></td>
<td class="text-center"><span t-field="capa.due_date" t-options="{'widget': 'date'}"/></td>
</tr>
</t>
</tbody>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,155 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Shared CSS for all Fusion Plating reports (portrait + landscape).
Section-header band: #c1c1c1 (neutral grey) with #4e4e4e text
Document titles (h2/h4): #4e4e4e
Hardcoded - used to follow `res.company.primary_color` but the
client wanted a uniform neutral palette across every FP report
regardless of company branding. `fp_primary` is kept in scope for
any per-report template that still wants the company colour.
To keep section-header markup concise in individual report files,
a utility class `.fp-header-primary` is exposed - apply that class
to any `<th>` or `<td>` that should render as a section banner
(e.g. CARGO DESCRIPTION, PAYMENT DETAILS).
-->
<odoo>
<!-- ============================================================= -->
<!-- Portrait Styles -->
<!-- ============================================================= -->
<template id="fp_portrait_styles">
<t t-set="_fp_company" t-value="doc.company_id if doc and 'company_id' in doc._fields else (company if company else user.company_id)"/>
<t t-set="fp_primary" t-value="(_fp_company.primary_color if _fp_company else False) or '#1d1f1e'"/>
<style>
.fp-report { font-family: Arial, sans-serif; font-size: 10pt; color: #000; }
.fp-report table { width: 100%; border-collapse: collapse; border-spacing: 0; margin-bottom: 10px; }
/* Standard collapse + longhand borders + background-clip.
Tried border-collapse:separate with single-side-per-cell
(right+bottom on cell, top+left on table) to fix wkhtmltopdf's
slightly-lighter-verticals quirk - but the `separate` model
makes column widths drift between tables with different
column counts, so tables stacked on the page no longer
line up at the outer edges. Reverted. The collapse pattern
gives correct alignment; the verticals may render a hair
softer than horizontals on entech wkhtmltopdf but that's
the less-bad trade-off vs misaligned tables. */
.fp-report table.bordered { border: 0; border-collapse: collapse; border-spacing: 0; }
.fp-report table.bordered th,
.fp-report table.bordered td {
border-width: 1px;
border-style: solid;
border-color: #000;
background-clip: padding-box;
box-sizing: border-box;
}
.fp-report th { background-color: #c1c1c1; color: #1d1f1e; padding: 6px 8px; font-weight: bold; text-align: center; font-size: 9pt; background-clip: padding-box; }
.fp-report td { padding: 6px 8px; vertical-align: top; font-size: 10pt; background-clip: padding-box; }
.fp-report .text-center { text-align: center; }
.fp-report .text-end { text-align: right; }
.fp-report .text-start { text-align: left; }
.fp-report .adp-bg { background-color: #e3f2fd; }
.fp-report .client-bg { background-color: #fff3e0; }
.fp-report .section-row { background-color: #f0f0f0; font-weight: bold; }
.fp-report .note-row { font-style: italic; color: #555; font-size: 9pt; }
.fp-report h4 { color: #2e2e2e; margin: 0 0 15px 0; font-size: 20pt; }
.fp-report .totals-table { border: 0; border-collapse: collapse; border-spacing: 0; }
.fp-report .totals-table td {
border-width: 1px;
border-style: solid;
border-color: #000;
padding: 6px 8px;
background-clip: padding-box;
box-sizing: border-box;
}
.fp-report .info-header { background-color: #f5f5f5; color: #333; }
.fp-report .adp-header { background-color: #e3f2fd; color: #333; }
.fp-report .highlight-box { border: 2px solid #c1c1c1; background-color: #f5f5f5; padding: 10px; margin: 10px 0; }
.fp-report .fp-header-primary { background-color: #c1c1c1; color: #1d1f1e; }
.fp-report .paid-stamp { color: #28a745; font-size: 36pt; font-weight: bold; border: 4px solid #28a745; padding: 10px 20px; transform: rotate(-8deg); display: inline-block; }
.fp-report .status-ok { color: #2e7d32; font-weight: bold; }
.fp-report .status-warning { color: #f57f17; font-weight: bold; }
.fp-report .status-fail { color: #c62828; font-weight: bold; }
.fp-report .sig-line { border-bottom: 1px solid #000; height: 60px; margin-bottom: 4px; }
.fp-report .sig-table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
margin-top: 16px;
border: 0;
page-break-inside: avoid;
break-inside: avoid;
}
.fp-report .sig-table .sig-cell {
padding: 14px 12px 8px 12px;
vertical-align: top;
border-width: 1px;
border-style: solid;
border-color: #000;
background-clip: padding-box;
box-sizing: border-box;
page-break-inside: avoid;
break-inside: avoid;
}
.fp-report .small-muted { font-size: 8pt; color: #666; }
.fp-report .fp-cell-mid { vertical-align: middle !important; }
.fp-report .fp-keep-together { page-break-inside: avoid; break-inside: avoid; }
.fp-report .fp-keep-together .row, .fp-report .fp-keep-together .col-4 { page-break-inside: avoid; break-inside: avoid; }
.fp-report table tr { page-break-inside: avoid; break-inside: avoid; }
</style>
</template>
<!-- ============================================================= -->
<!-- Landscape Styles -->
<!-- ============================================================= -->
<template id="fp_landscape_styles">
<t t-set="_fp_company" t-value="doc.company_id if doc and 'company_id' in doc._fields else (company if company else user.company_id)"/>
<t t-set="fp_primary" t-value="(_fp_company.primary_color if _fp_company else False) or '#1d1f1e'"/>
<style>
.fp-landscape { font-family: Arial, sans-serif; font-size: 10pt; color: #000; }
.fp-landscape table { width: 100%; border-collapse: collapse; border-spacing: 0; margin-bottom: 6px; }
/* Standard collapse + longhand + background-clip - see comment in fp_portrait_styles. */
.fp-landscape table.bordered { border: 0; border-collapse: collapse; border-spacing: 0; }
.fp-landscape table.bordered th,
.fp-landscape table.bordered td {
border-width: 1px;
border-style: solid;
border-color: #000;
background-clip: padding-box;
box-sizing: border-box;
}
.fp-landscape th { background-color: #c1c1c1; color: #1d1f1e; padding: 4px 8px; font-weight: bold; font-size: 9pt; background-clip: padding-box; }
.fp-landscape td { padding: 4px 8px; vertical-align: top; font-size: 9.5pt; }
.fp-landscape .text-center { text-align: center; }
.fp-landscape .text-end { text-align: right; }
.fp-landscape .text-start { text-align: left; }
.fp-landscape .adp-bg { background-color: #e3f2fd; }
.fp-landscape .client-bg { background-color: #fff3e0; }
.fp-landscape .section-row { background-color: #f0f0f0; font-weight: bold; }
.fp-landscape .note-row { font-style: italic; color: #555; }
.fp-landscape h2 { color: #2e2e2e; margin: 4px 0; font-size: 22pt; }
.fp-landscape .info-table td { padding: 8px 12px; font-size: 11pt; }
.fp-landscape .info-table th { background-color: #f5f5f5; color: #333; font-size: 10pt; padding: 6px 12px; }
.fp-landscape .totals-table { border: 1px solid #000; }
.fp-landscape .totals-table td { border: 1px solid #000; padding: 8px 12px; font-size: 11pt; }
.fp-landscape .highlight-box { border: 2px solid #c1c1c1; background-color: #f5f5f5; padding: 6px 10px; margin: 6px 0; font-size: 9pt; }
.fp-landscape .fp-header-primary { background-color: #c1c1c1; color: #1d1f1e; }
.fp-landscape .paid-stamp { color: #28a745; font-size: 42pt; font-weight: bold; border: 4px solid #28a745; padding: 10px 20px; transform: rotate(-8deg); display: inline-block; }
.fp-landscape .status-ok { color: #2e7d32; font-weight: bold; }
.fp-landscape .status-warning { color: #f57f17; font-weight: bold; }
.fp-landscape .status-fail { color: #c62828; font-weight: bold; }
.fp-landscape .sig-line { border-bottom: 1px solid #000; height: 45px; margin-bottom: 3px; }
.fp-landscape .sig-table { width: 100%; border-collapse: collapse; margin-top: 6px; border: 1px solid #000; page-break-inside: avoid; break-inside: avoid; }
.fp-landscape .sig-table .sig-cell { padding: 10px 10px 6px 10px; vertical-align: top; border: 1px solid #000; page-break-inside: avoid; break-inside: avoid; }
.fp-landscape .small-muted { font-size: 9pt; color: #666; }
.fp-landscape .fp-cell-mid { vertical-align: middle !important; }
.fp-landscape .fp-keep-together { page-break-inside: avoid; break-inside: avoid; }
.fp-landscape .fp-keep-together .row, .fp-landscape .fp-keep-together .col-4 { page-break-inside: avoid; break-inside: avoid; }
.fp-landscape table tr { page-break-inside: avoid; break-inside: avoid; }
</style>
</template>
</odoo>

View File

@@ -1,93 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Bath Chemistry Log Report
-->
<odoo>
<template id="report_bath_chemistry_log">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Bath Chemistry Log
<span t-field="doc.name"/>
</h2>
<!-- Header Info -->
<table class="bordered info-table">
<thead><tr>
<th>LOG REF</th>
<th>BATH</th>
<th>TANK</th>
<th>PROCESS TYPE</th>
<th>LOGGED AT</th>
<th>OPERATOR</th>
<th>SHIFT</th>
<th>STATUS</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.bath_id"/></td>
<td class="text-center"><span t-field="doc.tank_id"/></td>
<td class="text-center"><span t-field="doc.process_type_id"/></td>
<td class="text-center"><span t-field="doc.log_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.operator_id"/></td>
<td class="text-center"><span t-field="doc.shift"/></td>
<td class="text-center">
<span t-if="doc.status == 'ok'" class="status-ok">OK</span>
<span t-if="doc.status == 'warning'" class="status-warning">Warning</span>
<span t-if="doc.status == 'out_of_spec'" class="status-fail">Out of Spec</span>
</td>
</tr></tbody>
</table>
<!-- Readings Table -->
<t t-if="doc.line_ids">
<table class="bordered">
<thead><tr>
<th>PARAMETER</th>
<th>VALUE</th>
<th>TARGET MIN</th>
<th>TARGET MAX</th>
<th>UoM</th>
<th>STATUS</th>
<th>NOTES</th>
</tr></thead>
<tbody>
<t t-foreach="doc.line_ids" t-as="line">
<tr>
<td><span t-field="line.parameter_id"/></td>
<td class="text-center"><span t-field="line.value"/></td>
<td class="text-center"><span t-field="line.target_min"/></td>
<td class="text-center"><span t-field="line.target_max"/></td>
<td class="text-center"><span t-field="line.uom"/></td>
<td class="text-center">
<span t-if="line.status == 'ok'" class="status-ok">OK</span>
<span t-if="line.status == 'warning'" class="status-warning">Warning</span>
<span t-if="line.status == 'out_of_spec'" class="status-fail">Out of Spec</span>
</td>
<td><span t-field="line.notes"/></td>
</tr>
</t>
</tbody>
</table>
</t>
<!-- Notes -->
<t t-if="doc.notes">
<table class="bordered">
<tr class="section-row"><td>NOTES</td></tr>
<tr><td><span t-field="doc.notes"/></td></tr>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,90 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Calibration Certificate Report
-->
<odoo>
<template id="report_calibration_cert">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Calibration Certificate
<span t-field="doc.code"/>
</h2>
<!-- Equipment Info -->
<table class="bordered info-table">
<thead><tr>
<th>EQUIPMENT</th>
<th>ASSET CODE</th>
<th>TYPE</th>
<th>NIST TRACEABLE</th>
<th>STATUS</th>
<th>FACILITY</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.code"/></td>
<td class="text-center"><span t-field="doc.equipment_type"/></td>
<td class="text-center">
<t t-if="doc.nist_traceable">Yes</t>
<t t-else="">No</t>
</td>
<td class="text-center"><span t-field="doc.state"/></td>
<td class="text-center"><span t-field="doc.facility_id"/></td>
</tr></tbody>
</table>
<!-- Calibration Dates -->
<table class="bordered info-table">
<thead><tr>
<th>LAST CALIBRATION</th>
<th>NEXT CALIBRATION</th>
<th>INTERVAL (DAYS)</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.last_cal_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.next_cal_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.calibration_interval_days"/></td>
</tr></tbody>
</table>
<!-- Event History -->
<t t-if="doc.event_ids">
<table class="bordered">
<thead><tr>
<th>CALIBRATION DATE</th>
<th>PERFORMED BY</th>
<th>EXTERNAL LAB</th>
<th>RESULT</th>
<th>CERTIFICATE #</th>
<th>AS-FOUND</th>
<th>AS-LEFT</th>
</tr></thead>
<tbody>
<t t-foreach="doc.event_ids" t-as="evt">
<tr>
<td class="text-center"><span t-field="evt.cal_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="evt.performed_by_id"/></td>
<td class="text-center"><span t-field="evt.performed_by_external"/></td>
<td class="text-center"><span t-field="evt.result"/></td>
<td class="text-center"><span t-field="evt.certificate_ref"/></td>
<td><span t-field="evt.as_found_notes"/></td>
<td><span t-field="evt.as_left_notes"/></td>
</tr>
</t>
</tbody>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Corrective / Preventive Action Report
-->
<odoo>
<template id="report_capa">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
CAPA Report
<span t-field="doc.name"/>
</h2>
<!-- Header Info -->
<table class="bordered info-table">
<thead><tr>
<th>CAPA #</th>
<th>TYPE</th>
<th>STATUS</th>
<th>SOURCE NCR</th>
<th>OWNER</th>
<th>DUE DATE</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.type"/></td>
<td class="text-center"><span t-field="doc.state"/></td>
<td class="text-center"><span t-field="doc.ncr_id"/></td>
<td class="text-center"><span t-field="doc.owner_id"/></td>
<td class="text-center"><span t-field="doc.due_date" t-options="{'widget': 'date'}"/></td>
</tr></tbody>
</table>
<!-- Facility -->
<table class="bordered info-table">
<thead><tr>
<th>FACILITY</th>
<th>VERIFIED BY</th>
<th>VERIFICATION DATE</th>
<th>EFFECTIVE</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.facility_id"/></td>
<td class="text-center"><span t-field="doc.verification_by_id"/></td>
<td class="text-center"><span t-field="doc.verification_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center">
<t t-if="doc.is_effective">Yes</t>
<t t-else="">No</t>
</td>
</tr></tbody>
</table>
<!-- Description -->
<table class="bordered">
<tr class="section-row"><td>DESCRIPTION</td></tr>
<tr><td><t t-out="doc.description"/></td></tr>
</table>
<!-- Root Cause Analysis -->
<t t-if="doc.root_cause_analysis">
<table class="bordered">
<tr class="section-row"><td>ROOT CAUSE ANALYSIS</td></tr>
<tr><td><t t-out="doc.root_cause_analysis"/></td></tr>
</table>
</t>
<!-- Action Plan -->
<t t-if="doc.action_plan">
<table class="bordered">
<tr class="section-row"><td>ACTION PLAN</td></tr>
<tr><td><t t-out="doc.action_plan"/></td></tr>
</table>
</t>
<!-- Effectiveness Notes -->
<t t-if="doc.effectiveness_notes">
<table class="bordered">
<tr class="section-row"><td>EFFECTIVENESS NOTES</td></tr>
<tr><td><t t-out="doc.effectiveness_notes"/></td></tr>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,688 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Fusion Plating - Certificate of Conformance
Design note:
Single bilingual CoC - English and the French translation render
together. The body wraps in fp_external_layout_clean + a custom
coc_header (logo + Nadcap + title/barcode, mirroring the Sale Order)
instead of web.external_layout; coc_body_router picks the classic vs
chronological body.
Variants:
- report_coc_en The single bilingual cert, bound to the
"Certificate of Conformance" print action. The
former report_coc_fr was removed 2026-05-28.
- report_coc, report_coc_portrait
Legacy portal-job-bound variants (unchanged)
-->
<odoo>
<!-- ================================================================== -->
<!-- Shared CoC header - logo + Nadcap + title/barcode. Mirrors the -->
<!-- Sale Order header (report_fp_sale.xml fp-sale-header-row): company -->
<!-- logo + address LEFT, Nadcap accreditation logo CENTRE, document -->
<!-- title + Code128 barcode RIGHT. Rendered once by the EN/FR wrappers -->
<!-- above the body, in place of web.external_layout's company band. -->
<!-- ================================================================== -->
<template id="coc_header">
<t t-set="is_fr" t-value="LANG == 'fr'"/>
<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="coc_barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', doc.name, 600, 100) if doc.name else False"/>
<style>
/* Float-based 3-column header (avoid HTML tables - the global
bordered-table cascade bleeds borders onto nested tables on
entech wkhtmltopdf; see CLAUDE.md). No bottom border -
matches the Sale Order header; spacing alone separates it
from the body. */
.fp-coc-header-row { overflow: hidden; margin-bottom: 14px;
padding-bottom: 6px; }
.fp-coc-header-left { float: left; width: 38%; }
.fp-coc-header-mid { float: left; width: 24%; text-align: center; padding-top: 4px; }
.fp-coc-header-right { float: right; width: 38%; text-align: center; }
.fp-coc-logo { max-height: 50px; max-width: 280px; display: block; margin-bottom: 4px; }
.fp-coc-company-addr { font-size: 8.5pt; color: #222; line-height: 1.35; }
.fp-coc-company-addr div { margin: 0; }
.fp-coc-company-addr a { color: #2e6da4; text-decoration: none; }
.fp-coc-nadcap-logo { max-height: 45px; max-width: 115px; display: inline-block; }
.fp-coc-title { font-size: 18pt; font-weight: bold; color: #2e2e2e;
line-height: 1.1; display: block; }
.fp-coc-title-fr { font-size: 12pt; font-style: italic; color: #555;
line-height: 1.1; display: block; margin-top: 1px; }
/* Barcode: inline-block wrap so the cert-number label centres
under the bars. Explicit no-border (wkhtmltopdf frames
inline-data imgs on entech). */
.fp-coc-bc-wrap { display: inline-block; text-align: center; margin-top: 4px; }
.fp-coc-bc-wrap img { height: 48px; max-width: 240px; border: 0 !important; padding: 0; display: block; }
.fp-coc-bc-label { font-size: 14pt; font-weight: bold; color: #000;
margin-top: 6px; letter-spacing: 1.2px; }
</style>
<div class="fp-coc-header-row">
<div class="fp-coc-header-left">
<t t-if="logo_uri">
<img t-att-src="logo_uri" class="fp-coc-logo" alt="Logo"/>
</t>
<div class="fp-coc-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">&#160;&#160;&#160;</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>
<!-- Centre: NADCAP accreditation logo, base64-inlined from
company settings (wkhtmltopdf can't fetch over HTTP on
entech). Same source as the Sale Order header. -->
<div class="fp-coc-header-mid">
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
<img class="fp-coc-nadcap-logo"
t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
alt="Nadcap Accredited"/>
</t>
</div>
<div class="fp-coc-header-right">
<span class="fp-coc-title">Certificate of Conformance</span>
<span class="fp-coc-title-fr">Certificat de Conformité</span>
<t t-if="coc_barcode_uri">
<div class="fp-coc-bc-wrap">
<img t-att-src="coc_barcode_uri" alt="Cert Barcode"/>
<div class="fp-coc-bc-label"><span t-field="doc.name"/></div>
</div>
</t>
</div>
</div>
</template>
<!-- ================================================================== -->
<!-- Shared CoC body - rendered inside fp_external_layout_clean -->
<!-- ================================================================== -->
<template id="coc_body">
<t t-set="is_fr" t-value="LANG == 'fr'"/>
<!-- Signer + signature resolution (2026-05-17): unified with the
WO Detail certifier pattern. Signer = cert's certified_by
user; falls back to the company owner. Signature image is
that user's Plating Signature (x_fc_signature_image from
Preferences → My Profile). The previous HR Employee
signature lookup was retired in favour of this single
source so all FP reports pull from the same field. -->
<t t-set="signer_user" t-value="doc.certified_by_id or company.x_fc_owner_user_id or False"/>
<t t-set="signature_img" t-value="(signer_user and 'x_fc_signature_image' in signer_user._fields and signer_user.x_fc_signature_image) or False"/>
<t t-set="signer_name" t-value="(signer_user and signer_user.name) or ''"/>
<style>
/* No web.external_layout header band anymore - the CoC
renders its own header (coc_header) above the body, the
same way the Sale Order does. So no top padding is
needed to clear an Odoo header zone. */
.fp-coc { font-family: Arial, sans-serif; font-size: 9pt; color: #000;
padding-top: 0; }
.fp-coc table { width: 100%; border-collapse: collapse; margin-bottom: 6px; }
.fp-coc table.bordered,
.fp-coc table.bordered th,
.fp-coc table.bordered td { border: 1px solid #000; }
.fp-coc th { background-color: #ededed; font-weight: bold;
padding: 5px 8px; font-size: 8.5pt; text-align: center; }
.fp-coc td { padding: 5px 8px; vertical-align: top; font-size: 8.5pt; }
.fp-coc .text-center { text-align: center; }
.fp-coc .text-end { text-align: right; }
/* Bilingual stacked column titles - English (bold) over the
French translation (italic grey), same pattern as the SO
report's narrow-cell headers. Added 2026-05-28. */
.fp-coc .fp-bl-en-stk { display: block; font-weight: bold; }
.fp-coc .fp-bl-fr-stk { display: block; font-weight: normal;
font-style: italic; color: #555;
font-size: 80%; margin-top: 1px; }
/* Inline bilingual (EN bold / FR italic grey) for field labels
+ section headings. */
.fp-coc .fp-bl-en { font-weight: bold; }
.fp-coc .fp-bl-sep { color: #999; margin: 0 3px; font-weight: normal; }
.fp-coc .fp-bl-fr { font-weight: normal; font-style: italic; color: #555; }
.fp-coc .cert-statement-box { padding: 0; font-size: 8.5pt; }
.fp-coc .cert-statement-box h4 { margin: 0 0 6px 0; font-size: 9.5pt; font-weight: bold; }
.fp-coc .signature-img { max-height: 2.2cm; max-width: 7cm; }
.fp-coc .small-label { font-size: 7.5pt; opacity: 0.7; }
.fp-coc .brand-note { font-size: 7.5pt; color: #888; text-align: center;
margin-top: 10px; font-style: italic; }
/* Thickness block - single outer border, internal-only
cell dividers so the title / metadata / image+readings
look like one connected section. No nested .bordered
class on inner tables; each cell explicitly draws the
internal divider it needs. */
.fp-coc .fp-thickness-block { border: 1px solid #000; margin-top: 14px; }
.fp-coc .fp-thickness-block table { width: 100%; border-collapse: collapse;
margin: 0; }
.fp-coc .fp-thickness-block td,
.fp-coc .fp-thickness-block th { padding: 5px 8px; vertical-align: top;
font-size: 8.5pt; }
.fp-coc .fp-thickness-block .ftk-title {
background-color: #ededed; text-align: center; font-weight: bold;
font-size: 11pt; padding: 6px; border-bottom: 1px solid #000; }
.fp-coc .fp-thickness-block .ftk-label { background-color: #f7f7f7;
font-weight: bold; }
.fp-coc .fp-thickness-block .ftk-row-divider { border-bottom: 1px solid #000; }
.fp-coc .fp-thickness-block .ftk-cell-divider { border-right: 1px solid #000; }
.fp-coc .fp-thickness-block .ftk-img-cell {
text-align: center; vertical-align: middle; padding: 6px;
border-right: 1px solid #000; }
.fp-coc .fp-thickness-block .ftk-img-cell img { max-width: 100%; max-height: 10cm; }
.fp-coc .fp-thickness-block .ftk-readings-cell { padding: 0; vertical-align: top; }
.fp-coc .fp-thickness-block .ftk-readings th {
background-color: #ededed; text-align: center; font-weight: bold;
border-bottom: 1px solid #000; }
.fp-coc .fp-thickness-block .ftk-readings td { text-align: center; }
.fp-coc .fp-thickness-block .ftk-readings .ftk-stat-mean {
background-color: #ededed; font-weight: bold; }
.fp-coc .fp-thickness-block .ftk-readings .ftk-stat { background-color: #f7f7f7; }
</style>
<div class="fp-coc">
<!-- Title + accreditation logos moved to the shared
coc_header (rendered above this body by the EN/FR
wrapper). The 3-logo accreditation strip was dropped
per client request 2026-05-28; the Nadcap logo now
lives in the header, mirroring the Sale Order. -->
<!-- Customer block - 2 columns: address | contact. The
customer-logo column was dropped 2026-05-28 (usually
empty; the company logo is now in the header). -->
<table class="bordered">
<tr>
<td style="width: 50%; vertical-align: top;">
<div>
<span class="fp-bl-en">Customer Name</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Nom du client</span>:
<t t-esc="doc.partner_id.name or ''"/>
</div>
<div style="margin-top: 4px;">
<span class="fp-bl-en">Customer Address</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Adresse du client</span>:
<br/>
<t t-if="doc.partner_id.street"><t t-esc="doc.partner_id.street"/><br/></t>
<t t-if="doc.partner_id.street2"><t t-esc="doc.partner_id.street2"/><br/></t>
<span t-if="doc.partner_id.city"><t t-esc="doc.partner_id.city"/></span>
<span t-if="doc.partner_id.state_id">, <t t-esc="doc.partner_id.state_id.name"/></span>
<span t-if="doc.partner_id.zip"> <t t-esc="doc.partner_id.zip"/></span>
</div>
</td>
<td style="width: 50%; vertical-align: top;">
<t t-set="contact" t-value="doc.contact_partner_ids[:1] or doc.partner_id"/>
<div>
<span class="fp-bl-en">Contact Name</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Nom du contact</span>:
<t t-esc="contact.name or ''"/>
</div>
<div>
<span class="fp-bl-en">Customer Email</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Courriel</span>:
<t t-esc="contact.email or '-'"/>
</div>
<div>
<span class="fp-bl-en">Customer Phone</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Téléphone</span>:
<t t-esc="contact.phone or '-'"/>
</div>
</td>
</tr>
</table>
<!-- Certification info table -->
<table class="bordered" style="margin-top: 8px;">
<thead>
<tr>
<th style="width: 33%;">
<span class="fp-bl-en">Date of Certification</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Date du certificat</span>
</th>
<th style="width: 34%;">
<span class="fp-bl-en">Generated By</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Créé par</span>
</th>
<th style="width: 33%;">
<span class="fp-bl-en">Work Order #</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Bon de travail</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">
<span t-field="doc.issue_date" t-options="{'widget': 'date'}"/>
</td>
<td class="text-center">
<t t-esc="(doc.issued_by_id.name if doc.issued_by_id else '') or ''"/>
</td>
<td class="text-center">
<t t-esc="doc.entech_wo_number or (doc.production_id.name if doc.production_id else '') or '-'"/>
</td>
</tr>
</tbody>
</table>
<!-- Line-item table - the column headers already speak
for themselves (Shipped / NC Qty / etc.), so the
hovering "Quantities" caption above was just visual
noise. Removed 2026-05-21. Header row + body row
stay together (page-break-inside on tr per CLAUDE.md). -->
<table class="bordered" style="margin-top: 8px;
page-break-inside: avoid;">
<thead>
<tr>
<th style="width: 20%; line-height: 1.25;">
<div><span class="fp-bl-en">Part Number</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">No. de pièce</span></div>
<div><span class="fp-bl-en">Description</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Description</span></div>
<div><span class="fp-bl-en">Serial Number</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Numéro de série</span></div>
</th>
<th style="width: 32%;">
<span class="fp-bl-en">Process</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Procédé</span>
</th>
<th style="width: 16%;">
<span class="fp-bl-en">Customer PO</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Bon de commande</span>
</th>
<th style="width: 10%;">
<span class="fp-bl-en">Shipped</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Expédié</span>
</th>
<th style="width: 10%;">
<span class="fp-bl-en">NC Qty</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Qté NC</span>
</th>
<th style="width: 12%;">
<span class="fp-bl-en">Customer Job No.</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Bon de travail client</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center" style="line-height: 1.3;">
<t t-set="pid" t-value="doc._fp_resolve_part_identity()"/>
<div><t t-esc="pid[0] or '-'"/></div>
<div><t t-esc="pid[1] or '-'"/></div>
<div><t t-esc="pid[2] or '-'"/></div>
</td>
<td>
<!-- Customer-facing description is the cert's
spec / certificate info (client request
2026-05-28). Falls back to the recipe-
derived process_description. spec_reference,
now optional, still prints below when set. -->
<t t-set="cust_desc" t-value="doc._fp_resolve_customer_facing_description()"/>
<t t-esc="cust_desc or doc.process_description or ''"/>
<t t-if="doc.spec_reference">
<br/><em t-esc="doc.spec_reference"/>
</t>
</td>
<td class="text-center"><t t-esc="doc.po_number or '-'"/></td>
<td class="text-center"><t t-esc="doc.quantity_shipped or 0"/></td>
<td class="text-center"><t t-esc="doc.nc_quantity or 0"/></td>
<td class="text-center"><t t-esc="doc.customer_job_no or '-'"/></td>
</tr>
</tbody>
</table>
<!-- Thickness readings (Fischerscope XRF) - full report
block mirroring the original XDAL 600 export so the
customer/auditor sees the same context the gauge
produced: equipment, operator, calibration, product,
application, measuring time, all readings, plus
derived stats (mean, std dev, CoV, range, n). When
the source upload was an RTF/.docx, this replaces
the page-2 PDF merge. -->
<t t-if="doc.thickness_reading_ids">
<t t-set="readings" t-value="doc.thickness_reading_ids.sorted('reading_number')"/>
<t t-set="calib" t-value="(doc.x_fc_thickness_calibration if 'x_fc_thickness_calibration' in doc._fields else '') or (readings and readings[0].calibration_std_ref or '')"/>
<t t-set="n" t-value="len(readings)"/>
<t t-set="nip_vals" t-value="[r.nip_mils for r in readings if r.nip_mils]"/>
<t t-set="ni_vals" t-value="[r.ni_percent for r in readings if r.ni_percent]"/>
<t t-set="p_vals" t-value="[r.p_percent for r in readings if r.p_percent]"/>
<!-- Stats computed in the template so the wizard
parser doesn't have to handle two number formats
(XDAL exports `0.5857` and `92.727` etc; we
recompute from the source readings to guarantee
the printed report agrees with the source data). -->
<t t-set="nip_mean" t-value="(sum(nip_vals) / len(nip_vals)) if nip_vals else 0"/>
<t t-set="ni_mean" t-value="(sum(ni_vals) / len(ni_vals)) if ni_vals else 0"/>
<t t-set="p_mean" t-value="(sum(p_vals) / len(p_vals)) if p_vals else 0"/>
<t t-set="nip_std" t-value="((sum((v - nip_mean) ** 2 for v in nip_vals) / (len(nip_vals) - 1)) ** 0.5) if len(nip_vals) > 1 else 0"/>
<t t-set="ni_std" t-value="((sum((v - ni_mean) ** 2 for v in ni_vals) / (len(ni_vals) - 1)) ** 0.5) if len(ni_vals) > 1 else 0"/>
<t t-set="p_std" t-value="((sum((v - p_mean) ** 2 for v in p_vals) / (len(p_vals) - 1)) ** 0.5) if len(p_vals) > 1 else 0"/>
<t t-set="nip_cov" t-value="(nip_std / nip_mean * 100) if nip_mean else 0"/>
<t t-set="ni_cov" t-value="(ni_std / ni_mean * 100) if ni_mean else 0"/>
<t t-set="p_cov" t-value="(p_std / p_mean * 100) if p_mean else 0"/>
<t t-set="nip_range" t-value="(max(nip_vals) - min(nip_vals)) if nip_vals else 0"/>
<t t-set="ni_range" t-value="(max(ni_vals) - min(ni_vals)) if ni_vals else 0"/>
<t t-set="p_range" t-value="(max(p_vals) - min(p_vals)) if p_vals else 0"/>
<!-- Whole block stays together when it fits; wraps
to a fresh page if it doesn't. Prevents the
wkhtmltopdf company header from overlapping the
readings table mid-row on page 2. -->
<div class="fp-thickness-block">
<!-- Section header - full-width bar, drawn by the
div's bottom-border, no internal table needed. -->
<div class="ftk-title">
<span class="fp-bl-en">Fischerscope XRF Thickness Report</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Rapport d'épaisseur Fischerscope XRF</span>
</div>
<!-- Equipment metadata - 4-column key/value grid.
Per-cell border-right + per-row border-bottom
draw the internal grid; the outer perimeter
comes from .fp-thickness-block's border. Last
row + last column omit their dividers so we
don't double up against the parent border. -->
<table>
<tr class="ftk-row-divider">
<td class="ftk-label ftk-cell-divider" style="width: 18%;">
<span class="fp-bl-en">Equipment</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Équipement</span>
</td>
<td class="ftk-cell-divider" style="width: 32%;">
<t t-esc="doc.x_fc_thickness_equipment or '-'"/>
</td>
<td class="ftk-label ftk-cell-divider" style="width: 18%;">
<span class="fp-bl-en">Calibration Std.</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Étalon</span>
</td>
<td style="width: 32%;"><t t-esc="calib or '-'"/></td>
</tr>
<tr class="ftk-row-divider">
<td class="ftk-label ftk-cell-divider">
<span class="fp-bl-en">Product</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Produit</span>
</td>
<td class="ftk-cell-divider"><t t-esc="doc.x_fc_thickness_product or '-'"/></td>
<td class="ftk-label ftk-cell-divider">
<span class="fp-bl-en">Operator</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Opérateur</span>
</td>
<td><t t-esc="doc.x_fc_thickness_operator or '-'"/></td>
</tr>
<tr class="ftk-row-divider">
<td class="ftk-label ftk-cell-divider">
<span class="fp-bl-en">Application</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Application</span>
</td>
<td class="ftk-cell-divider"><t t-esc="doc.x_fc_thickness_application or '-'"/></td>
<td class="ftk-label ftk-cell-divider">
<span class="fp-bl-en">Measured</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Mesuré le</span>
</td>
<td>
<t t-if="doc.x_fc_thickness_datetime"
t-esc="doc.x_fc_thickness_datetime.strftime('%Y-%m-%d %H:%M')"/>
<t t-if="not doc.x_fc_thickness_datetime">-</t>
</td>
</tr>
<tr class="ftk-row-divider">
<td class="ftk-label ftk-cell-divider">
<span class="fp-bl-en">Directory</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Répertoire</span>
</td>
<td class="ftk-cell-divider"><t t-esc="doc.x_fc_thickness_directory or '-'"/></td>
<td class="ftk-label ftk-cell-divider">
<span class="fp-bl-en">Measuring Time</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Durée de mesure</span>
</td>
<td>
<t t-if="doc.x_fc_thickness_measuring_time_sec"
t-esc="'%d sec' % doc.x_fc_thickness_measuring_time_sec"/>
<t t-if="not doc.x_fc_thickness_measuring_time_sec">-</t>
</td>
</tr>
</table>
<!-- Image (left) + readings (right). The image
cell's border-right is the only divider; the
readings inner table is borderless on its
perimeter (the parent cell's edges + the
block's outer border do all the bounding).
Inner-cell dividers are drawn per th/td. -->
<table>
<tr>
<td t-if="doc.x_fc_thickness_image_id"
class="ftk-img-cell" style="width: 45%;">
<img t-att-src="'/web/image/%s' % doc.x_fc_thickness_image_id.id"/>
</td>
<td class="ftk-readings-cell"
t-att-style="'width: 55%;' if doc.x_fc_thickness_image_id else 'width: 100%;'">
<table class="ftk-readings">
<thead>
<tr>
<th class="ftk-cell-divider" style="width: 28%;">#</th>
<th class="ftk-cell-divider">NiP (mils)</th>
<th class="ftk-cell-divider">Ni %</th>
<th>P %</th>
</tr>
</thead>
<tbody>
<tr t-foreach="readings" t-as="r"
class="ftk-row-divider"
style="page-break-inside: avoid;">
<td class="ftk-cell-divider"><t t-esc="r.reading_number or r_index + 1"/></td>
<td class="ftk-cell-divider"><t t-esc="'%.4f' % (r.nip_mils or 0)"/></td>
<td class="ftk-cell-divider"><t t-esc="'%.3f' % (r.ni_percent or 0)"/></td>
<td><t t-esc="'%.3f' % (r.p_percent or 0)"/></td>
</tr>
<!-- Stats: Mean / Std Dev / CoV / Range / N -->
<tr t-if="nip_vals"
class="ftk-stat-mean ftk-row-divider"
style="page-break-inside: avoid;">
<td class="ftk-cell-divider">
<span class="fp-bl-en">Mean</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Moyenne</span>
</td>
<td class="ftk-cell-divider"><t t-esc="'%.4f' % nip_mean"/></td>
<td class="ftk-cell-divider"><t t-esc="'%.3f' % ni_mean"/></td>
<td><t t-esc="'%.3f' % p_mean"/></td>
</tr>
<tr t-if="nip_vals and n > 1"
class="ftk-stat ftk-row-divider"
style="page-break-inside: avoid;">
<td class="ftk-cell-divider">
<span class="fp-bl-en">Std Dev</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Écart-type</span>
</td>
<td class="ftk-cell-divider"><t t-esc="'%.4f' % nip_std"/></td>
<td class="ftk-cell-divider"><t t-esc="'%.3f' % ni_std"/></td>
<td><t t-esc="'%.3f' % p_std"/></td>
</tr>
<tr t-if="nip_vals and n > 1"
class="ftk-stat ftk-row-divider"
style="page-break-inside: avoid;">
<td class="ftk-cell-divider">CoV (%)</td>
<td class="ftk-cell-divider"><t t-esc="'%.2f' % nip_cov"/></td>
<td class="ftk-cell-divider"><t t-esc="'%.2f' % ni_cov"/></td>
<td><t t-esc="'%.2f' % p_cov"/></td>
</tr>
<tr t-if="nip_vals and n > 1"
class="ftk-stat ftk-row-divider"
style="page-break-inside: avoid;">
<td class="ftk-cell-divider">
<span class="fp-bl-en">Range</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Étendue</span>
</td>
<td class="ftk-cell-divider"><t t-esc="'%.4f' % nip_range"/></td>
<td class="ftk-cell-divider"><t t-esc="'%.3f' % ni_range"/></td>
<td><t t-esc="'%.3f' % p_range"/></td>
</tr>
<tr t-if="nip_vals" class="ftk-stat"
style="page-break-inside: avoid;">
<td class="ftk-cell-divider">N</td>
<td class="ftk-cell-divider"><t t-esc="n"/></td>
<td class="ftk-cell-divider"><t t-esc="n"/></td>
<td><t t-esc="n"/></td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<!-- Source-file footnote - italic + opacity:0.7
(from .small-label) renders jagged/washed-out
on entech wkhtmltopdf. Solid #555 grey at
normal weight prints cleanly. -->
<div t-if="doc.x_fc_thickness_source_filename"
style="margin-top: 4px; font-size: 8pt; color: #555;">
<span class="fp-bl-en">Source file</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Fichier source</span>:
<t t-esc="' ' + (doc.x_fc_thickness_source_filename or '')"/>
<t t-if="doc.x_fc_local_thickness_evidence_id"> (attached to cert as evidence)</t>
</div>
</div>
</t>
<!-- Signature + certification statement - never split
across pages (page-break-inside on the row works on
entech wkhtmltopdf; see CLAUDE.md). Bordered table draws
the outer box + the column divider between Certified By
and the statement; the statement itself has no separate
inner border (2026-05-28). -->
<table class="bordered" style="margin-top: 18px;">
<tr style="page-break-inside: avoid;">
<td style="width: 50%; vertical-align: top;">
<div>
<span class="fp-bl-en">Certified By</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Certifié par</span>:
</div>
<div style="min-height: 2.5cm; margin-top: 6px;">
<img t-if="signature_img"
class="signature-img"
t-att-src="'data:image/png;base64,%s' % signature_img.decode()"
alt=""/>
</div>
<div style="margin-top: 4px; border-top: 1px solid #000; padding-top: 4px;">
<span class="fp-bl-en">Name</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Nom</span>:
<t t-esc="signer_name or ''"/>
</div>
</td>
<td style="width: 50%; vertical-align: top; padding-left: 14px;">
<div class="cert-statement-box">
<h4><span class="fp-bl-en">Certification Statement</span><span class="fp-bl-sep">/</span><span class="fp-bl-fr">Énoncé de conformité</span></h4>
<p style="margin: 0;">
This is to certify that the items listed herein have
been processed, inspected and tested in accordance
with your Purchase Order, drawings and specification
requirements. All chemistry used in this order is
Made in Canada. There is no Mercury used in the
processing of this order.
</p>
<p style="margin: 6px 0 0 0; font-style: italic; color: #444;">
Ceci est pour certifier que les articles inscrits ont
été procédés, inspectés et mis à l'essai selon votre
bon de commande, dessins et spécifications. Tous les
produits chimiques utilisés dans cette commande sont
fabriqués au Canada. Il n'y a pas de mercure dans les
procédés de fabrication de cette commande.
</p>
</div>
</td>
</tr>
</table>
<!-- Brand note (small, above Odoo's page footer) -->
<div class="brand-note">
<t t-if="not is_fr">Fusion Plating by Nexa Systems</t>
<t t-if="is_fr">Fusion Plating par Nexa Systems</t>
</div>
</div>
</template>
<!-- ================================================================== -->
<!-- English CoC - wrapped in web.external_layout -->
<!-- ================================================================== -->
<template id="report_coc_en">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-set="company" t-value="(doc.sale_order_id.company_id if doc.sale_order_id else False) or (doc.production_id.company_id if doc.production_id else False) or env.company"/>
<!-- Custom SO-style header instead of web.external_layout's
company band. fp_external_layout_clean provides the
.article wrapper Odoo needs for correct UTF-8 dispatch
plus a minimal page-number footer, with NO auto header
div - coc_header renders the visible header instead. -->
<t t-call="fusion_plating_reports.fp_external_layout_clean">
<t t-set="LANG" t-value="'en'"/>
<t t-call="fusion_plating_reports.coc_header"/>
<div class="page">
<!-- Sub 12c - router picks chronological vs classic body -->
<t t-call="fusion_plating_reports.coc_body_router"/>
</div>
</t>
</t>
</t>
</template>
<!-- French CoC template removed 2026-05-28 - the single bilingual
report_coc_en renders English + French together. -->
<!-- ================================================================== -->
<!-- Legacy portrait (bound to fusion.plating.portal.job) -->
<!-- ================================================================== -->
<template id="report_coc_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<div class="page">
<h4>
Certificate of Conformance -
<span t-field="doc.name"/>
</h4>
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 25%;">JOB REF</th>
<th class="info-header" style="width: 25%;">QTY</th>
<th class="info-header" style="width: 25%;">RECEIVED</th>
<th class="info-header" style="width: 25%;">SHIP DATE</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.quantity"/></td>
<td class="text-center"><span t-field="doc.received_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.actual_ship_date" t-options="{'widget': 'date'}"/></td>
</tr>
</tbody>
</table>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ================================================================== -->
<!-- Legacy landscape (bound to fusion.plating.portal.job) -->
<!-- ================================================================== -->
<template id="report_coc">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Certificate of Conformance
<span t-field="doc.name"/>
</h2>
<table class="bordered info-table">
<thead><tr>
<th>JOB REF</th>
<th>CUSTOMER</th>
<th>QUANTITY</th>
<th>RECEIVED</th>
<th>SHIP DATE</th>
<th>TRACKING REF</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td><span t-field="doc.partner_id"/></td>
<td class="text-center"><span t-field="doc.quantity"/></td>
<td class="text-center"><span t-field="doc.received_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.actual_ship_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.tracking_ref"/></td>
</tr></tbody>
</table>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,260 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12c - Chronological CoC body.
Walks fp.job.step.move records in time order (chain-of-custody),
rendering each transition as a heading ("Step Name (Tank Code)")
with a "Moved By / Time" meta line + a 5-column measurement
sub-table when the destination step has captured input values.
Mirrors Steelhead's CoC PDF layout (screens 19-24).
Wired via fp.certificate.body_style = 'chronological' through the
coc_body_router template.
-->
<odoo>
<template id="coc_body_chronological">
<!-- Resolve the linked job. fp.certificate has x_fc_job_id (Sub 11+). -->
<t t-set="job" t-value="('x_fc_job_id' in doc._fields and doc.x_fc_job_id) or False"/>
<t t-set="moves" t-value="(job and 'move_ids' in job._fields and job.move_ids.sorted('move_datetime')) or []"/>
<style>
.fp-coc-chrono { font-family: Arial, sans-serif; font-size: 9pt; color: #000; padding-top: 0; }
.fp-coc-chrono h3 { font-size: 11pt; margin: 8px 0 2px 0; font-weight: bold; }
.fp-coc-chrono .fp-chrono-meta { font-size: 8.5pt; color: #444; margin-bottom: 4px; }
.fp-coc-chrono table.bordered,
.fp-coc-chrono table.bordered th,
.fp-coc-chrono table.bordered td { border: 1px solid #000; border-collapse: collapse; }
.fp-coc-chrono table.bordered { width: 100%; margin-bottom: 8px; }
.fp-coc-chrono table.bordered th { background: #ededed; padding: 4px 6px; font-size: 8.5pt; text-align: center; }
.fp-coc-chrono table.bordered td { padding: 4px 6px; vertical-align: top; font-size: 8.5pt; }
.fp-coc-chrono .text-center { text-align: center; }
.fp-coc-chrono hr.heavy { border: 0; border-top: 2px solid #000; margin: 8px 0; }
</style>
<div class="fp-coc-chrono">
<!-- Title + Nadcap logo render in the shared coc_header above
this body (rendered once by the EN/FR wrapper). -->
<!-- Job header (compact) -->
<table class="bordered">
<tr>
<th style="width: 18%;">Part Number</th>
<th style="width: 28%;">Description</th>
<th style="width: 8%;">Quantity</th>
<th style="width: 8%;">Work Order</th>
<th style="width: 14%;">PO Number</th>
<th style="width: 14%;">Packing List No</th>
<th style="width: 10%;">Date</th>
</tr>
<tr>
<td>
<t t-if="job and 'part_catalog_id' in job._fields and job.part_catalog_id">
<span t-esc="job.part_catalog_id.part_number or job.product_id.default_code or '-'"/>
</t>
<t t-else="">
<span t-esc="(job and job.product_id and job.product_id.default_code) or '-'"/>
</t>
</td>
<td>
<t t-if="job and 'part_catalog_id' in job._fields and job.part_catalog_id">
<span t-esc="job.part_catalog_id.name or job.product_id.name or '-'"/>
</t>
<t t-else="">
<span t-esc="(job and job.product_id and job.product_id.name) or '-'"/>
</t>
</td>
<td class="text-center">
<span t-esc="(job and job.qty) or ''"/>
</td>
<td class="text-center">
<span t-esc="(job and job.name) or '-'"/>
</td>
<td>
<span t-esc="(job and job.sale_order_id and job.sale_order_id.client_order_ref) or '-'"/>
</td>
<td/>
<td>
<span t-esc="(doc.create_date and doc.create_date.strftime('%Y-%m-%d')) or ''"/>
</td>
</tr>
</table>
<h3 style="margin-top: 6px;">Specification(s):
<span style="font-weight: normal;"
t-esc="(job and job.recipe_id and job.recipe_id.name) or '-'"/>
</h3>
<hr class="heavy"/>
<!-- Chain-of-custody walk -->
<t t-foreach="moves" t-as="mv">
<t t-set="dest" t-value="mv.to_step_id"/>
<t t-set="tank_code" t-value="(mv.to_tank_id and mv.to_tank_id.code) or (dest and dest.tank_id and dest.tank_id.code) or ''"/>
<!-- Sub 12d - input_ids lives on recipe_node, not job.step.
Walk via recipe_node_id; filter to step_input + collect=True. -->
<t t-set="recipe_node" t-value="(dest and dest.recipe_node_id) or False"/>
<t t-set="captured" t-value="(recipe_node and recipe_node.input_ids.filtered(lambda i: (i.kind or 'step_input') == 'step_input' and (i.collect if 'collect' in i._fields else True)).sorted('sequence')) or []"/>
<h3>
<span t-esc="(dest and dest.name) or '-'"/>
<t t-if="tank_code"> (<span t-esc="tank_code"/>)</t>
</h3>
<div class="fp-chrono-meta">
<strong>Moved By:</strong> <span t-esc="mv.moved_by_user_id.name"/>
<span> · </span>
<strong>Time:</strong>
<span t-esc="mv.move_datetime and mv.move_datetime.strftime('%b %d, %Y %I:%M:%S %p') or ''"/>
<t t-if="mv.qty_moved">
<span> · </span>
<strong>Qty:</strong> <span t-esc="mv.qty_moved"/>
</t>
</div>
<!-- Sub 12c+ - index captured values from the move's transition_input_value_ids
by node_input_id so we can render Actual alongside Target. -->
<t t-set="captured_values_by_input" t-value="{v.node_input_id.id: v for v in mv.transition_input_value_ids}"/>
<!-- Measurement sub-table - show whenever destination has any
step_input prompts OR the move recorded any captured values. -->
<t t-set="prompts" t-value="captured"/>
<t t-if="not prompts and mv.transition_input_value_ids">
<t t-set="prompts" t-value="mv.transition_input_value_ids.mapped('node_input_id')"/>
</t>
<t t-if="prompts">
<table class="bordered">
<thead>
<tr>
<th style="width: 24%;">Name</th>
<th style="width: 28%;">Description</th>
<th style="width: 14%;">Target</th>
<th style="width: 20%;">Actual</th>
<th style="width: 14%;">Recorded By</th>
</tr>
</thead>
<tbody>
<t t-foreach="prompts" t-as="inp">
<t t-set="cv" t-value="captured_values_by_input.get(inp.id)"/>
<t t-set="actual_str" t-value="''"/>
<t t-if="cv">
<t t-if="inp.input_type == 'multi_point_thickness' and cv.value_text">
<t t-set="_payload" t-value="cv.value_text"/>
<t t-set="actual_str" t-value="_payload"/>
</t>
<t t-elif="inp.input_type == 'bath_chemistry_panel' and cv.value_text">
<t t-set="actual_str" t-value="cv.value_text"/>
</t>
<t t-elif="inp.input_type == 'ph' and cv.value_number">
<t t-set="actual_str" t-value="'pH %.2f' % cv.value_number"/>
</t>
<t t-elif="cv.value_text">
<t t-set="actual_str" t-value="cv.value_text"/>
</t>
<t t-elif="cv.value_number">
<t t-set="actual_str" t-value="('%s %s' % (cv.value_number, (inp.target_unit if 'target_unit' in inp._fields and inp.target_unit else ''))).strip()"/>
</t>
<t t-elif="cv.value_boolean is not False">
<t t-set="actual_str" t-value="'PASS' if cv.value_boolean else 'FAIL'"/>
</t>
<t t-elif="cv.value_date">
<t t-set="actual_str" t-value="cv.value_date.strftime('%Y-%m-%d %H:%M')"/>
</t>
</t>
<tr>
<td><span t-esc="inp.name"/></td>
<td>
<t t-if="inp.hint">
<span t-esc="inp.hint"/>
</t>
</td>
<td class="text-center">
<t t-if="'target_min' in inp._fields and inp.target_min and inp.target_max">
<span t-esc="inp.target_min"/>-<span t-esc="inp.target_max"/>
<t t-if="'target_unit' in inp._fields and inp.target_unit">
<span> </span><span t-esc="inp.target_unit"/>
</t>
</t>
<t t-elif="'target_unit' in inp._fields and inp.target_unit">
<span t-esc="inp.target_unit"/>
</t>
</td>
<td>
<t t-if="actual_str">
<strong t-esc="actual_str"/>
</t>
<t t-elif="cv and cv.value_attachment_id">
<!-- Photo: render as a thumbnail (height-capped); fall back to filename. -->
<img t-att-src="'/web/image/%s' % cv.value_attachment_id.id"
style="max-height: 80px; max-width: 160px; border: 1px solid #ccc;"
t-att-alt="cv.value_attachment_id.name"/>
<div style="font-size: 7.5pt; color: #555;">
<span t-esc="cv.value_attachment_id.name"/>
</div>
</t>
</td>
<td>
<span t-esc="(mv.moved_by_user_id and mv.moved_by_user_id.name) or ''"/>
</td>
</tr>
</t>
</tbody>
</table>
</t>
</t>
<hr class="heavy"/>
<!-- Sign-off block - unified with WO Detail / CoC (2026-05-17).
Signer = cert's certified_by user → falls back to company
owner. Signature image = signer's Plating Signature
(x_fc_signature_image from Preferences → My Profile). -->
<t t-set="signer_user" t-value="doc.certified_by_id or company.x_fc_owner_user_id or False"/>
<t t-set="signature_img" t-value="(signer_user and 'x_fc_signature_image' in signer_user._fields and signer_user.x_fc_signature_image) or False"/>
<t t-set="signer_name" t-value="(signer_user and signer_user.name) or ''"/>
<!-- Sub 12c+ - cert statement: per-customer override → company default → hardcoded fallback -->
<t t-set="_cust_stmt" t-value="('x_fc_cert_statement' in doc.partner_id._fields and doc.partner_id.x_fc_cert_statement) or False"/>
<t t-set="_co_stmt" t-value="('x_fc_default_cert_statement' in company._fields and company.x_fc_default_cert_statement) or False"/>
<t t-set="cert_statement" t-value="_cust_stmt or _co_stmt or 'We certify that the parts listed above have been processed in accordance with the specifications referenced and that all required tests have been performed. Records on file at our facility per AS9100 / ISO 9001 retention policy.'"/>
<table class="bordered">
<tr>
<td style="width: 50%; vertical-align: top;">
<strong>Certified By:</strong><br/>
<t t-if="signature_img">
<img t-att-src="'data:image/png;base64,%s' % signature_img.decode()"
style="max-height: 22mm; max-width: 70mm;"/>
</t><br/>
<strong>Name:</strong> <span t-esc="signer_name"/>
</td>
<td style="width: 50%; vertical-align: top;">
<strong>Certification Statement:</strong>
<span style="font-size: 8.5pt;">
Ref. WO# <span t-esc="(job and job.name) or ''"/>
</span>
<p style="font-size: 8pt; margin-top: 4px; white-space: pre-wrap;"
t-esc="cert_statement"/>
</td>
</tr>
</table>
</div>
</template>
<!-- ============================================================== -->
<!-- Router - picks chronological vs classic body -->
<!-- Wired into the existing CoC actions in report_coc.xml. -->
<!-- ============================================================== -->
<template id="coc_body_router">
<t t-if="doc.body_style == 'chronological' and 'x_fc_job_id' in doc._fields and doc.x_fc_job_id">
<t t-call="fusion_plating_reports.coc_body_chronological"/>
</t>
<t t-else="">
<t t-call="fusion_plating_reports.coc_body"/>
</t>
</template>
</odoo>

View File

@@ -1,105 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Discharge Sample Report
-->
<odoo>
<template id="report_discharge_sample">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Discharge Sample Report
<span t-field="doc.name"/>
</h2>
<!-- Header Info -->
<table class="bordered info-table">
<thead><tr>
<th>SAMPLE #</th>
<th>SAMPLE DATE</th>
<th>FACILITY</th>
<th>SAMPLE POINT</th>
<th>STATUS</th>
<th>WORST RESULT</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.sample_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.facility_id"/></td>
<td class="text-center"><span t-field="doc.sample_point"/></td>
<td class="text-center"><span t-field="doc.state"/></td>
<td class="text-center">
<span t-if="doc.worst_status == 'ok'" class="status-ok">OK</span>
<span t-if="doc.worst_status == 'warning'" class="status-warning">Warning</span>
<span t-if="doc.worst_status == 'out_of_spec'" class="status-fail">Out of Spec</span>
<span t-if="doc.worst_status == 'pending'" style="color: #666;">Pending</span>
</td>
</tr></tbody>
</table>
<!-- Lab Details -->
<table class="bordered info-table">
<thead><tr>
<th>COLLECTED BY</th>
<th>CHAIN OF CUSTODY #</th>
<th>LAB</th>
<th>LAB REPORT #</th>
<th>RESULTS RECEIVED</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.collected_by_id"/></td>
<td class="text-center"><span t-field="doc.chain_of_custody_ref"/></td>
<td class="text-center"><span t-field="doc.lab_id"/></td>
<td class="text-center"><span t-field="doc.lab_report_ref"/></td>
<td class="text-center"><span t-field="doc.received_date" t-options="{'widget': 'date'}"/></td>
</tr></tbody>
</table>
<!-- Parameter Lines -->
<t t-if="doc.line_ids">
<table class="bordered">
<thead><tr>
<th>PARAMETER</th>
<th>RESULT</th>
<th>UoM</th>
<th>STATUS</th>
<th>NOTE</th>
</tr></thead>
<tbody>
<t t-foreach="doc.line_ids" t-as="line">
<tr>
<td><span t-field="line.parameter"/></td>
<td class="text-center"><span t-field="line.value"/></td>
<td class="text-center"><span t-field="line.uom"/></td>
<td class="text-center">
<span t-if="line.status == 'ok'" class="status-ok">OK</span>
<span t-if="line.status == 'warning'" class="status-warning">Warning</span>
<span t-if="line.status == 'out_of_spec'" class="status-fail">Out of Spec</span>
<span t-if="line.status == 'pending'" style="color: #666;">Pending</span>
</td>
<td><span t-field="line.notes"/></td>
</tr>
</t>
</tbody>
</table>
</t>
<!-- Notes -->
<t t-if="doc.notes">
<table class="bordered">
<tr class="section-row"><td>NOTES</td></tr>
<tr><td><t t-out="doc.notes"/></td></tr>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
First Article Inspection Report
-->
<odoo>
<template id="report_fair">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
First Article Inspection Report
<span t-field="doc.name"/>
</h2>
<!-- Header Info -->
<table class="bordered info-table">
<thead><tr>
<th>FAIR #</th>
<th>PART NUMBER</th>
<th>REVISION</th>
<th>CUSTOMER</th>
<th>RESULT</th>
<th>STATUS</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.part_number"/></td>
<td class="text-center"><span t-field="doc.part_revision"/></td>
<td class="text-center"><span t-field="doc.customer_id"/></td>
<td class="text-center">
<span t-if="doc.result == 'pass'" class="status-ok">Pass</span>
<span t-if="doc.result == 'fail'" class="status-fail">Fail</span>
<span t-if="doc.result == 'conditional'" class="status-warning">Conditional</span>
</td>
<td class="text-center"><span t-field="doc.state"/></td>
</tr></tbody>
</table>
<!-- Inspection Details -->
<table class="bordered info-table">
<thead><tr>
<th>INSPECTION DATE</th>
<th>INSPECTOR</th>
<th>PROCESSES</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.performed_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.performed_by_id"/></td>
<td class="text-center">
<t t-foreach="doc.process_type_ids" t-as="pt">
<span t-out="pt.name"/>
<t t-if="not pt_last">, </t>
</t>
</td>
</tr></tbody>
</table>
<!-- Notes / Findings -->
<t t-if="doc.notes">
<table class="bordered">
<tr class="section-row"><td>FINDINGS / NOTES</td></tr>
<tr><td><t t-out="doc.notes"/></td></tr>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,151 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Sub 12 Phase E - 8D Report (NCR + linked CAPA combined).
Bound to fusion.plating.ncr. Renders all 8 disciplines in one PDF.
Degraded mode if no CAPA is linked: D4-D8 sections show a placeholder
note that the CAPA hasn't been opened yet.
-->
<odoo>
<template id="report_fp_8d_doc">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="ncr">
<t t-set="capa" t-value="ncr.capa_ids[:1] if ncr.capa_ids else False"/>
<t t-call="web.external_layout">
<div class="page" style="font-family: 'Helvetica Neue', Arial, sans-serif; color: #2b2b2b; font-size: 12px;">
<div style="border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 18px;">
<h1 style="margin: 0; font-size: 22px;">8D Report</h1>
<div style="display: flex; justify-content: space-between; margin-top: 6px;">
<span><strong>NCR:</strong> <span t-out="ncr.name"/></span>
<span t-if="capa"><strong>CAPA:</strong> <span t-out="capa.name"/></span>
<span><strong>Issued:</strong> <span t-out="ncr.reported_date" t-options='{"widget": "date"}'/></span>
</div>
</div>
<!-- D1 - Team -->
<div style="margin-bottom: 14px;">
<h2 style="font-size: 14px; background: #eee; padding: 6px 10px; margin: 0 0 6px 0;">D1 - Team</h2>
<table style="width: 100%; padding: 6px;">
<tr><td style="width: 130px; color: #666;">Lead</td><td><span t-out="ncr.team_id.lead_user_id.name or ncr.reported_by_id.name"/></td></tr>
<tr><td style="color: #666;">Team</td><td><span t-out="ncr.team_id.name or 'Unassigned'"/></td></tr>
<tr t-if="ncr.team_id and ncr.team_id.member_ids"><td style="color: #666;">Members</td><td><span t-out="', '.join(ncr.team_id.member_ids.mapped('name'))"/></td></tr>
</table>
</div>
<!-- D2 - Problem Description -->
<div style="margin-bottom: 14px;">
<h2 style="font-size: 14px; background: #eee; padding: 6px 10px; margin: 0 0 6px 0;">D2 - Problem Description</h2>
<div style="padding: 6px;">
<table style="width: 100%; margin-bottom: 8px;">
<tr><td style="width: 130px; color: #666;">Severity</td><td><span t-field="ncr.severity"/></td></tr>
<tr><td style="color: #666;">Source</td><td><span t-field="ncr.source"/></td></tr>
<tr t-if="ncr.customer_partner_id"><td style="color: #666;">Customer</td><td><span t-out="ncr.customer_partner_id.name"/></td></tr>
<tr t-if="ncr.part_ref"><td style="color: #666;">Part / Lot</td><td><span t-out="ncr.part_ref"/></td></tr>
<tr t-if="ncr.quantity_affected"><td style="color: #666;">Qty Affected</td><td><span t-out="ncr.quantity_affected"/></td></tr>
</table>
<div style="background: #fafafa; padding: 8px; border-left: 3px solid #ddd;">
<t t-out="ncr.description or 'No description recorded.'"/>
</div>
</div>
</div>
<!-- D3 - Containment -->
<div style="margin-bottom: 14px;">
<h2 style="font-size: 14px; background: #eee; padding: 6px 10px; margin: 0 0 6px 0;">D3 - Containment Action</h2>
<div style="padding: 6px; background: #fafafa; border-left: 3px solid #ddd;">
<t t-out="ncr.containment or 'No containment narrative recorded.'"/>
</div>
</div>
<!-- D4 - Root Cause -->
<div style="margin-bottom: 14px;">
<h2 style="font-size: 14px; background: #eee; padding: 6px 10px; margin: 0 0 6px 0;">D4 - Root Cause Analysis</h2>
<div style="padding: 6px;">
<table t-if="capa or ncr.reason_id" style="width: 100%; margin-bottom: 8px;">
<tr t-if="ncr.reason_id"><td style="width: 130px; color: #666;">Classified Reason</td><td><span t-out="ncr.reason_id.name"/> (<span t-field="ncr.reason_id.category"/>)</td></tr>
<tr t-if="capa and capa.reason_id"><td style="width: 130px; color: #666;">CAPA Reason</td><td><span t-out="capa.reason_id.name"/></td></tr>
</table>
<div style="background: #fafafa; padding: 8px; border-left: 3px solid #ddd;">
<t t-if="capa and capa.root_cause_analysis"><t t-out="capa.root_cause_analysis"/></t>
<t t-elif="ncr.root_cause"><t t-out="ncr.root_cause"/></t>
<t t-else="">Root cause not yet documented.</t>
</div>
</div>
</div>
<!-- D5 - Permanent Corrective Action -->
<div style="margin-bottom: 14px;">
<h2 style="font-size: 14px; background: #eee; padding: 6px 10px; margin: 0 0 6px 0;">D5 - Permanent Corrective Action</h2>
<div style="padding: 6px; background: #fafafa; border-left: 3px solid #ddd;">
<t t-if="capa and capa.action_plan"><t t-out="capa.action_plan"/></t>
<t t-else="">No corrective action plan recorded - open a CAPA from the NCR to populate this section.</t>
</div>
</div>
<!-- D6 - Implement &amp; Verify -->
<div style="margin-bottom: 14px;">
<h2 style="font-size: 14px; background: #eee; padding: 6px 10px; margin: 0 0 6px 0;">D6 - Implement &amp; Verify</h2>
<div style="padding: 6px;">
<table t-if="capa" style="width: 100%; margin-bottom: 8px;">
<tr><td style="width: 130px; color: #666;">CAPA State</td><td><span t-field="capa.state"/></td></tr>
<tr><td style="color: #666;">Owner</td><td><span t-out="capa.owner_id.name or '-'"/></td></tr>
<tr><td style="color: #666;">Due</td><td><span t-out="capa.due_date or '-'"/></td></tr>
<tr><td style="color: #666;">Verification</td><td><span t-out="capa.verification_date or 'Pending'"/></td></tr>
<tr><td style="color: #666;">Effective</td><td><span t-out="'Yes' if capa.is_effective else 'Pending'"/></td></tr>
</table>
<div style="background: #fafafa; padding: 8px; border-left: 3px solid #ddd;" t-if="capa">
<t t-out="capa.effectiveness_notes or 'No effectiveness notes yet.'"/>
</div>
<div t-if="not capa" style="color: #666;">No CAPA opened - implementation tracking unavailable.</div>
</div>
</div>
<!-- D7 - Prevent Recurrence -->
<div style="margin-bottom: 14px;">
<h2 style="font-size: 14px; background: #eee; padding: 6px 10px; margin: 0 0 6px 0;">D7 - Prevent Recurrence</h2>
<div style="padding: 6px; background: #fafafa; border-left: 3px solid #ddd;">
<t t-if="capa and capa.type == 'preventive' and capa.action_plan">
<t t-out="capa.action_plan"/>
</t>
<t t-elif="capa and capa.action_plan">
<em>Refer to D5 - corrective action plan covers preventive measures.</em>
</t>
<t t-else="">No preventive actions recorded. Open a Preventive-type CAPA to track recurrence-prevention measures separately.</t>
</div>
</div>
<!-- D8 - Recognise the Team -->
<div style="margin-bottom: 14px;">
<h2 style="font-size: 14px; background: #eee; padding: 6px 10px; margin: 0 0 6px 0;">D8 - Recognise the Team</h2>
<div style="padding: 6px;">
<p t-if="capa and capa.state in ('effective', 'closed')">
Closure verified <span t-out="capa.verification_date"/> by <span t-out="capa.verification_by_id.name or '-'"/>.
</p>
<p t-else="">Pending closure.</p>
<table style="width: 100%; margin-top: 8px; font-size: 11px; color: #666;">
<tr><td>NCR Closed:</td><td><span t-out="ncr.closed_date or 'Open'"/></td></tr>
<tr t-if="capa"><td>CAPA Verified:</td><td><span t-out="capa.verification_date or '-'"/></td></tr>
</table>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
<record id="action_report_fp_8d" model="ir.actions.report">
<field name="name">8D Report</field>
<field name="model">fusion.plating.ncr</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_8d_doc</field>
<field name="report_file">fusion_plating_reports.report_fp_8d_doc</field>
<field name="binding_model_id" ref="fusion_plating_quality.model_fusion_plating_ncr"/>
<field name="binding_type">report</field>
<field name="print_report_name">'8D-' + (object.name or '').replace('/', '-')</field>
</record>
</odoo>

View File

@@ -1,459 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Fusion Plating - Bill of Lading (Portrait + Landscape).
Binds to fusion.plating.delivery. Includes shipper, consignee, carrier,
cargo description, special instructions, and sign-off lines.
-->
<odoo>
<!-- ============================================================= -->
<!-- PORTRAIT -->
<!-- ============================================================= -->
<template id="report_fp_bol_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<div class="page">
<!-- Resolve shipper company defensively - fall back to env.company
when delivery.company_id is missing on legacy records. -->
<t t-set="ship_co" t-value="doc.company_id or env.company"/>
<h2 class="text-center" style="text-align: center; font-size: 24pt; margin: 0 0 6px 0;">
BILL OF LADING
</h2>
<div class="text-center" style="text-align: center; margin-bottom: 14px; font-size: 13pt;">
<strong>BoL #: <span t-field="doc.name"/></strong>
</div>
<!-- Shipper / Consignee -->
<table class="bordered">
<thead>
<tr>
<th class="fp-header-primary" style="width: 50%;">SHIPPER</th>
<th class="fp-header-primary" style="width: 50%;">CONSIGNEE</th>
</tr>
</thead>
<tbody>
<tr>
<td style="height: 110px;">
<strong><span t-esc="ship_co.name"/></strong><br/>
<t t-if="doc.source_facility_id">
<em t-field="doc.source_facility_id.name"/><br/>
</t>
<div t-field="ship_co.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 110px;">
<strong><span t-field="doc.partner_id.name"/></strong><br/>
<t t-if="doc.delivery_address_id">
<div t-field="doc.delivery_address_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone'], 'no_marker': True}"/>
</t>
<t t-else="">
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone'], 'no_marker': True}"/>
</t>
<t t-if="doc.contact_name">
<strong>Attn: </strong><span t-field="doc.contact_name"/><br/>
</t>
<t t-if="doc.contact_phone">
<strong>Phone: </strong><span t-field="doc.contact_phone"/>
</t>
</td>
</tr>
</tbody>
</table>
<!-- Shipment info -->
<table class="bordered">
<thead>
<tr>
<th class="fp-header-primary" style="width: 33%;">SHIP DATE</th>
<th class="fp-header-primary" style="width: 33%;">DRIVER</th>
<th class="fp-header-primary" style="width: 34%;">VEHICLE</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center fp-cell-mid"><span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center fp-cell-mid">
<t t-if="doc.assigned_driver_id">
<span t-field="doc.assigned_driver_id.name"/>
</t>
<t t-else="">-</t>
</td>
<td class="text-center fp-cell-mid">
<t t-if="doc.vehicle_id">
<span t-field="doc.vehicle_id"/>
</t>
<t t-else="">-</t>
</td>
</tr>
</tbody>
</table>
<!-- Job ref / TDG -->
<table class="bordered">
<thead>
<tr>
<th class="fp-header-primary" style="width: 50%;">JOB REFERENCE</th>
<th class="fp-header-primary" style="width: 50%;">DANGEROUS GOODS (TDG)</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center fp-cell-mid"><span t-esc="doc.job_ref or '-'"/></td>
<td class="text-center fp-cell-mid">
<span t-if="doc.tdg_required" class="status-warning">TDG REQUIRED</span>
<span t-else="" class="status-ok">No TDG</span>
</td>
</tr>
</tbody>
</table>
<!-- Cargo description - iterate the linked fp.job's SO lines
so each part renders with its customer part number via
the shared macro. Sub 11 - replaced mrp.production lookup. -->
<t t-set="_job" t-value="env['fp.job'].sudo().search([('name', '=', doc.job_ref)], limit=1) if doc.job_ref else env['fp.job']"/>
<t t-set="_so" t-value="_job.sale_order_id if _job else False"/>
<t t-set="_lines" t-value="_so.order_line.filtered(lambda l: l.product_id and l.product_uom_qty &gt; 0) if _so else False"/>
<table class="bordered">
<thead>
<tr>
<th colspan="5" class="fp-header-primary">CARGO DESCRIPTION</th>
</tr>
<tr>
<th class="fp-header-primary" style="width: 12%;">PACKAGES</th>
<th class="fp-header-primary text-start" style="width: 48%;">DESCRIPTION OF GOODS</th>
<th class="fp-header-primary" style="width: 12%;">QTY</th>
<th class="fp-header-primary" style="width: 14%;">WEIGHT</th>
<th class="fp-header-primary" style="width: 14%;">CLASS</th>
</tr>
</thead>
<tbody>
<t t-if="_lines">
<t t-foreach="_lines" t-as="line">
<tr>
<td class="text-center fp-cell-mid">
<t t-if="line_first">1</t>
<t t-else=""/>
</td>
<td class="fp-cell-mid">
<t t-call="fusion_plating_reports.customer_line_header"/>
<t t-if="line_last and doc.notes">
<br/><span t-field="doc.notes"/>
</t>
</td>
<td class="text-center fp-cell-mid">
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
</td>
<td class="text-center fp-cell-mid">-</td>
<td class="text-center fp-cell-mid">
<span t-if="doc.tdg_required">TDG</span>
<span t-else="">NON-HAZ</span>
</td>
</tr>
</t>
</t>
<t t-else="">
<tr>
<td class="text-center fp-cell-mid">1</td>
<td class="fp-cell-mid">
Plated parts - Job <span t-esc="doc.job_ref or doc.name"/>
<t t-if="doc.notes">
<br/><span t-field="doc.notes"/>
</t>
</td>
<td class="text-center fp-cell-mid">
<span t-esc="int(_mo.product_qty) if _mo else '-'"/>
</td>
<td class="text-center fp-cell-mid">-</td>
<td class="text-center fp-cell-mid">
<span t-if="doc.tdg_required">TDG</span>
<span t-else="">NON-HAZ</span>
</td>
</tr>
</t>
</tbody>
</table>
<!-- Certificate + Packing list refs -->
<table class="bordered">
<thead>
<tr>
<th class="fp-header-primary" style="width: 50%;">CoC ATTACHED</th>
<th class="fp-header-primary" style="width: 50%;">PACKING LIST</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center fp-cell-mid">
<span t-if="doc.coc_attachment_id" class="status-ok">✓ Attached</span>
<span t-else="">-</span>
</td>
<td class="text-center fp-cell-mid">
<span t-if="doc.packing_list_attachment_id" class="status-ok">✓ Attached</span>
<span t-else="">-</span>
</td>
</tr>
</tbody>
</table>
<!-- Cert statement + signatures held together so the
BoL doesn't split the signature row across pages. -->
<div class="fp-keep-together">
<div class="highlight-box" style="margin-top: 10px;">
This is to certify that the above-named materials are properly classified,
packaged, marked, and labelled, and are in proper condition for transportation
according to the applicable regulations of the Department of Transportation.
</div>
<table class="bordered sig-table">
<tr>
<td class="sig-cell" style="width: 33.33%;">
<div class="sig-line"/>
<div class="small-muted">Shipper (Signature / Date)</div>
</td>
<td class="sig-cell" style="width: 33.33%;">
<div class="sig-line"/>
<div class="small-muted">Carrier / Driver (Signature / Date)</div>
</td>
<td class="sig-cell" style="width: 33.33%;">
<div class="sig-line"/>
<div class="small-muted">Consignee (Signature / Date)</div>
</td>
</tr>
</table>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- LANDSCAPE -->
<!-- ============================================================= -->
<template id="report_fp_bol_landscape">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<t t-set="ship_co" t-value="doc.company_id or env.company"/>
<h2 style="text-align: center; font-size: 18pt; margin: 0 0 2px 0;">BILL OF LADING</h2>
<div class="text-center" style="text-align: center; margin-bottom: 6px; font-size: 11pt;">
<strong>BoL #: <span t-field="doc.name"/></strong>
</div>
<!-- Shipper / Consignee -->
<table class="bordered">
<thead>
<tr>
<th class="fp-header-primary" style="width: 50%;">SHIPPER</th>
<th class="fp-header-primary" style="width: 50%;">CONSIGNEE</th>
</tr>
</thead>
<tbody>
<tr>
<td style="height: 70px; font-size: 10pt;">
<strong><span t-esc="ship_co.name"/></strong><br/>
<t t-if="doc.source_facility_id">
<em t-field="doc.source_facility_id.name"/><br/>
</t>
<div t-field="ship_co.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 70px; font-size: 10pt;">
<strong><span t-field="doc.partner_id.name"/></strong><br/>
<t t-if="doc.delivery_address_id">
<div t-field="doc.delivery_address_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone'], 'no_marker': True}"/>
</t>
<t t-else="">
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone'], 'no_marker': True}"/>
</t>
<t t-if="doc.contact_name">
<strong>Attn: </strong><span t-field="doc.contact_name"/><br/>
</t>
<t t-if="doc.contact_phone">
<strong>Phone: </strong><span t-field="doc.contact_phone"/>
</t>
</td>
</tr>
</tbody>
</table>
<!-- Shipment info (wide) -->
<table class="bordered info-table">
<thead>
<tr>
<th>SHIP DATE</th>
<th>DRIVER</th>
<th>VEHICLE</th>
<th>JOB REFERENCE</th>
<th>TDG</th>
<th>STATUS</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.scheduled_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center">
<t t-if="doc.assigned_driver_id"><span t-field="doc.assigned_driver_id.name"/></t>
<t t-else="">-</t>
</td>
<td class="text-center">
<t t-if="doc.vehicle_id"><span t-field="doc.vehicle_id"/></t>
<t t-else="">-</t>
</td>
<td class="text-center"><span t-esc="doc.job_ref or '-'"/></td>
<td class="text-center">
<span t-if="doc.tdg_required" class="status-warning">REQUIRED</span>
<span t-else="" class="status-ok">None</span>
</td>
<td class="text-center"><span t-field="doc.state"/></td>
</tr>
</tbody>
</table>
<!-- Cargo description - iterate the linked fp.job's SO lines.
Sub 11 - replaced mrp.production lookup. -->
<t t-set="_job" t-value="env['fp.job'].sudo().search([('name', '=', doc.job_ref)], limit=1) if doc.job_ref else env['fp.job']"/>
<t t-set="_so" t-value="_job.sale_order_id if _job else False"/>
<t t-set="_lines" t-value="_so.order_line.filtered(lambda l: l.product_id and l.product_uom_qty &gt; 0) if _so else False"/>
<table class="bordered">
<thead>
<tr>
<th colspan="6" class="fp-header-primary">CARGO DESCRIPTION</th>
</tr>
<tr>
<th style="width: 10%;">PACKAGES</th>
<th class="text-start" style="width: 40%;">DESCRIPTION OF GOODS</th>
<th style="width: 12%;">QTY</th>
<th style="width: 12%;">WEIGHT</th>
<th style="width: 12%;">CLASS</th>
<th style="width: 14%;">SPECIAL HANDLING</th>
</tr>
</thead>
<tbody>
<t t-if="_lines">
<t t-foreach="_lines" t-as="line">
<tr>
<td class="text-center">
<t t-if="line_first">1</t>
<t t-else=""/>
</td>
<td>
<t t-call="fusion_plating_reports.customer_line_header"/>
<t t-if="line_last and doc.notes">
<br/><span t-field="doc.notes"/>
</t>
</td>
<td class="text-center">
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
</td>
<td class="text-center">-</td>
<td class="text-center">
<span t-if="doc.tdg_required">TDG</span>
<span t-else="">NON-HAZ</span>
</td>
<td class="text-center">
<span t-if="doc.tdg_required" class="status-warning">TDG HANDLING</span>
<span t-else="">Standard</span>
</td>
</tr>
</t>
</t>
<t t-else="">
<tr>
<td class="text-center">1</td>
<td>
Plated parts - Job <span t-esc="doc.job_ref or doc.name"/>
<t t-if="doc.notes">
<br/><span t-field="doc.notes"/>
</t>
</td>
<td class="text-center">-</td>
<td class="text-center">-</td>
<td class="text-center">
<span t-if="doc.tdg_required">TDG</span>
<span t-else="">NON-HAZ</span>
</td>
<td class="text-center">
<span t-if="doc.tdg_required" class="status-warning">TDG HANDLING</span>
<span t-else="">Standard</span>
</td>
</tr>
</t>
</tbody>
</table>
<!-- Attachment refs -->
<table class="bordered info-table">
<thead>
<tr>
<th>CoC</th>
<th>PACKING LIST</th>
<th>CUSTODY EVENTS</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">
<span t-if="doc.coc_attachment_id" class="status-ok">✓ Attached</span>
<span t-else="">-</span>
</td>
<td class="text-center">
<span t-if="doc.packing_list_attachment_id" class="status-ok">✓ Attached</span>
<span t-else="">-</span>
</td>
<td class="text-center"><span t-field="doc.custody_event_count"/></td>
</tr>
</tbody>
</table>
<!-- Certification -->
<div class="highlight-box" style="margin-top: 10px;">
This is to certify that the above-named materials are properly classified,
packaged, marked, and labelled, and are in proper condition for transportation
according to the applicable regulations of the Department of Transportation.
</div>
<!-- Sign off -->
<table class="bordered sig-table">
<tr>
<td class="sig-cell" style="width: 33.33%;">
<div class="sig-line"/>
<div class="small-muted">Shipper (Signature / Date)</div>
</td>
<td class="sig-cell" style="width: 33.33%;">
<div class="sig-line"/>
<div class="small-muted">Carrier / Driver (Signature / Date)</div>
</td>
<td class="sig-cell" style="width: 33.33%;">
<div class="sig-line"/>
<div class="small-muted">Consignee (Signature / Date)</div>
</td>
</tr>
</table>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,741 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Fusion Plating - Invoice (Portrait + Landscape).
Binds to account.move. Includes invoice strategy, deposit context,
payment details, and amount due.
-->
<odoo>
<!-- ============================================================= -->
<!-- PORTRAIT -->
<!-- ============================================================= -->
<template id="report_fp_invoice_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<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"/>
<!-- 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">&#160;&#160;&#160;</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%;">
<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>
<tr>
<td style="height: 70px;">
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 70px;">
<t t-if="doc.partner_shipping_id">
<div t-field="doc.partner_shipping_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
</t>
<t t-else="">
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
</t>
</td>
</tr>
</tbody>
</table>
<!-- Row 1: Invoice Date | Due Date | Sales Rep | Customer PO # | Payment Ref -->
<table class="bordered">
<thead>
<tr>
<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_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>
<!-- Row 2: Customer Job # | Delivery Method (pulled from source SO; hidden on manual invoices). -->
<t t-if="source_so and (source_so.x_fc_customer_job_number or delivery_method_label)">
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 50%;">
<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: 50%;">
<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="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: 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="6"><strong t-field="line.name"/></td></tr>
</t>
<t t-elif="line.display_type == 'line_note'">
<tr class="note-row"><td colspan="6"><span t-field="line.name"/></td></tr>
</t>
<!-- Charge / fee lines render in the totals block, not here. -->
<t t-elif="(not line.display_type or line.display_type == 'product') and line.x_fc_part_catalog_id">
<tr>
<td>
<!-- 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="line.x_fc_serial_ids">
<span t-esc="', '.join(line.x_fc_serial_ids.mapped('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">
<span t-esc="int(line.quantity) if line.quantity == int(line.quantity) else line.quantity"/>
</td>
<td class="text-center"><span t-field="line.product_uom_id"/></td>
<td class="text-end">
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
<td class="text-end">
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
</t>
</tbody>
</table>
<!-- Terms + Totals -->
<div class="row" style="margin-top: 15px;">
<div class="col-6">
<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;">
<!-- Additional charges (tooling, rush, etc.) carry from
the SO as real taxed invoice lines but render here
under the subtotal, NOT in the parts table above.
Subtotal is parts-only so tax + grand total stay the
standard Odoo figures. -->
<t t-set="fp_charge_lines" t-value="doc.invoice_line_ids.filtered(lambda l: (not l.display_type or l.display_type == 'product') and not l.x_fc_part_catalog_id)"/>
<t t-set="fp_charge_total" t-value="sum(fp_charge_lines.mapped('price_subtotal'))"/>
<t t-set="fp_parts_subtotal" t-value="doc.amount_untaxed - fp_charge_total"/>
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<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-out="fp_parts_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<t t-foreach="fp_charge_lines" t-as="cl">
<tr>
<td><span t-esc="cl.name or 'Additional Charge'"/></td>
<td class="text-end">
<span t-field="cl.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
<tr>
<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>
<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><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>
</tr>
</t>
</table>
</div>
</div>
<!-- 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 / PAYÉ</span>
</div>
</t>
<!-- Notes -->
<t t-if="doc.narration">
<div style="margin-top: 15px;">
<strong>Notes / Remarques:</strong>
<div t-field="doc.narration"/>
</div>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- LANDSCAPE -->
<!-- ============================================================= -->
<template id="report_fp_invoice_landscape">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<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"/>
<!-- 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">&#160;&#160;&#160;</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%;">
<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>
<tr>
<td style="height: 70px; font-size: 12pt;">
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 70px; font-size: 12pt;">
<t t-if="doc.partner_shipping_id">
<div t-field="doc.partner_shipping_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
</t>
<t t-else="">
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
</t>
</td>
</tr>
</tbody>
</table>
<!-- Invoice info row (wide, 6 cols on landscape) -->
<table class="bordered info-table">
<thead>
<tr>
<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_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>
<td class="text-center"><span t-field="doc.currency_id.name"/></td>
</tr>
</tbody>
</table>
<!-- 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 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">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="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="7 if has_discount else 6"/>
<table class="bordered">
<thead>
<tr>
<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>
<t t-foreach="doc.invoice_line_ids" t-as="line">
<t t-if="line.display_type == 'line_section'">
<tr class="section-row"><td t-att-colspan="col_count"><strong t-field="line.name"/></td></tr>
</t>
<t t-elif="line.display_type == 'line_note'">
<tr class="note-row"><td t-att-colspan="col_count"><span t-field="line.name"/></td></tr>
</t>
<!-- Charge / fee lines render in the totals block, not here. -->
<t t-elif="(not line.display_type or line.display_type == 'product') and line.x_fc_part_catalog_id">
<tr>
<td>
<!-- 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="line.x_fc_serial_ids">
<span t-esc="', '.join(line.x_fc_serial_ids.mapped('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">
<span t-esc="int(line.quantity) if line.quantity == int(line.quantity) else line.quantity"/>
</td>
<td class="text-center"><span t-field="line.product_uom_id"/></td>
<td class="text-end">
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</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-end">
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
</t>
</tbody>
</table>
<!-- Terms + Totals -->
<div class="row" style="margin-top: 15px;">
<div class="col-7">
<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;">
<!-- Additional charges render under the subtotal (see the
portrait template for the rationale). -->
<t t-set="fp_charge_lines" t-value="doc.invoice_line_ids.filtered(lambda l: (not l.display_type or l.display_type == 'product') and not l.x_fc_part_catalog_id)"/>
<t t-set="fp_charge_total" t-value="sum(fp_charge_lines.mapped('price_subtotal'))"/>
<t t-set="fp_parts_subtotal" t-value="doc.amount_untaxed - fp_charge_total"/>
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<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-out="fp_parts_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<t t-foreach="fp_charge_lines" t-as="cl">
<tr>
<td><span t-esc="cl.name or 'Additional Charge'"/></td>
<td class="text-end">
<span t-field="cl.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
<tr>
<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>
<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><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>
</tr>
</t>
</table>
</div>
</div>
<!-- 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 / PAYÉ</span>
</div>
</t>
<!-- Payment history -->
<t t-if="doc.payment_state != 'invoicing_legacy'">
<t t-set="payments_vals" t-value="doc.sudo().invoice_payments_widget and doc.sudo().invoice_payments_widget.get('content') or []"/>
<t t-if="payments_vals">
<table class="bordered" style="margin-top: 15px;">
<thead>
<tr>
<th colspan="3" style="background-color: #28a745; color: white;">
<t t-if="doc.payment_state in ('paid', 'in_payment')">✓ PAYMENT DETAILS - PAID IN FULL</t>
<t t-elif="doc.payment_state == 'partial'">PAYMENT DETAILS - PARTIALLY PAID</t>
<t t-else="">PAYMENT DETAILS</t>
</th>
</tr>
<tr style="background-color: #f5f5f5;">
<th style="width: 30%;">Date</th>
<th style="width: 40%;">Payment Method</th>
<th style="width: 30%;" class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<t t-foreach="payments_vals" t-as="pv">
<tr t-if="not pv.get('is_exchange')">
<td><span t-out="pv.get('date')" t-options='{"widget": "date"}'/></td>
<td><span t-out="pv.get('payment_method_name') or '-'"/></td>
<td class="text-end">
<span t-out="pv.get('amount')" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
</tbody>
</table>
</t>
</t>
<!-- Notes -->
<t t-if="doc.narration">
<div style="margin-top: 15px;">
<strong>Notes:</strong>
<div t-field="doc.narration"/>
</div>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,623 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Fusion Plating - Job Traveller (Shop Router).
One centralised document that follows a job through the shop. Pulls
together everything about the MO: customer + PO, receiving, recipe,
every work order with a sign-off row, bath chemistry targets,
quality holds, certificates issued, delivery info, notes.
Two templates:
- report_fp_job_traveller_mo: canonical, per MO
- report_fp_job_traveller_so: SO wrapper that iterates the SO's MOs
Two paper formats (portrait + landscape) wrap the inner body so the
planner can choose. Landscape is the shop-floor default because the
routing table is wider.
-->
<odoo>
<!-- ============================================================= -->
<!-- INNER BODY - shared between portrait and landscape -->
<!-- Receives `mo` (mrp.production) in the t-call context. -->
<!-- ============================================================= -->
<template id="report_fp_job_traveller_body">
<t t-set="so" t-value="mo.env['sale.order'].search([('name', '=', mo.origin)], limit=1) if mo.origin else mo.env['sale.order']"/>
<t t-set="job" t-value="mo.x_fc_portal_job_id"/>
<t t-set="Cert" t-value="mo.env.get('fp.certificate')"/>
<t t-set="certs" t-value="Cert.search([('production_id', '=', mo.id)]) if Cert is not None else []"/>
<t t-set="Delivery" t-value="mo.env.get('fusion.plating.delivery')"/>
<t t-set="deliveries" t-value="Delivery.search([('job_ref', '=', job.name)]) if (Delivery is not None and job) else []"/>
<t t-set="holds" t-value="mo.env['fusion.plating.quality.hold'].search([('production_id', '=', mo.id)])"/>
<t t-set="Receiving" t-value="mo.env.get('fp.receiving')"/>
<t t-set="receivings" t-value="Receiving.search([('sale_order_id', '=', so.id)]) if (Receiving is not None and so) else []"/>
<t t-set="wos" t-value="mo.workorder_ids.sorted('sequence')"/>
<!-- ===== Title bar ===== -->
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 10px;">
<h2 style="margin: 0; font-size: 20pt;">
Job Traveller - <span t-field="mo.name"/>
</h2>
<div style="text-align: right;">
<t t-if="mo.x_fc_is_rework">
<span class="status-warning" style="font-size: 11pt;">
<i class="fa fa-refresh"/> REWORK
<t t-if="mo.x_fc_original_production_id">
of <span t-field="mo.x_fc_original_production_id.name"/>
</t>
</span>
</t>
<t t-if="so and so.x_fc_rush_order">
<span class="status-fail" style="font-size: 11pt; margin-left: 12px;">
<i class="fa fa-bolt"/> RUSH ORDER
</span>
</t>
</div>
</div>
<!-- ===== 1. JOB HEADER - Customer / PO / Part / Qty / Dates ===== -->
<table class="bordered">
<thead>
<tr><th colspan="6" class="fp-header-primary">JOB HEADER</th></tr>
</thead>
<tbody>
<tr>
<th class="info-header" style="width: 15%;">Customer</th>
<td style="width: 20%;"><span t-field="mo.product_id.product_tmpl_id"/>
<t t-if="so"><br/><span t-field="so.partner_id"/></t>
</td>
<th class="info-header" style="width: 10%;">Sale Order</th>
<td class="text-center" style="width: 15%;">
<t t-if="so"><span t-field="so.name"/></t>
<t t-else="">-</t>
</td>
<th class="info-header" style="width: 10%;">Customer PO #</th>
<td class="text-center" style="width: 15%;">
<t t-if="so and so.x_fc_po_number"><span t-field="so.x_fc_po_number"/></t>
<t t-else="">-</t>
</td>
</tr>
<tr>
<th class="info-header">Part Number</th>
<td>
<!-- Sub 2: prefer the linked part catalog (part_number + rev);
fall back to SO-level catalog ref, then product default_code. -->
<t t-set="_line" t-value="(mo.x_fc_sale_order_line_ids.filtered(lambda l: l.product_id == mo.product_id)[:1] or mo.x_fc_sale_order_line_ids[:1]) if 'x_fc_sale_order_line_ids' in mo._fields else mo.env['sale.order.line']"/>
<t t-if="(not _line) and so">
<t t-set="_line" t-value="so.order_line.filtered(lambda l: l.product_id == mo.product_id)[:1] or so.order_line[:1]"/>
</t>
<t t-set="_part" t-value="(_line.x_fc_part_catalog_id if (_line and 'x_fc_part_catalog_id' in _line._fields) else False) or (so.x_fc_part_catalog_id if so else False)"/>
<t t-if="_part">
<strong>
<span t-esc="_part.part_number or _part.name"/>
<t t-if="_part.revision">
<span> (Rev <span t-esc="_part.revision"/>)</span>
</t>
</strong>
</t>
<t t-else=""><span t-field="mo.product_id.default_code"/></t>
</td>
<th class="info-header">Product</th>
<td><span t-field="mo.product_id"/></td>
<th class="info-header">Quantity</th>
<td class="text-center">
<span t-esc="int(mo.product_qty)"/>
<span t-field="mo.product_uom_id"/>
</td>
</tr>
<tr>
<th class="info-header">Specification</th>
<td>
<t t-if="so and so.x_fc_customer_spec_id">
<span t-field="so.x_fc_customer_spec_id"/>
</t>
<t t-else="">-</t>
</td>
<th class="info-header">Recipe</th>
<td>
<t t-if="mo.x_fc_recipe_id"><span t-field="mo.x_fc_recipe_id"/></t>
<t t-else=""><span class="status-warning">No recipe assigned</span></t>
</td>
<th class="info-header">Facility</th>
<td><span t-field="mo.x_fc_facility_id"/></td>
</tr>
<tr>
<th class="info-header">Date Planned</th>
<td class="text-center">
<t t-if="mo.date_start">
<span t-field="mo.date_start" t-options="{'widget': 'date'}"/>
</t>
<t t-else="">-</t>
</td>
<th class="info-header">Target Ship</th>
<td class="text-center">
<t t-if="job and job.target_ship_date">
<span t-field="job.target_ship_date"/>
</t>
<t t-elif="so and so.commitment_date">
<span t-field="so.commitment_date" t-options="{'widget': 'date'}"/>
</t>
<t t-else="">-</t>
</td>
<th class="info-header">Current Location</th>
<td class="text-center">
<span t-field="mo.x_fc_current_location"/>
</td>
</tr>
</tbody>
</table>
<!-- ===== 1b. PART DESCRIPTIONS (Sub 2) - internal report keeps the service
SKU visible while surfacing customer-facing + internal workflow text. -->
<t t-set="_trav_line" t-value="(mo.x_fc_sale_order_line_ids.filtered(lambda l: l.product_id == mo.product_id)[:1] or mo.x_fc_sale_order_line_ids[:1]) if 'x_fc_sale_order_line_ids' in mo._fields else mo.env['sale.order.line']"/>
<t t-if="(not _trav_line) and so">
<t t-set="_trav_line" t-value="so.order_line.filtered(lambda l: l.product_id == mo.product_id)[:1] or so.order_line[:1]"/>
</t>
<table class="bordered">
<thead>
<tr><th colspan="4" class="fp-header-primary">PART DESCRIPTIONS</th></tr>
</thead>
<tbody>
<tr>
<th class="info-header" style="width: 20%;">Customer-Facing Description</th>
<td style="width: 40%;">
<t t-if="_trav_line"><span t-esc="_trav_line.name or '-'"/></t>
<t t-else="">-</t>
</td>
<th class="info-header" style="width: 15%;">Service SKU</th>
<td style="width: 25%;"><span t-esc="mo.product_id.default_code or '-'"/></td>
</tr>
<tr>
<th class="info-header">Internal Description / Workflow</th>
<td colspan="3">
<t t-if="_trav_line and 'x_fc_internal_description' in _trav_line._fields and _trav_line.x_fc_internal_description">
<span t-esc="_trav_line.x_fc_internal_description" style="white-space: pre-wrap;"/>
</t>
<t t-else="">-</t>
</td>
</tr>
</tbody>
</table>
<!-- ===== 2. RECEIVING ===== -->
<table class="bordered">
<thead>
<tr><th colspan="5" class="fp-header-primary">RECEIVING</th></tr>
</thead>
<tbody>
<tr>
<th class="info-header" style="width: 15%;">Ref</th>
<th class="info-header" style="width: 15%;">Received On</th>
<th class="info-header" style="width: 15%;">Qty Received</th>
<th class="info-header" style="width: 15%;">Status</th>
<th class="info-header" style="width: 40%;">Damage / Notes</th>
</tr>
<t t-if="receivings">
<tr t-foreach="receivings" t-as="rec">
<td class="text-center"><span t-field="rec.name"/></td>
<td class="text-center">
<t t-if="rec.received_date">
<span t-field="rec.received_date" t-options="{'widget': 'datetime'}"/>
</t>
<t t-else="">-</t>
</td>
<td class="text-center">
<t t-if="'quantity_received' in rec._fields"><span t-esc="rec.quantity_received"/></t>
<t t-else="">-</t>
</td>
<td class="text-center"><span t-field="rec.state"/></td>
<td>
<t t-if="'damage_ids' in rec._fields and rec.damage_ids">
<span class="status-warning">
<i class="fa fa-warning"/> <span t-esc="len(rec.damage_ids)"/> damage entries
</span>
</t>
<t t-else="">-</t>
</td>
</tr>
</t>
<t t-else="">
<tr>
<td colspan="5" class="text-center note-row">No receiving record found.</td>
</tr>
</t>
</tbody>
</table>
<!-- ===== 3. ROUTING TABLE - the main event ===== -->
<table class="bordered" style="margin-top: 12px;">
<thead>
<tr><th colspan="12" class="fp-header-primary">PROCESS ROUTING</th></tr>
<tr>
<th style="width: 4%;">#</th>
<th style="width: 16%;">Operation</th>
<th style="width: 11%;">Work Centre</th>
<th style="width: 9%;">Bath</th>
<th style="width: 7%;">Tank</th>
<th style="width: 7%;">Target Thk</th>
<th style="width: 5%;">Dwell</th>
<th style="width: 6%;">Exp. Dur.</th>
<th style="width: 8%;">Operator</th>
<th style="width: 9%;">Date / Time</th>
<th style="width: 5%;">Initials</th>
<th style="width: 13%;">Qty Pass / Reject</th>
</tr>
</thead>
<tbody>
<t t-if="wos">
<tr t-foreach="wos" t-as="wo">
<td class="text-center"><span t-esc="wo_index + 1"/></td>
<td><span t-field="wo.name"/></td>
<td><span t-field="wo.workcenter_id"/></td>
<td><span t-field="wo.x_fc_bath_id"/></td>
<td class="text-center"><span t-field="wo.x_fc_tank_id"/></td>
<td class="text-center">
<t t-if="wo.x_fc_thickness_target">
<span t-esc="wo.x_fc_thickness_target"/>
<span t-esc="dict(wo._fields['x_fc_thickness_uom'].selection).get(wo.x_fc_thickness_uom, '')"/>
</t>
<t t-else="">-</t>
</td>
<td class="text-center">
<t t-if="wo.x_fc_dwell_time_minutes">
<span t-esc="wo.x_fc_dwell_time_minutes"/>m
</t>
<t t-else="">-</t>
</td>
<td class="text-center">
<t t-if="wo.duration_expected">
<span t-esc="'%.0f' % wo.duration_expected"/>m
</t>
<t t-else="">-</t>
</td>
<td class="sig-line"/>
<td class="sig-line"/>
<td class="sig-line"/>
<td class="sig-line"/>
</tr>
</t>
<t t-else="">
<tr>
<td colspan="12" class="text-center note-row">
No work orders generated yet. Assign a recipe and confirm the MO to generate the routing.
</td>
</tr>
</t>
</tbody>
</table>
<!-- ===== 4. BATH CHEMISTRY TARGETS ===== -->
<t t-set="baths" t-value="wos.mapped('x_fc_bath_id').filtered(lambda b: b.target_line_ids)"/>
<t t-if="baths">
<table class="bordered" style="margin-top: 12px;">
<thead>
<tr><th colspan="5" class="fp-header-primary">BATH CHEMISTRY TARGETS</th></tr>
<tr>
<th style="width: 25%;">Bath</th>
<th style="width: 30%;">Parameter</th>
<th style="width: 15%;">Min</th>
<th style="width: 15%;">Max</th>
<th style="width: 15%;">UOM</th>
</tr>
</thead>
<tbody>
<t t-foreach="baths" t-as="bath">
<t t-foreach="bath.target_line_ids" t-as="p">
<tr>
<td t-if="p_index == 0" t-attf-rowspan="{{ len(bath.target_line_ids) }}">
<span t-field="bath.name"/>
</td>
<td><span t-field="p.parameter_id"/></td>
<td class="text-center"><span t-esc="p.target_min or '-'"/></td>
<td class="text-center"><span t-esc="p.target_max or '-'"/></td>
<td class="text-center"><span t-esc="p.uom or '-'"/></td>
</tr>
</t>
</t>
</tbody>
</table>
</t>
<!-- ===== 5. QUALITY HOLDS - only if present ===== -->
<t t-if="holds">
<div class="highlight-box" style="border-color: #c62828; background-color: #ffebee; margin-top: 12px;">
<strong class="status-fail">
<i class="fa fa-exclamation-triangle"/>
<span t-esc="len(holds)"/> QUALITY HOLD(S) ON THIS JOB
</strong>
<table class="bordered" style="margin-top: 8px; margin-bottom: 0;">
<thead>
<tr>
<th style="width: 12%;">Ref</th>
<th style="width: 13%;">Reason</th>
<th style="width: 10%;">Qty</th>
<th style="width: 10%;">State</th>
<th style="width: 15%;">Operator</th>
<th style="width: 40%;">Description</th>
</tr>
</thead>
<tbody>
<tr t-foreach="holds" t-as="hold">
<td class="text-center"><span t-field="hold.name"/></td>
<td><span t-field="hold.hold_reason"/></td>
<td class="text-center"><span t-field="hold.qty_on_hold"/></td>
<td class="text-center"><span t-field="hold.state"/></td>
<td><span t-field="hold.operator_id"/></td>
<td><span t-field="hold.description"/></td>
</tr>
</tbody>
</table>
</div>
</t>
<!-- ===== 6. CERTIFICATES + DELIVERY - side by side ===== -->
<table style="margin-top: 12px; border: none;">
<tr style="border: none;">
<td style="width: 50%; padding-right: 6px; border: none; vertical-align: top;">
<table class="bordered">
<thead>
<tr><th colspan="3" class="fp-header-primary">CERTIFICATES ISSUED</th></tr>
<tr>
<th style="width: 40%;">Reference</th>
<th style="width: 30%;">Type</th>
<th style="width: 30%;">Status</th>
</tr>
</thead>
<tbody>
<t t-if="certs">
<tr t-foreach="certs" t-as="c">
<td class="text-center"><span t-field="c.name"/></td>
<td class="text-center">
<span t-esc="dict(c._fields['certificate_type'].selection).get(c.certificate_type, c.certificate_type)"/>
</td>
<td class="text-center"><span t-field="c.state"/></td>
</tr>
</t>
<t t-else="">
<tr><td colspan="3" class="text-center note-row">None issued yet.</td></tr>
</t>
</tbody>
</table>
</td>
<td style="width: 50%; padding-left: 6px; border: none; vertical-align: top;">
<table class="bordered">
<thead>
<tr><th colspan="4" class="fp-header-primary">DELIVERY</th></tr>
<tr>
<th style="width: 30%;">Ref</th>
<th style="width: 25%;">State</th>
<th style="width: 20%;">Driver</th>
<th style="width: 25%;">Tracking</th>
</tr>
</thead>
<tbody>
<t t-if="deliveries">
<tr t-foreach="deliveries" t-as="dlv">
<td class="text-center"><span t-field="dlv.name"/></td>
<td class="text-center"><span t-field="dlv.state"/></td>
<td class="text-center">
<t t-if="dlv.assigned_driver_id"><span t-field="dlv.assigned_driver_id"/></t>
<t t-else="">-</t>
</td>
<td class="text-center">
<t t-if="'tracking_ref' in dlv._fields and dlv.tracking_ref">
<span t-field="dlv.tracking_ref"/>
</t>
<t t-else="">-</t>
</td>
</tr>
</t>
<t t-else="">
<tr><td colspan="4" class="text-center note-row">No delivery scheduled yet.</td></tr>
</t>
</tbody>
</table>
</td>
</tr>
</table>
<!-- ===== 7. REWORK REASON - only if rework ===== -->
<t t-if="mo.x_fc_is_rework and mo.x_fc_rework_reason">
<div class="highlight-box" style="margin-top: 12px;">
<strong>Rework Reason:</strong>
<div style="margin-top: 4px;"><span t-field="mo.x_fc_rework_reason"/></div>
</div>
</t>
<!-- ===== 8. NOTES / EXCEPTIONS (blank ruled area) ===== -->
<div style="margin-top: 14px;">
<strong>Shop Floor Notes / Exceptions:</strong>
<div style="border: 1px solid #000; min-height: 90px; margin-top: 4px; background-image: linear-gradient(to bottom, transparent 29px, #ccc 29px, #ccc 30px, transparent 30px); background-size: 100% 30px;"/>
</div>
<!-- ===== 9. FINAL SIGN-OFF ===== -->
<table class="bordered" style="margin-top: 14px;">
<thead>
<tr>
<th colspan="4" class="fp-header-primary">JOB COMPLETION SIGN-OFF</th>
</tr>
<tr>
<th style="width: 30%;">Shop Supervisor</th>
<th style="width: 20%;">Date</th>
<th style="width: 30%;">QA Inspector</th>
<th style="width: 20%;">Date</th>
</tr>
</thead>
<tbody>
<tr>
<td class="sig-line"/>
<td class="sig-line"/>
<td class="sig-line"/>
<td class="sig-line"/>
</tr>
</tbody>
</table>
<p class="small-muted" style="margin-top: 12px; text-align: right;">
Generated <span t-esc="mo.env.cr.now()" t-options="{'widget': 'datetime'}"/>
- This traveller must remain with the parts through all operations.
</p>
</template>
<!-- ============================================================= -->
<!-- MO-based Traveller - LANDSCAPE (default) -->
<!-- ============================================================= -->
<template id="report_fp_job_traveller_mo_landscape">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="mo">
<t t-call="web.external_layout">
<t t-set="doc" t-value="mo"/>
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<t t-call="fusion_plating_reports.report_fp_job_traveller_body"/>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- MO-based Traveller - PORTRAIT -->
<!-- ============================================================= -->
<template id="report_fp_job_traveller_mo_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="mo">
<t t-call="web.external_layout">
<t t-set="doc" t-value="mo"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<div class="page">
<t t-call="fusion_plating_reports.report_fp_job_traveller_body"/>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- SO-based Traveller - iterates the SO's MOs -->
<!-- Landscape default -->
<!-- ============================================================= -->
<template id="report_fp_job_traveller_so_landscape">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="so">
<!-- Sub 11 - MRP gone; find fp.jobs for this SO and delegate
to the native fp.job traveller template (jobs module). -->
<t t-set="jobs" t-value="so.env['fp.job'].search([('sale_order_id', '=', so.id)])"/>
<t t-if="not jobs">
<t t-call="web.external_layout">
<t t-set="doc" t-value="so"/>
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2>Job Traveller - <span t-field="so.name"/></h2>
<div class="highlight-box">
<strong class="status-warning">
<i class="fa fa-info-circle"/>
No plating job has been generated for this Sale Order yet.
</strong>
</div>
</div>
</div>
</t>
</t>
<t t-foreach="jobs" t-as="job">
<t t-call="web.external_layout">
<div class="page">
<h1>Job Traveller - <span t-esc="job.name"/></h1>
<table class="table table-sm" style="margin-top: 1em;">
<tr><th>Customer</th><td><span t-esc="job.partner_id.name"/></td></tr>
<tr><th>SO</th><td><span t-esc="job.sale_order_id.name or '-'"/></td></tr>
<tr><th>Qty</th><td><span t-esc="job.qty"/></td></tr>
<tr><th>Recipe</th><td><span t-esc="job.recipe_id.name or '-'"/></td></tr>
<tr><th>Deadline</th><td><span t-esc="job.date_deadline and job.date_deadline.strftime('%b %d, %Y') or '-'"/></td></tr>
<tr><th>Status</th><td><span t-esc="job.state"/></td></tr>
</table>
<h3 style="margin-top: 1.5em;">Steps</h3>
<table class="table table-sm">
<thead>
<tr><th>#</th><th>Step</th><th>Work Centre</th><th>Kind</th><th>State</th></tr>
</thead>
<tbody>
<tr t-foreach="job.step_ids.sorted('sequence')" t-as="step">
<td><span t-esc="step.sequence"/></td>
<td><span t-esc="step.name"/></td>
<td><span t-esc="step.work_centre_id.name or '-'"/></td>
<td><span t-esc="step.kind or '-'"/></td>
<td><span t-esc="step.state"/></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- SO-based Traveller - PORTRAIT -->
<!-- ============================================================= -->
<template id="report_fp_job_traveller_so_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="so">
<t t-set="jobs" t-value="so.env['fp.job'].search([('sale_order_id', '=', so.id)])"/>
<t t-if="not jobs">
<t t-call="web.external_layout">
<t t-set="doc" t-value="so"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<div class="page">
<h4>Job Traveller - <span t-field="so.name"/></h4>
<div class="highlight-box">
<strong class="status-warning">
<i class="fa fa-info-circle"/>
No plating job has been generated for this Sale Order yet.
</strong>
</div>
</div>
</div>
</t>
</t>
<t t-foreach="jobs" t-as="job">
<t t-call="web.external_layout">
<div class="page">
<h2>Job Traveller - <span t-esc="job.name"/></h2>
<table class="table table-sm" style="margin-top: 1em;">
<tr><th>Customer</th><td><span t-esc="job.partner_id.name"/></td></tr>
<tr><th>SO</th><td><span t-esc="job.sale_order_id.name or '-'"/></td></tr>
<tr><th>Qty</th><td><span t-esc="job.qty"/></td></tr>
<tr><th>Recipe</th><td><span t-esc="job.recipe_id.name or '-'"/></td></tr>
<tr><th>Deadline</th><td><span t-esc="job.date_deadline and job.date_deadline.strftime('%b %d, %Y') or '-'"/></td></tr>
<tr><th>Status</th><td><span t-esc="job.state"/></td></tr>
</table>
<h3 style="margin-top: 1.5em;">Steps</h3>
<table class="table table-sm">
<thead>
<tr><th>#</th><th>Step</th><th>Work Centre</th><th>Kind</th><th>State</th></tr>
</thead>
<tbody>
<tr t-foreach="job.step_ids.sorted('sequence')" t-as="step">
<td><span t-esc="step.sequence"/></td>
<td><span t-esc="step.name"/></td>
<td><span t-esc="step.work_centre_id.name or '-'"/></td>
<td><span t-esc="step.kind or '-'"/></td>
<td><span t-esc="step.state"/></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,731 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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. 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>
/* CoC pattern: paperformat_fp_a4_portrait reserves only
margin_top=8mm; the body padding-top clears the header
band. Same trick as .fp-coc and .fp-sale. */
.fp-report.fp-ps .page { padding-top: 20mm; }
/* Title bar: float div layout (NO table - see CLAUDE.md
wkhtmltopdf overlap §2). Stacked bilingual title with
English bold on top and French italic-grey below. */
.fp-ps-titlebar { margin: 0 0 10px 0; padding: 0; overflow: hidden; }
.fp-ps-title-en { font-size: 18pt; font-weight: bold; color: #2e2e2e; line-height: 1.1; display: block; }
.fp-ps-title-fr { font-size: 13pt; font-style: italic; color: #555; line-height: 1.1; display: block; margin-top: 2px; }
.fp-ps-title-num { font-weight: bold; margin-left: 6px; }
/* Code128 barcode block (top-right of title bar). Same
sizing/centering as the SO confirmation; encodes the
packing-slip number so the printed label matches the
visible title. */
.fp-ps-barcode { float: right; margin-left: 12px; }
.fp-ps-barcode .fp-bc-wrap { display: inline-block; text-align: center; }
.fp-ps-barcode img { height: 48px; max-width: 240px; border: 0 !important; padding: 0; display: block; }
.fp-ps-barcode .fp-bc-label { font-size: 10pt; color: #333; margin-top: 6px; letter-spacing: 0.5px; }
.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).
NOTE: `t-field` in Odoo 19 requires a dotted path
("record.field_name") - passing a bare `partner` variable
via t-set and then `t-field="partner"` raises
`AssertionError: t-field must have at least a dot`.
The contact-widget pattern (`t-field="x.partner_id"
t-options="{'widget': 'contact', ...}"`) only works when
x.partner_id is a real field traversal on the document.
So we render the address parts manually with `t-esc` and
per-field guards. -->
<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-if="partner.street"><span t-esc="partner.street"/></div>
<div t-if="partner.street2"><span t-esc="partner.street2"/></div>
<div t-if="partner.city or partner.state_id or partner.zip">
<t t-if="partner.city"><span t-esc="partner.city"/></t><t t-if="partner.state_id">, <span t-esc="partner.state_id.code or partner.state_id.name"/></t><t t-if="partner.zip"> <span t-esc="partner.zip"/></t>
</div>
<div t-if="partner.country_id"><span t-esc="partner.country_id.name"/></div>
<div t-if="partner.phone"><span t-esc="partner.phone"/></div>
<div t-if="partner.email"><span t-esc="partner.email"/></div>
</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" 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 -->
<!-- ============================================================= -->
<template id="report_fp_packing_slip_portrait">
<t t-call="web.html_container">
<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 '')"/>
<!-- Packing slip number derived from the SO so it
matches the order/work order number: SO-30045
→ "30045". When the SO has multiple outbound
pickings (partial shipments), the 2nd onwards
get a -NN suffix: "30045-02", "30045-03". The
first/only picking is just the bare number.
Falls back to picking name (WH/OUT/00168) for
pickings without a linked SO. -->
<t t-set="so_name_raw" t-value="doc.sale_id.name if doc.sale_id else doc.name"/>
<t t-set="ps_base_num" t-value="so_name_raw.rsplit('-', 1)[-1] if '-' in so_name_raw else so_name_raw"/>
<t t-set="outbound_pickings" t-value="doc.sale_id.picking_ids.filtered(lambda p: p.picking_type_id and p.picking_type_id.code == 'outgoing') if doc.sale_id else doc"/>
<t t-set="picking_position" t-value="(sorted(outbound_pickings.ids).index(doc.id) + 1) if (doc.sale_id and doc.id in outbound_pickings.ids) else 1"/>
<t t-set="ps_number" t-value="ps_base_num if picking_position &lt;= 1 else ('%s-%02d' % (ps_base_num, picking_position))"/>
<!-- Code128 barcode encoding the packing-slip number so
the printed label matches the visible title. Inlined
via barcode_data_uri (no /report/barcode/ HTTP fetch
- wkhtmltopdf network calls fail on entech). -->
<t t-set="ps_barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', ps_number, 600, 100) if ps_number else False"/>
<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 fp-ps">
<div class="page">
<!-- Bilingual title (EN bold on top, FR
italic-grey below) matching SO and CoC. -->
<div class="fp-ps-titlebar">
<t t-if="ps_barcode_uri">
<div class="fp-ps-barcode">
<div class="fp-bc-wrap">
<img t-att-src="ps_barcode_uri" alt="Packing Slip Barcode"/>
<div class="fp-bc-label"><t t-esc="ps_number"/></div>
</div>
</div>
</t>
<span class="fp-ps-title-en">
Packing Slip<span class="fp-ps-title-num"># <t t-esc="ps_number"/></span>
</span>
<span class="fp-ps-title-fr">Bordereau d'expédition</span>
</div>
<!-- Bill To / Ship To -->
<table class="bordered fp-ps-addrtable">
<tbody>
<tr>
<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>
</td>
</tr>
</tbody>
</table>
<!-- Ship details -->
<table class="bordered fp-ps-info-table">
<thead>
<tr>
<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><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;">
<strong>Notes:</strong>
<div t-field="doc.note"/>
</div>
</t>
<!-- Sign-off + QR -->
<t t-call="fusion_plating_reports.fp_packing_slip_signoff"/>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- LANDSCAPE -->
<!-- ============================================================= -->
<template id="report_fp_packing_slip_landscape">
<t t-call="web.html_container">
<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 '')"/>
<!-- See portrait template for the numbering logic. -->
<t t-set="so_name_raw" t-value="doc.sale_id.name if doc.sale_id else doc.name"/>
<t t-set="ps_base_num" t-value="so_name_raw.rsplit('-', 1)[-1] if '-' in so_name_raw else so_name_raw"/>
<t t-set="outbound_pickings" t-value="doc.sale_id.picking_ids.filtered(lambda p: p.picking_type_id and p.picking_type_id.code == 'outgoing') if doc.sale_id else doc"/>
<t t-set="picking_position" t-value="(sorted(outbound_pickings.ids).index(doc.id) + 1) if (doc.sale_id and doc.id in outbound_pickings.ids) else 1"/>
<t t-set="ps_number" t-value="ps_base_num if picking_position &lt;= 1 else ('%s-%02d' % (ps_base_num, picking_position))"/>
<!-- Code128 barcode encoding the packing-slip number so
the printed label matches the visible title. Inlined
via barcode_data_uri (no /report/barcode/ HTTP fetch
- wkhtmltopdf network calls fail on entech). -->
<t t-set="ps_barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', ps_number, 600, 100) if ps_number else False"/>
<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 fp-ps">
<div class="page">
<div class="fp-ps-titlebar">
<t t-if="ps_barcode_uri">
<div class="fp-ps-barcode">
<div class="fp-bc-wrap">
<img t-att-src="ps_barcode_uri" alt="Packing Slip Barcode"/>
<div class="fp-bc-label"><t t-esc="ps_number"/></div>
</div>
</div>
</t>
<span class="fp-ps-title-en">
Packing Slip<span class="fp-ps-title-num"># <t t-esc="ps_number"/></span>
</span>
<span class="fp-ps-title-fr">Bordereau d'expédition</span>
</div>
<!-- Bill To / Ship To -->
<table class="bordered fp-ps-addrtable">
<tbody>
<tr>
<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>
</td>
</tr>
</tbody>
</table>
<!-- Ship details -->
<table class="bordered fp-ps-info-table">
<thead>
<tr>
<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><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: 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">
<div style="margin-top: 10px;">
<strong>Notes:</strong>
<div t-field="doc.note"/>
</div>
</t>
<!-- Sign-off + QR -->
<t t-call="fusion_plating_reports.fp_packing_slip_signoff"/>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- LOCAL DELIVERY variant (fusion.plating.delivery) -->
<!-- The stock.picking templates above don't fit shops that ship -->
<!-- via fusion.plating.delivery (no picking). This variant -->
<!-- resolves the SO + lines from the delivery's job_ref (same -->
<!-- pattern as the BoL report) and reuses the shared styles / -->
<!-- address / signoff bits. Items come from the SO lines (vs -->
<!-- stock moves). -->
<!-- ============================================================= -->
<!-- Items table sourced from sale.order.line (not stock.move) -->
<template id="fp_packing_slip_items_lines">
<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="lines" t-as="line">
<t t-set="ordered_qty" t-value="line.product_uom_qty or 0.0"/>
<t t-set="wo_job" t-value="line.env['fp.job'].sudo().search([('sale_order_line_ids', 'in', line.ids)], limit=1)"/>
<t t-set="done_qty" t-value="(wo_job.qty_done if wo_job and wo_job.qty_done else ordered_qty)"/>
<t t-set="bo_qty" t-value="ordered_qty - done_qty if ordered_qty &gt; done_qty else 0.0"/>
<t t-set="proc_variant" t-value="(line.x_fc_process_variant_id if 'x_fc_process_variant_id' in line._fields else False)"/>
<t t-set="proc_label" t-value="(proc_variant.variant_label or proc_variant.name) if proc_variant else ((line.x_fc_part_catalog_id.default_process_id.variant_label or line.x_fc_part_catalog_id.default_process_id.name) if line.x_fc_part_catalog_id and line.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>
<template id="report_fp_packing_slip_delivery_portrait">
<t t-call="web.html_container">
<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"/>
<!-- Model-agnostic resolution. This ONE template backs
four report actions (sale.order, fp.job,
fp.receiving, fusion.plating.delivery) so the
packing slip prints from any of those screens.
Resolve the sale order + ship-to per doc type, then
render a common layout from the SO lines. (Template
id kept as "...delivery..." for back-compat with the
action + Python that reference it.) -->
<t t-set="m" t-value="doc._name"/>
<t t-set="_so" t-value="False"/>
<t t-if="m == 'sale.order'">
<t t-set="_so" t-value="doc"/>
</t>
<t t-elif="m == 'fp.job' or m == 'fp.receiving'">
<t t-set="_so" t-value="doc.sale_order_id"/>
</t>
<t t-elif="m == 'fusion.plating.delivery'">
<t t-set="_dlv_job" t-value="env['fp.job'].sudo().search([('name', '=', doc.job_ref)], limit=1) if doc.job_ref else env['fp.job']"/>
<t t-set="_so" t-value="_dlv_job.sale_order_id if _dlv_job else False"/>
</t>
<t t-set="_lines" t-value="_so.order_line.filtered(lambda l: l.product_id and not l.display_type and l.product_uom_qty &gt; 0) if _so else False"/>
<t t-set="ship_partner" t-value="(doc.delivery_address_id or doc.partner_id) if m == 'fusion.plating.delivery' else ((_so.partner_shipping_id or _so.partner_id) if _so else doc.partner_id)"/>
<t t-set="bill_partner" t-value="(_so.partner_invoice_id if _so and _so.partner_invoice_id else (ship_partner.commercial_partner_id or ship_partner))"/>
<t t-set="has_carrier" t-value="m == 'fusion.plating.delivery' and 'x_fc_carrier_id' in doc._fields and doc.x_fc_carrier_id"/>
<t t-set="ship_via" t-value="(doc.x_fc_carrier_id.name if has_carrier else (_so.x_fc_ship_via if _so and 'x_fc_ship_via' in _so._fields and _so.x_fc_ship_via else 'CUSTOMER PICKUP'))"/>
<t t-set="tracking_text" t-value="'Ready for pick up' if not has_carrier else '-'"/>
<t t-set="po_number" t-value="(_so.client_order_ref if _so and _so.client_order_ref else '')"/>
<t t-set="_scheduled" t-value="doc.scheduled_date if m == 'fusion.plating.delivery' else (doc.commitment_date if m == 'sale.order' else (_so.commitment_date if _so else False))"/>
<t t-set="_notes" t-value="doc.notes if m == 'fusion.plating.delivery' else (doc.note if m == 'sale.order' else False)"/>
<t t-set="so_name_raw" t-value="_so.name if _so else (doc.name or '')"/>
<t t-set="ps_number" t-value="so_name_raw.rsplit('-', 1)[-1] if '-' in so_name_raw else so_name_raw"/>
<t t-set="ps_barcode_uri" t-value="doc.env['ir.actions.report'].sudo().barcode_data_uri('Code128', ps_number, 600, 100) if ps_number else False"/>
<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 fp-ps">
<div class="page">
<div class="fp-ps-titlebar">
<t t-if="ps_barcode_uri">
<div class="fp-ps-barcode">
<div class="fp-bc-wrap">
<img t-att-src="ps_barcode_uri" alt="Packing Slip Barcode"/>
<div class="fp-bc-label"><t t-esc="ps_number"/></div>
</div>
</div>
</t>
<span class="fp-ps-title-en">
Packing Slip<span class="fp-ps-title-num"># <t t-esc="ps_number"/></span>
</span>
<span class="fp-ps-title-fr">Bordereau d'expédition</span>
</div>
<table class="bordered fp-ps-addrtable">
<tbody>
<tr>
<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>
</td>
</tr>
</tbody>
</table>
<table class="bordered fp-ps-info-table">
<thead>
<tr>
<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><span t-esc="ship_via"/></td>
<td>
<t t-if="_scheduled">
<span t-out="_scheduled" t-options="{'widget': 'date'}"/>
</t>
<t t-else="">-</t>
</td>
<td><span t-esc="tracking_text"/></td>
</tr>
</tbody>
</table>
<t t-if="_lines">
<t t-call="fusion_plating_reports.fp_packing_slip_items_lines">
<t t-set="lines" t-value="_lines"/>
</t>
</t>
<t t-else="">
<p style="margin-top: 10px; color: #555;">
No order lines found for this document
(<span t-esc="doc.name or ''"/>).
</p>
</t>
<t t-if="_notes">
<div style="margin-top: 10px;">
<strong>Notes:</strong>
<div t-out="_notes"/>
</div>
</t>
<t t-call="fusion_plating_reports.fp_packing_slip_signoff"/>
</div>
</div>
</t>
</t>
</t>
</template>
<record id="action_report_fp_packing_slip_delivery_portrait" model="ir.actions.report">
<field name="name">Packing Slip</field>
<field name="model">fusion.plating.delivery</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="print_report_name">'Packing Slip - %s' % (object.name or '')</field>
<field name="binding_model_id" ref="fusion_plating_logistics.model_fusion_plating_delivery"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
</record>
<!-- Same packing slip, exposed in the Print menu of the Sale Order,
Work Order (fp.job) and Receiving/Shipping (fp.receiving) screens.
All reuse the model-agnostic template above. -->
<record id="action_report_fp_packing_slip_so_portrait" model="ir.actions.report">
<field name="name">Packing Slip</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="print_report_name">'Packing Slip - %s' % (object.name or '')</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
</record>
<record id="action_report_fp_packing_slip_job_portrait" model="ir.actions.report">
<field name="name">Packing Slip</field>
<field name="model">fp.job</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="print_report_name">'Packing Slip - %s' % (object.name or '')</field>
<field name="binding_model_id" ref="fusion_plating.model_fp_job"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
</record>
<record id="action_report_fp_packing_slip_receiving_portrait" model="ir.actions.report">
<field name="name">Packing Slip</field>
<field name="model">fp.receiving</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="report_file">fusion_plating_reports.report_fp_packing_slip_delivery_portrait</field>
<field name="print_report_name">'Packing Slip - %s' % (object.name or '')</field>
<field name="binding_model_id" ref="fusion_plating_receiving.model_fp_receiving"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_a4_portrait"/>
</record>
</odoo>

View File

@@ -1,118 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Sub 12 Phase E - Monthly Quality Summary report.
On-demand from the Quality Dashboard. Counts by record type / severity /
customer. Overdue ageing buckets. CAPA effectiveness rate. Repeat-customer
flag (>2 NCRs same customer in 90 days). Run via menu action
`action_fp_quality_monthly_summary`.
-->
<odoo>
<template id="report_fp_quality_monthly_doc">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="company">
<t t-call="web.external_layout">
<div class="page" style="font-family: 'Helvetica Neue', Arial, sans-serif; color: #2b2b2b; font-size: 12px;">
<div style="border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px;">
<h1 style="margin: 0; font-size: 24px;">Monthly Quality Summary</h1>
<div style="font-size: 13px; color: #666; margin-top: 4px;">
<span t-out="company.name"/>
- <span t-out="data['period_label']"/>
</div>
</div>
<!-- Headline counts -->
<table style="width: 100%; border-collapse: collapse; margin-bottom: 24px;">
<thead>
<tr style="background: #eee;">
<th style="padding: 8px; text-align: left;">Record Type</th>
<th style="padding: 8px; text-align: right; width: 110px;">Opened (period)</th>
<th style="padding: 8px; text-align: right; width: 110px;">Closed (period)</th>
<th style="padding: 8px; text-align: right; width: 110px;">Open Total</th>
<th style="padding: 8px; text-align: right; width: 110px;">Overdue</th>
</tr>
</thead>
<tbody>
<tr t-foreach="data['by_type']" t-as="row">
<td style="padding: 6px 8px; border-bottom: 1px solid #eee;"><strong t-out="row['label']"/></td>
<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: right;"><span t-out="row['opened']"/></td>
<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: right;"><span t-out="row['closed']"/></td>
<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: right;"><span t-out="row['open_total']"/></td>
<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: right; color: #c33; font-weight: bold;" t-if="row['overdue'] &gt; 0"><span t-out="row['overdue']"/></td>
<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: right; color: #999;" t-else="">0</td>
</tr>
</tbody>
</table>
<!-- Severity breakdown -->
<h2 style="font-size: 14px; margin: 0 0 8px 0;">NCR Severity Breakdown</h2>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 24px;">
<thead>
<tr style="background: #eee;">
<th style="padding: 6px 8px; text-align: left;">Severity</th>
<th style="padding: 6px 8px; text-align: right; width: 100px;">Count</th>
</tr>
</thead>
<tbody>
<tr t-foreach="data['ncr_severity']" t-as="row">
<td style="padding: 6px 8px; border-bottom: 1px solid #eee;"><span t-out="row['label']"/></td>
<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: right;"><span t-out="row['count']"/></td>
</tr>
</tbody>
</table>
<!-- CAPA effectiveness -->
<h2 style="font-size: 14px; margin: 0 0 8px 0;">CAPA Effectiveness</h2>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 24px;">
<tr><td style="padding: 4px 8px; color: #666; width: 250px;">Closed in period</td><td style="padding: 4px 8px;"><span t-out="data['capa']['closed']"/></td></tr>
<tr><td style="padding: 4px 8px; color: #666;">Verified effective</td><td style="padding: 4px 8px;"><span t-out="data['capa']['effective']"/></td></tr>
<tr><td style="padding: 4px 8px; color: #666;">Not effective (re-opened follow-up)</td><td style="padding: 4px 8px; color: #c33;"><span t-out="data['capa']['not_effective']"/></td></tr>
<tr><td style="padding: 4px 8px; color: #666;">Effectiveness rate</td><td style="padding: 4px 8px;"><strong t-out="data['capa']['rate_pct']"/>%</td></tr>
</table>
<!-- Repeat customers -->
<h2 style="font-size: 14px; margin: 0 0 8px 0;">Repeat-Issue Customers (≥3 NCRs in 90 days)</h2>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #eee;">
<th style="padding: 6px 8px; text-align: left;">Customer</th>
<th style="padding: 6px 8px; text-align: right; width: 100px;">NCRs (90d)</th>
<th style="padding: 6px 8px; text-align: right; width: 100px;">Open RMAs</th>
</tr>
</thead>
<tbody>
<tr t-foreach="data['repeat_customers']" t-as="row">
<td style="padding: 6px 8px; border-bottom: 1px solid #eee;"><strong t-out="row['name']"/></td>
<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: right;"><span t-out="row['ncr_count']"/></td>
<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: right;"><span t-out="row['rma_count']"/></td>
</tr>
<tr t-if="not data['repeat_customers']">
<td colspan="3" style="padding: 10px; color: #666; font-style: italic;">No customers with ≥3 NCRs in the last 90 days. Nice work.</td>
</tr>
</tbody>
</table>
<p style="font-size: 11px; color: #666; margin-top: 28px; border-top: 1px solid #eee; padding-top: 8px;">
Generated <span t-out="data['generated_at']"/> for <span t-out="company.name"/>.
Data source: fusion.plating.* live tables. Run on-demand from Quality Dashboard.
</p>
</div>
</t>
</t>
</t>
</template>
<record id="action_report_fp_quality_monthly" model="ir.actions.report">
<field name="name">Monthly Quality Summary</field>
<field name="model">res.company</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_quality_monthly_doc</field>
<field name="report_file">fusion_plating_reports.report_fp_quality_monthly_doc</field>
<field name="binding_type">action</field>
<field name="print_report_name">'Quality-Monthly-' + (object.name or '').replace(' ', '-')</field>
</record>
</odoo>

View File

@@ -1,138 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Sub 12c+ - Rack Travel Ticket.
Closes the gap left by Sub 12b's Rack Parts dialog 'Save + Print'
button. Operator presses Save + Print → tablet opens
/web/report/pdf/fp.rack.travel/<rack_id> in a new tab → this report
renders.
Single-page A5 landscape, large fonts, big QR/Code128 barcode.
Designed to be physically attached to the rack itself so
downstream operators can scan and pull up the rack's parts list.
-->
<odoo>
<record id="paperformat_fp_rack_travel" model="report.paperformat">
<field name="name">FP Rack Travel - A5 landscape</field>
<field name="format">A5</field>
<field name="orientation">Landscape</field>
<field name="margin_top">8</field>
<field name="margin_bottom">8</field>
<field name="margin_left">8</field>
<field name="margin_right">8</field>
<field name="header_spacing">5</field>
<field name="dpi">90</field>
</record>
<record id="action_report_fp_rack_travel" model="ir.actions.report">
<field name="name">Rack Travel Ticket</field>
<field name="model">fusion.plating.rack</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_rack_travel_template</field>
<field name="report_file">fusion_plating_reports.report_fp_rack_travel_template</field>
<field name="print_report_name">'Rack-Travel-%s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="fusion_plating.model_fusion_plating_rack"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_rack_travel"/>
</record>
<template id="report_fp_rack_travel_template">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="rack">
<t t-call="web.basic_layout">
<t t-set="batches" t-value="env['fp.job.step'].search([('rack_id','=',rack.id)])"/>
<div class="page fp-rack-travel">
<style>
.fp-rack-travel { font-family: Arial, sans-serif; font-size: 11pt; color: #000; padding-top: 4mm; }
.fp-rack-travel h1 { font-size: 28pt; margin: 0; font-weight: bold; }
.fp-rack-travel .fp-rack-id { font-size: 36pt; font-weight: bold; }
.fp-rack-travel table.bordered,
.fp-rack-travel table.bordered th,
.fp-rack-travel table.bordered td { border: 2px solid #000; border-collapse: collapse; }
.fp-rack-travel table.bordered { width: 100%; margin-top: 6px; }
.fp-rack-travel table.bordered th { background: #ededed; padding: 4px 8px; font-size: 11pt; text-align: left; }
.fp-rack-travel table.bordered td { padding: 6px 8px; vertical-align: top; font-size: 11pt; }
.fp-rack-travel .fp-tag-chip { display: inline-block; padding: 2px 8px; margin-right: 4px;
border-radius: 999px; border: 1px solid #000; font-size: 10pt; font-weight: bold; }
</style>
<table style="width: 100%;">
<tr>
<td style="width: 60%; vertical-align: middle;">
<div style="font-size: 10pt; color: #666;">RACK TRAVEL TICKET</div>
<h1>Rack <span class="fp-rack-id" t-esc="rack.name"/></h1>
<div style="font-size: 10pt; margin-top: 4px;">
<strong>Type:</strong> <span t-esc="rack.rack_type"/>
&#160;·&#160;
<strong>State:</strong>
<t t-if="'racking_state' in rack._fields">
<span t-esc="dict(rack._fields['racking_state'].selection).get(rack.racking_state, rack.racking_state) or '-'"/>
</t>
<t t-else="">-</t>
</div>
<div style="margin-top: 6px;">
<t t-if="'tag_ids' in rack._fields">
<span t-foreach="rack.tag_ids" t-as="tag" t-key="tag.id"
class="fp-tag-chip">
<t t-esc="tag.name"/>
</span>
</t>
</div>
</td>
<td style="width: 40%; vertical-align: middle; text-align: right;">
<img t-att-src="'/report/barcode/Code128/FP-RACK:%s?humanreadable=1' % rack.name"
style="height: 22mm;"/>
</td>
</tr>
</table>
<table class="bordered">
<thead>
<tr>
<th style="width: 10%;">Qty</th>
<th style="width: 22%;">Part Number</th>
<th style="width: 18%;">Work Order</th>
<th style="width: 30%;">Customer</th>
<th style="width: 20%;">Current Step</th>
</tr>
</thead>
<tbody>
<t t-if="batches">
<tr t-foreach="batches" t-as="b">
<td><span t-esc="(b.qty_done or 0) - (b.qty_scrapped or 0)"/></td>
<td>
<t t-if="b.job_id and b.job_id.product_id">
<span t-esc="b.job_id.product_id.default_code or b.job_id.product_id.name or '-'"/>
</t>
<t t-else="">-</t>
</td>
<td><span t-esc="(b.job_id and b.job_id.name) or '-'"/></td>
<td><span t-esc="(b.job_id and b.job_id.partner_id and b.job_id.partner_id.name) or '-'"/></td>
<td><span t-esc="b.name or '-'"/></td>
</tr>
</t>
<t t-else="">
<tr>
<td colspan="5" style="text-align: center; color: #888;">
No part batches currently on this rack.
</td>
</tr>
</t>
</tbody>
</table>
<div style="margin-top: 8px; font-size: 9pt; color: #666;">
Printed <span t-esc="datetime.datetime.now().strftime('%Y-%m-%d %H:%M')"/>
&#160;·&#160; Operator scans FP-RACK:<span t-esc="rack.name"/> at any tablet to load this rack.
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,282 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Fusion Plating - Payment Receipt (Portrait + Landscape).
Binds to account.payment. Shows amount paid, method, reference,
applied invoices, and a "PAID" stamp.
-->
<odoo>
<!-- ============================================================= -->
<!-- PORTRAIT -->
<!-- ============================================================= -->
<template id="report_fp_receipt_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<div class="page">
<h4 style="text-align: center;">
PAYMENT RECEIPT
</h4>
<div class="text-center" style="text-align: center; margin-bottom: 15px;">
<strong>Receipt #: <span t-field="doc.name"/></strong>
</div>
<!-- Paid stamp -->
<div class="text-center" style="text-align: center; margin: 20px 0;">
<span class="paid-stamp">✓ RECEIVED</span>
</div>
<!-- From / Received by -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">RECEIVED FROM</th>
<th style="width: 50%;">RECEIVED BY</th>
</tr>
</thead>
<tbody>
<tr>
<td style="height: 70px;">
<strong><span t-field="doc.partner_id.name"/></strong><br/>
<div t-field="doc.partner_id"
t-options="{'widget': 'contact', 'fields': ['address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 70px;">
<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>
</tr>
</tbody>
</table>
<!-- Payment details -->
<table class="bordered">
<thead>
<tr>
<th colspan="2" class="fp-header-primary">PAYMENT DETAILS</th>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 40%; background-color: #f5f5f5; font-weight: bold;">Payment Date</td>
<td><span t-field="doc.date" t-options="{'widget': 'date'}"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Payment Method</td>
<td><span t-field="doc.payment_method_line_id"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Journal</td>
<td><span t-field="doc.journal_id"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Reference / Memo</td>
<td><span t-esc="doc.ref or doc.memo or '-'"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Amount</td>
<td style="font-size: 14pt;"><strong>
<span t-field="doc.amount" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</strong></td>
</tr>
</tbody>
</table>
<!-- Applied invoices -->
<t t-if="doc.reconciled_invoice_ids">
<table class="bordered">
<thead>
<tr>
<th colspan="3" class="fp-header-primary">APPLIED TO INVOICES</th>
</tr>
<tr>
<th style="width: 34%;">INVOICE #</th>
<th style="width: 33%;">DATE</th>
<th style="width: 33%;">AMOUNT</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.reconciled_invoice_ids" t-as="inv">
<tr>
<td class="text-center"><span t-field="inv.name"/></td>
<td class="text-center"><span t-field="inv.invoice_date"/></td>
<td class="text-end">
<span t-field="inv.amount_total" t-options='{"widget": "monetary", "display_currency": inv.currency_id}'/>
</td>
</tr>
</t>
</tbody>
</table>
</t>
<!-- Thank you -->
<div class="highlight-box" style="margin-top: 20px; text-align: center;">
Thank you for your payment. This receipt confirms that payment has
been received in full for the amount stated above.
</div>
<!-- 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">Received by (Signature)</div>
</div>
</div>
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Date</div>
</div>
</div>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- LANDSCAPE -->
<!-- ============================================================= -->
<template id="report_fp_receipt_landscape">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: center;">PAYMENT RECEIPT</h2>
<div class="text-center" style="text-align: center; margin-bottom: 15px;">
<strong>Receipt #: <span t-field="doc.name"/></strong>
</div>
<!-- Paid stamp -->
<div class="text-center" style="text-align: center; margin: 20px 0;">
<span class="paid-stamp">✓ RECEIVED</span>
</div>
<!-- From / Received by -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">RECEIVED FROM</th>
<th style="width: 50%;">RECEIVED BY</th>
</tr>
</thead>
<tbody>
<tr>
<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', 'email'], 'no_marker': True}"/>
</td>
<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>
</tr>
</tbody>
</table>
<!-- Payment details (wide) -->
<table class="bordered info-table">
<thead>
<tr>
<th>PAYMENT DATE</th>
<th>METHOD</th>
<th>JOURNAL</th>
<th>REFERENCE</th>
<th>CURRENCY</th>
<th>AMOUNT</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.payment_method_line_id"/></td>
<td class="text-center"><span t-field="doc.journal_id"/></td>
<td class="text-center"><span t-esc="doc.ref or doc.memo or '-'"/></td>
<td class="text-center"><span t-field="doc.currency_id.name"/></td>
<td class="text-end" style="font-size: 13pt;"><strong>
<span t-field="doc.amount" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</strong></td>
</tr>
</tbody>
</table>
<!-- Applied invoices -->
<t t-if="doc.reconciled_invoice_ids">
<table class="bordered">
<thead>
<tr>
<th colspan="5" class="fp-header-primary">APPLIED TO INVOICES</th>
</tr>
<tr>
<th style="width: 20%;">INVOICE #</th>
<th style="width: 20%;">DATE</th>
<th style="width: 20%;">DUE DATE</th>
<th style="width: 20%;">TOTAL</th>
<th style="width: 20%;">PAYMENT STATE</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.reconciled_invoice_ids" t-as="inv">
<tr>
<td class="text-center"><span t-field="inv.name"/></td>
<td class="text-center"><span t-field="inv.invoice_date"/></td>
<td class="text-center"><span t-field="inv.invoice_date_due"/></td>
<td class="text-end">
<span t-field="inv.amount_total" t-options='{"widget": "monetary", "display_currency": inv.currency_id}'/>
</td>
<td class="text-center">
<span t-if="inv.payment_state == 'paid'" class="status-ok">Paid</span>
<span t-elif="inv.payment_state == 'partial'" class="status-warning">Partial</span>
<span t-else=""><span t-field="inv.payment_state"/></span>
</td>
</tr>
</t>
</tbody>
</table>
</t>
<!-- Thank you -->
<div class="highlight-box" style="margin-top: 20px; text-align: center;">
Thank you for your payment. This receipt confirms that payment has
been received in full for the amount stated above.
</div>
<!-- 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">Received by (Signature)</div>
</div>
</div>
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Date</div>
</div>
</div>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,141 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Sub 12 Phase E - RMA Authorisation PDF.
Single-page customer-facing PDF emailed when an RMA is authorised.
Contains: our header, customer info, RMA number, parts table, return
address, QR code linking to /fp/rma/<id>, and carrier instructions.
-->
<odoo>
<template id="report_fp_rma_authorisation_doc">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="rma">
<t t-call="web.external_layout">
<div class="page" style="font-family: 'Helvetica Neue', Arial, sans-serif; color: #2b2b2b;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px;">
<div>
<h1 style="margin: 0; font-size: 26px;">Return Material Authorisation</h1>
<p style="margin: 4px 0 0 0; font-size: 14px; color: #666;">
EN Technologies - Plating &amp; Metal Finishing
</p>
</div>
<div style="text-align: right;">
<div style="font-size: 22px; font-weight: bold;">
<t t-esc="rma.name"/>
</div>
<div style="font-size: 12px; color: #666;">
Issued <span t-out="rma.create_date" t-options='{"widget": "date"}'/>
</div>
</div>
</div>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 18px;">
<tr>
<td style="vertical-align: top; width: 60%;">
<h3 style="margin: 0 0 6px 0; font-size: 14px;">Customer</h3>
<div style="font-weight: bold;"><span t-out="rma.partner_id.name"/></div>
<div t-if="rma.partner_id.street"><span t-out="rma.partner_id.street"/></div>
<div t-if="rma.partner_id.city">
<span t-out="rma.partner_id.city"/><t t-if="rma.partner_id.state_id">, <span t-out="rma.partner_id.state_id.name"/></t>
<t t-if="rma.partner_id.zip"> <span t-out="rma.partner_id.zip"/></t>
</div>
<div t-if="rma.partner_id.country_id"><span t-out="rma.partner_id.country_id.name"/></div>
</td>
<td style="vertical-align: top; width: 40%;">
<h3 style="margin: 0 0 6px 0; font-size: 14px;">Return Details</h3>
<table style="width: 100%; font-size: 12px;">
<tr><td style="color: #666;">Original Order</td><td><span t-out="rma.sale_order_id.name"/></td></tr>
<tr><td style="color: #666;">Trigger</td><td><span t-field="rma.trigger_source"/></td></tr>
<tr><td style="color: #666;">Severity</td><td><span t-field="rma.severity"/></td></tr>
<tr><td style="color: #666;">Qty Returning</td><td><span t-out="rma.qty_returned"/></td></tr>
</table>
</td>
</tr>
</table>
<h3 style="font-size: 14px; margin-bottom: 6px;">Returned Lines</h3>
<table style="width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 18px;">
<thead>
<tr style="background: #f3f3f3;">
<th style="text-align: left; padding: 6px; border-bottom: 1px solid #ccc;">Part #</th>
<th style="text-align: left; padding: 6px; border-bottom: 1px solid #ccc;">Description</th>
<th style="text-align: right; padding: 6px; border-bottom: 1px solid #ccc; width: 70px;">Ordered</th>
</tr>
</thead>
<tbody>
<tr t-foreach="rma.sale_order_line_ids" t-as="line">
<td style="padding: 6px; border-bottom: 1px solid #eee;">
<span t-out="line.product_id.default_code or line.product_id.name"/>
</td>
<td style="padding: 6px; border-bottom: 1px solid #eee;">
<span t-out="line.name"/>
</td>
<td style="padding: 6px; border-bottom: 1px solid #eee; text-align: right;">
<span t-out="line.product_uom_qty"/>
</td>
</tr>
</tbody>
</table>
<div t-if="rma.complaint_description" style="margin-bottom: 18px;">
<h3 style="font-size: 14px; margin-bottom: 6px;">Customer Complaint</h3>
<div style="font-size: 12px; padding: 10px; background: #fafafa; border-left: 3px solid #ddd;">
<t t-out="rma.complaint_description"/>
</div>
</div>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="vertical-align: top; width: 65%;">
<h3 style="font-size: 14px; margin-bottom: 6px;">Return Shipping Instructions</h3>
<ol style="font-size: 12px; line-height: 1.5;">
<li>Print this authorisation and include it with your shipment.</li>
<li>Pack returned parts in their <strong>original boxes</strong> (we ship back in the same boxes per shop policy).</li>
<li>Mark each box clearly with the RMA number <strong t-out="rma.name"/>.</li>
<li>Ship to the address below - pre-paid carrier of your choice.</li>
<li>Send your tracking number to your account contact so we can monitor the return.</li>
</ol>
<p style="font-size: 12px; margin-top: 12px;"><strong>Return Address:</strong></p>
<p style="font-size: 12px; margin-left: 14px;">
EN Technologies<br/>
Receiving - RMA <span t-out="rma.name"/><br/>
[shop street address]<br/>
[city, province, postal code]
</p>
</td>
<td style="vertical-align: top; text-align: center; padding-left: 20px;">
<div t-if="rma.qr_code">
<img t-att-src="image_data_uri(rma.qr_code)"
style="width: 140px; height: 140px;"/>
<div style="font-size: 10px; color: #666; margin-top: 4px;">
Scan to track this RMA
</div>
</div>
</td>
</tr>
</table>
<p style="font-size: 11px; color: #666; margin-top: 30px; border-top: 1px solid #eee; padding-top: 8px;">
This authorisation is valid for 60 days from the issue date. Returns received without an RMA number will not be processed. Questions? Reply to the email this PDF was attached to or call your account contact.
</p>
</div>
</t>
</t>
</t>
</template>
<record id="action_report_fp_rma_authorisation" model="ir.actions.report">
<field name="name">RMA Authorisation</field>
<field name="model">fusion.plating.rma</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_rma_authorisation_doc</field>
<field name="report_file">fusion_plating_reports.report_fp_rma_authorisation_doc</field>
<field name="binding_model_id" ref="fusion_plating_quality.model_fusion_plating_rma"/>
<field name="binding_type">report</field>
<field name="print_report_name">'RMA-' + (object.name or '').replace('/', '-')</field>
</record>
</odoo>

View File

@@ -1,785 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Fusion Plating - Quotation / Sales Order (Portrait + Landscape)
Renders the same sale.order with a title that flips between
"Quotation" (draft/sent) and "Sales Order" (confirmed/done).
-->
<odoo>
<!-- ============================================================= -->
<!-- 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. -->
<!-- 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
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; }
/* 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",
so the only reliable fix is to avoid the table element. */
.fp-sale-titlebar { margin: 0 0 10px 0; padding: 0; overflow: hidden; }
/* Stacked title: English bold/large on top, French italic/
grey below. Sizes picked so the French line is roughly
the same horizontal width as the English line (English
is shorter character-wise but rendered larger). */
.fp-sale-title-en { font-size: 18pt; font-weight: bold; color: #2e2e2e; line-height: 1.1; display: block; }
.fp-sale-title-fr { font-size: 13pt; font-style: italic; color: #555; line-height: 1.1; display: block; margin-top: 2px; }
.fp-sale-title-num { font-weight: bold; margin-left: 6px; }
/* Barcode: bigger so it scans reliably. Wrap the img + label
in an inline-block so the label centers under the barcode
(not under the full floated column). Explicit no-border on
the img (wkhtmltopdf adds a 1px frame to inline-data img
elements by default on entech). */
.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: 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">
<!-- 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 ''"/>
<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">
<!-- 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">&#160;&#160;&#160;</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">
<thead>
<tr>
<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>
<tr>
<td style="height: 70px;">
<div t-field="doc.partner_invoice_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 70px;">
<div t-field="doc.partner_shipping_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
</td>
</tr>
</tbody>
</table>
<!-- 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: 25%;">
<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: 25%;">
<span class="fp-bl-en-stk">Salesperson</span>
<span class="fp-bl-fr-stk">Vendeur</span>
</th>
<th class="info-header" style="width: 25%;">
<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: 25%;">
<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.user_id"/></td>
<td class="text-center"><span t-esc="doc.x_fc_po_number or '-'"/></td>
<td class="text-center">
<!-- 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>
</td>
</tr>
</tbody>
</table>
<!-- Row 2: 2 wide cells (50% each) - inline. -->
<t t-if="doc.x_fc_customer_job_number or delivery_method_label">
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 50%;">
<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: 50%;">
<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="doc.x_fc_customer_job_number or '-'"/></td>
<td class="text-center"><span t-esc="delivery_method_label or '-'"/></td>
</tr>
</tbody>
</table>
</t>
<!-- 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 / 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 / Expéditions partielles bloquées.</strong>
The order ships as one complete batch.
</t>
</div>
</t>
<!-- 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: 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="6"><strong t-field="line.name"/></td></tr>
</t>
<t t-elif="line.display_type == 'line_note'">
<tr class="note-row"><td colspan="6"><span t-field="line.name"/></td></tr>
</t>
<!-- Charge / fee lines (product line with no part
catalog) are skipped here and rendered in the
totals block under the subtotal. -->
<t t-elif="(not line.display_type or line.display_type == 'product') and line.x_fc_part_catalog_id">
<tr>
<td>
<!-- 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">
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
</td>
<td class="text-center"><span t-field="line.product_uom_id"/></td>
<td class="text-end">
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
<td class="text-end">
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
</t>
</tbody>
</table>
<!-- Terms + Totals -->
<div class="row" style="margin-top: 15px;">
<div class="col-6">
<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;">
<!-- Additional charges (tooling, rush, etc.) are real
taxed SO lines but render here under the subtotal,
NOT in the parts table above. Any product line with
no part catalog is treated as a charge. Subtotal is
parts-only (amount_untaxed minus the charges), so the
tax + grand total stay the standard Odoo figures. -->
<t t-set="fp_charge_lines" t-value="doc.order_line.filtered(lambda l: (not l.display_type or l.display_type == 'product') and not l.x_fc_part_catalog_id)"/>
<t t-set="fp_charge_total" t-value="sum(fp_charge_lines.mapped('price_subtotal'))"/>
<t t-set="fp_parts_subtotal" t-value="doc.amount_untaxed - fp_charge_total"/>
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<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-out="fp_parts_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<t t-foreach="fp_charge_lines" t-as="cl">
<tr>
<td><span t-esc="cl.name or 'Additional Charge'"/></td>
<td class="text-end">
<span t-field="cl.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
<tr>
<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>
<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>
</table>
</div>
</div>
<!-- External (customer-visible) notes -->
<t t-if="doc.x_fc_external_note">
<div style="margin-top: 15px;">
<strong>Notes / Remarques:</strong>
<div t-field="doc.x_fc_external_note"/>
</div>
</t>
<!-- Terms and Conditions -->
<t t-if="doc.note">
<div style="margin-top: 15px;">
<strong>Terms and Conditions / Conditions générales:</strong>
<div t-field="doc.note"/>
</div>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- LANDSCAPE -->
<!-- ============================================================= -->
<template id="report_fp_sale_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">
<!-- Title -->
<h2 style="text-align: left;">
<span t-if="doc.state in ['draft','sent']">Quotation # </span>
<span t-else="">Sales Order # </span>
<span t-field="doc.name"/>
</h2>
<!-- Billing / Shipping -->
<table class="bordered">
<thead>
<tr>
<th style="width: 50%;">BILLING ADDRESS</th>
<th style="width: 50%;">SHIPPING ADDRESS</th>
</tr>
</thead>
<tbody>
<tr>
<td style="height: 70px; font-size: 12pt;">
<div t-field="doc.partner_invoice_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone', 'email'], 'no_marker': True}"/>
</td>
<td style="height: 70px; font-size: 12pt;">
<div t-field="doc.partner_shipping_id"
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
</td>
</tr>
</tbody>
</table>
<!-- Order info (wide) -->
<table class="bordered info-table">
<thead>
<tr>
<th>ORDER DATE</th>
<th>EXPIRATION</th>
<th>SALESPERSON</th>
<th>CUSTOMER PO #</th>
<th>DELIVERY METHOD</th>
<th>RUSH</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">
<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-if="doc.x_fc_rush_order" class="status-warning">RUSH</span>
<span t-else="">Standard</span>
</td>
</tr>
</tbody>
</table>
<!-- Plating details -->
<t t-if="doc.x_fc_part_catalog_id">
<table class="bordered info-table">
<thead>
<tr>
<th>PART CATALOG</th>
<th>INVOICE STRATEGY</th>
<th>DEPOSIT %</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.x_fc_part_catalog_id"/></td>
<td class="text-center">
<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"/>
</td>
<td class="text-center">
<t t-if="doc.x_fc_deposit_percent">
<span t-esc="doc.x_fc_deposit_percent"/>%
</t>
<t t-else="">-</t>
</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 info-table">
<thead>
<tr>
<th>CUSTOMER JOB #</th>
<th>PLANNED START</th>
<th>CUSTOMER DEADLINE</th>
<th>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 -->
<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>
Parts will be released in quantities over time.
</t>
<t t-if="doc.x_fc_block_partial_shipments">
<strong>Partial shipments blocked.</strong>
The order ships as one complete batch.
</t>
</div>
</t>
<!-- Order lines - hide discount column unless at least one line has a discount -->
<t t-set="has_discount" t-value="any(l.discount for l in doc.order_line)"/>
<t t-set="col_count" t-value="8 if has_discount else 7"/>
<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>
</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 t-att-colspan="col_count"><strong t-field="line.name"/></td></tr>
</t>
<t t-elif="line.display_type == 'line_note'">
<tr class="note-row"><td t-att-colspan="col_count"><span t-field="line.name"/></td></tr>
</t>
<!-- Charge / fee lines render in the totals block, not here. -->
<t t-elif="(not line.display_type or line.display_type == 'product') and line.x_fc_part_catalog_id">
<tr>
<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(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
</td>
<td class="text-center"><span t-field="line.product_uom_id"/></td>
<td class="text-end">
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</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 '-'"/>
</td>
<td class="text-end">
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
</t>
</tbody>
</table>
<!-- Terms + Totals -->
<div class="row" style="margin-top: 15px;">
<div class="col-7">
<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>
</div>
<div class="col-5" style="text-align: right;">
<!-- Additional charges render under the subtotal (see
the portrait template for the rationale). -->
<t t-set="fp_charge_lines" t-value="doc.order_line.filtered(lambda l: (not l.display_type or l.display_type == 'product') and not l.x_fc_part_catalog_id)"/>
<t t-set="fp_charge_total" t-value="sum(fp_charge_lines.mapped('price_subtotal'))"/>
<t t-set="fp_parts_subtotal" t-value="doc.amount_untaxed - fp_charge_total"/>
<table class="totals-table" style="width: auto; margin-left: auto;">
<tr>
<td style="min-width: 200px;">Subtotal</td>
<td class="text-end" style="min-width: 150px;">
<span t-out="fp_parts_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
<t t-foreach="fp_charge_lines" t-as="cl">
<tr>
<td><span t-esc="cl.name or 'Additional Charge'"/></td>
<td class="text-end">
<span t-field="cl.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
</tr>
</t>
<tr>
<td>Taxes</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 class="text-end"><strong>
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</strong></td>
</tr>
</table>
</div>
</div>
<!-- External (customer-visible) notes -->
<t t-if="doc.x_fc_external_note">
<div style="margin-top: 15px;">
<strong>Notes:</strong>
<div t-field="doc.x_fc_external_note"/>
</div>
</t>
<!-- Terms and Conditions -->
<t t-if="doc.note">
<div style="margin-top: 15px;">
<strong>Terms and Conditions:</strong>
<div t-field="doc.note"/>
</div>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,607 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Parts-box identification sticker - printed on a 4x3" label.
Bound to mrp.production (MO), mrp.workorder (WO), fp.job, and
sale.order. The shop talks in "WO #" terms (Steelhead legacy) but
the data may hang off any of those records. The inner template
normalises every input to the same set of resolved variables and
accepts either pre-resolved values from the outer template OR
resolves them itself from `_mo` when called from an mrp.* context.
Variables an outer template MAY pre-set (otherwise falls back to
`_mo`-based resolution):
* _order_id - number to print as "WO #"
* _scan_id - id encoded into the QR URL
* _scan_path - '/fp/job/' or '/fp/wo/' prefix (default '/fp/wo/')
* _mo - the mrp.production record (or False)
* _so, _line - the originating sale order / line
* _part - fp.part.catalog
* _spec - fusion.plating.customer.spec (audit-tracked spec)
* _process - the resolved fusion.plating.process.node tree
* _due - datetime/date for "Due Date" row
* _qty - float for "Qty" row
* _po_number - overrides _so.x_fc_po_number
* _partner_name - overrides _so.partner_id.name
* _mo_ref - string shown muted in "(WH/MO/...)" - '' to hide
* _internal_note- free text for "Notes" row
-->
<odoo>
<!-- ========== Shared inner template ========== -->
<template id="report_fp_wo_sticker_inner">
<t t-set="_base_url" t-value="env['ir.config_parameter'].sudo().get_param('web.base.url', '')"/>
<t t-set="_scan_path" t-value="_scan_path or '/fp/wo/'"/>
<t t-set="_scan_url" t-value="_base_url + _scan_path + str(_scan_id)"/>
<!-- Each variable: prefer the outer-supplied value, otherwise
resolve from _mo. This lets fp.job / sale.order outers feed
pre-resolved data while keeping the original mrp.production /
mrp.workorder callers working untouched. -->
<t t-set="_so" t-value="_so or (_mo and env['sale.order'].sudo().search(
[('name', '=', _mo.origin)], limit=1)) or False"/>
<t t-set="_line" t-value="_line
or (_mo and 'x_fc_sale_order_line_ids' in _mo._fields
and _mo.x_fc_sale_order_line_ids[:1])
or (_so and _so.order_line[:1])
or False"/>
<t t-set="_part" t-value="_part or (_line and _line.x_fc_part_catalog_id) or False"/>
<t t-set="_spec" t-value="_spec or (_line and _line.x_fc_customer_spec_id) or False"/>
<t t-set="_process" t-value="_process
or (_part and _part.default_process_id)
or False"/>
<t t-set="_due" t-value="_due
or (_mo and (_mo.date_deadline or _mo.date_finished))
or (_line and _line.x_fc_part_deadline)
or False"/>
<t t-set="_qty" t-value="_qty if _qty is not None and _qty is not False
else (_mo and _mo.product_qty) or 0"/>
<t t-set="_po_number" t-value="_po_number or (_so and _so.x_fc_po_number) or '-'"/>
<t t-set="_partner_name" t-value="_partner_name or (_so and _so.partner_id.name) or '-'"/>
<!-- Customer short-code for shop-floor "secrecy cover" - operators
see "ABC-MANU" instead of "ABC Manufacturing Inc", so visiting
customers / unauthorised passers-by can't immediately tell whose
parts are on which rack. Rule: first 3 chars of word[0] + "-"
+ first 4 chars of word[1], all uppercase. Single-word names:
just the first 3 chars. Strips non-alphanumeric per word so
punctuation in "St. John's Mfg." doesn't poison the slice. -->
<t t-set="_partner_words"
t-value="[''.join(c for c in w if c.isalnum())
for w in (_partner_name or '').split()
if ''.join(c for c in w if c.isalnum())]"/>
<t t-set="_partner_display" t-value="
(_partner_words[0][:3].upper() + '-' + _partner_words[1][:4].upper())
if len(_partner_words) &gt;= 2
else (_partner_words[0][:3].upper() if _partner_words else (_partner_name or '-'))
"/>
<!-- _mo_ref controls the muted "(WH/MO/00033)" suffix next to PO.
Outer can pass '' to hide it (e.g. fp.job already shows its
own name in the header). Defaults to _mo.name. -->
<t t-set="_mo_ref" t-value="_mo_ref if _mo_ref is not None and _mo_ref is not False
else (_mo and _mo.name) or ''"/>
<t t-set="_internal_note" t-value="_internal_note
or (_so and _so.x_fc_internal_note
and _so.x_fc_internal_note.striptags()[:100])
or '-'"/>
<!-- Serial number - Sub 5 added x_fc_serial_id (M2O fp.serial) on
the SO line. The serial record's `name` is the printable label. -->
<t t-set="_serial_number" t-value="(_line and 'x_fc_serial_id' in _line._fields and _line.x_fc_serial_id and _line.x_fc_serial_id.name) or '-'"/>
<!-- Thickness - operator-typed Char range, e.g. "0.0005-0.0008 mils".
Stored as-typed; ASCII-safe by convention. Strip en/em-dash
defensively for the wkhtmltopdf font path on entech. -->
<t t-set="_thickness_raw" t-value="_line and 'x_fc_thickness_range' in _line._fields and _line.x_fc_thickness_range"/>
<t t-set="_thickness" t-value="(_thickness_raw and _thickness_raw.replace(u'\u2013', '-').replace(u'\u2014', '-')) or '-'"/>
<!-- Notes content - outer can pre-set this (e.g. the Internal
variant passes line.x_fc_internal_description). Otherwise
falls back to line.name (customer-facing description per
Sub 2 Q6), then to part.name. Strip en/em-dash, smart
quotes, and ellipsis defensively for the wkhtmltopdf font
path on entech - same treatment as thickness above, which
otherwise turns "-" into the "â€"" mojibake. -->
<t t-set="_notes_raw" t-value="_notes_content
or (_line and _line.name)
or (_part and _part.name)
or '-'"/>
<t t-set="_notes_content" t-value="_notes_raw
.replace(u'\u2014', '-').replace(u'\u2013', '-')
.replace(u'', &quot;'&quot;).replace(u'', &quot;'&quot;)
.replace(u'“', '&quot;').replace(u'”', '&quot;')
.replace(u'…', '...')"/>
<!-- Inline the QR as base64 data URI so wkhtmltopdf doesn't need
to fetch /report/barcode/ over the network during rendering.
1000x1000 source - Odoo core caps barcode area at 1.2M pixels
(`width * height > 1200000` raises "Barcode too large"), so we
stay under that ceiling. 1000x1000 at the 31mm wrapper gives
~821ppi effective - far above the 203dpi thermal printer. -->
<t t-set="_qr_src" t-value="env['ir.actions.report'].barcode_data_uri(
'QR', _scan_url, width=1000, height=1000)"/>
<style>
@page { margin: 0; size: 152mm 102mm; }
html, body {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
height: 100% !important;
}
/* 3-cell header (Logo | WO# | QR) + 2-region body (fields left,
Notes column right). Absolute positioning + % heights/widths
are mandatory - wkhtmltopdf ignores vh/vw/flex. ----------- */
.fp-sticker {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #000;
position: absolute;
top: 6px; left: 6px; right: 6px; bottom: 6px;
padding: 0;
box-sizing: border-box;
border: 2px solid #000;
page-break-after: always;
page-break-inside: avoid;
}
/* ---- HEADER band: 3 horizontal cells, divided by vertical
rules. Logo / WO# / QR. 32% to fit the +30% QR. ---- */
.fp-sticker-head-wrap {
position: absolute;
left: 0; right: 0; top: 0;
height: 32%;
border-bottom: 2px solid #000;
box-sizing: border-box;
}
table.fp-sticker-head {
width: 100%;
height: 100%;
table-layout: fixed;
border-collapse: collapse;
}
col.fp-col-head-logo { width: 28%; }
col.fp-col-head-wo { width: 44%; }
col.fp-col-head-qr { width: 28%; }
table.fp-sticker-head td {
padding: 0;
vertical-align: middle;
text-align: center;
overflow: hidden;
}
td.fp-sticker-head-logo { border-right: 2px solid #000; padding: 0 6px; }
td.fp-sticker-head-wo { border-right: 2px solid #000; }
.fp-sticker-logo {
max-height: 135px;
max-width: 95%;
display: inline-block;
vertical-align: middle;
}
.fp-sticker-wo {
font-size: 72pt;
font-weight: 900;
letter-spacing: 0.1mm;
line-height: 1;
white-space: nowrap;
margin: 0;
}
/* QR wrapper crops the ~12% quiet-zone the barcode generator
adds around the QR pattern. We render the image larger than
the wrapper and offset so the wrapper clips that border out.
Wrapper 365px = ~30.9mm at 300dpi (30% larger than the
previous 280px). 1000x1000 source = print-sharp at the
paperformat DPI (under Odoo's 1.2M-pixel barcode cap). ---- */
.fp-sticker-qr-wrap {
width: 365px;
height: 365px;
display: inline-block;
position: relative;
overflow: hidden;
}
.fp-sticker-qr {
width: 480px;
height: 480px;
position: absolute;
top: -58px;
left: -58px;
margin: 0;
display: block;
}
/* ---- BODY band: left fields region + right Notes region. ---- */
.fp-sticker-body-wrap {
position: absolute;
left: 0; right: 0;
top: 32%; bottom: 0;
}
.fp-body-left {
position: absolute;
left: 0; top: 0; bottom: 0;
width: 64%;
border-right: 2px solid #000;
box-sizing: border-box;
}
.fp-body-right {
position: absolute;
left: 64%; right: 0; top: 0; bottom: 0;
box-sizing: border-box;
padding: 8px 10px;
overflow: hidden;
}
table.fp-sticker-body {
width: 100%;
height: 100%;
table-layout: fixed;
border-collapse: collapse;
}
table.fp-sticker-body tr { height: 14.28%; }
table.fp-sticker-body tr + tr td { border-top: 1px solid #000; }
col.fp-col-label { width: 38%; }
col.fp-col-value { width: 62%; }
table.fp-sticker-body td {
vertical-align: middle;
padding: 0 8px;
font-size: 50pt;
line-height: 1.0;
}
td.fp-sticker-label {
font-weight: 700;
white-space: nowrap;
border-right: 1px solid #000;
background-color: #f1f2f4;
}
td.fp-sticker-value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Values used to be bold via .fp-sticker-strong (PO/Part#/Qty).
Per ops, only the field title and the WO# header should be
bold; values stay regular weight. Font sizes unchanged from
the original layout - bumping them broke wkhtmltopdf's row
packing on entech, so we accept the same visual weight as
before. -->
.fp-sticker-strong { font-weight: 400; }
.fp-sticker-muted { color: #555; font-size: 30pt; }
/* Notes column on the right side of the body. */
.fp-notes-label {
font-weight: 700;
font-size: 48pt;
margin: 0 0 10px 0;
}
.fp-notes-content {
font-size: 36pt;
line-height: 1.1;
white-space: pre-line;
word-wrap: break-word;
overflow: hidden;
}
</style>
<!-- Per-box loop: renders one sticker page per physical box in
the line/job qty. When _qty_total is missing/0/1, falls
back to a single render (no "X / N" indicator). -->
<!-- Hard safety cap (defense in depth): never render more than 100
label pages in one pass, regardless of what _qty_total resolves
to. A sticker is a per-box identification label; rendering
thousands (each with an inlined logo + QR data-URI) OOMs the
worker. WO-30072 (qty 2000 parts) crashed the PDF engine here. -->
<t t-set="_label_count_raw" t-value="int(_qty_total or 1)"/>
<t t-set="_label_count" t-value="100 if _label_count_raw &gt; 100 else (1 if _label_count_raw &lt; 1 else _label_count_raw)"/>
<t t-foreach="range(_label_count)" t-as="_box_idx0">
<t t-set="_box_idx" t-value="_box_idx0 + 1"/>
<div class="fp-sticker">
<!-- 3-cell header: Logo | WO# | QR -->
<div class="fp-sticker-head-wrap">
<table class="fp-sticker-head">
<colgroup>
<col class="fp-col-head-logo"/>
<col class="fp-col-head-wo"/>
<col class="fp-col-head-qr"/>
</colgroup>
<tr>
<td class="fp-sticker-head-logo">
<!-- env.company.logo is often blank while logo_web
is populated from the partner's image. Fall
back across both + partner.image_1920. -->
<t t-set="_logo" t-value="env.company.logo
or env.company.logo_web
or env.company.partner_id.image_1920
or False"/>
<img t-if="_logo"
class="fp-sticker-logo"
t-att-src="image_data_uri(_logo)"/>
</td>
<td class="fp-sticker-head-wo">
<div class="fp-sticker-wo">
<span t-esc="_order_id"/>
</div>
</td>
<td>
<div class="fp-sticker-qr-wrap" t-if="_qr_src">
<img class="fp-sticker-qr"
t-att-src="_qr_src"/>
</div>
</td>
</tr>
</table>
</div>
<!-- Body: 7-row field table on the left, full-height Notes
column on the right showing the customer-facing description. -->
<div class="fp-sticker-body-wrap">
<div class="fp-body-left">
<table class="fp-sticker-body">
<colgroup>
<col class="fp-col-label"/>
<col class="fp-col-value"/>
</colgroup>
<tr>
<td class="fp-sticker-label">PO #:</td>
<td class="fp-sticker-value">
<span class="fp-sticker-strong"
t-esc="_po_number"/>
</td>
</tr>
<tr>
<td class="fp-sticker-label">SN #:</td>
<td class="fp-sticker-value">
<span t-esc="_serial_number"/>
</td>
</tr>
<tr>
<td class="fp-sticker-label">Customer:</td>
<td class="fp-sticker-value">
<span t-esc="_partner_display"/>
</td>
</tr>
<tr>
<td class="fp-sticker-label">Part #:</td>
<td class="fp-sticker-value">
<t t-if="_multi_line">
<span class="fp-sticker-strong">Multiple Line Items</span>
</t>
<t t-elif="_part">
<span class="fp-sticker-strong"
t-esc="_part.part_number"/>
<t t-if="_part.revision">
<!-- Strip "Rev " prefix if the field
value already includes it, so we
don't print "Rev Rev 1". -->
<t t-set="_rev_clean" t-value="_part.revision.strip()"/>
<t t-if="_rev_clean.lower().startswith('rev ')">
<t t-set="_rev_clean" t-value="_rev_clean[4:].strip()"/>
</t>
<span class="fp-sticker-muted">
Rev <span t-esc="_rev_clean"/>
</span>
</t>
</t>
<t t-else="">-</t>
</td>
</tr>
<tr>
<td class="fp-sticker-label">Due Date:</td>
<td class="fp-sticker-value">
<t t-if="_due">
<span t-esc="_due.strftime('%b %d, %Y')"/>
</t>
<t t-else="">-</t>
</td>
</tr>
<tr>
<td class="fp-sticker-label">Thickness:</td>
<td class="fp-sticker-value">
<span t-esc="_thickness"/>
</td>
</tr>
<tr>
<td class="fp-sticker-label">Qty:</td>
<td class="fp-sticker-value">
<span class="fp-sticker-strong">
<t t-if="_qty_total and int(_qty_total) &gt; 1">
<span t-esc="_box_idx"/> / <span t-esc="int(_qty_total)"/>
</t>
<t t-else="">
<span t-esc="int(_qty) if _qty == int(_qty) else _qty"/>
</t>
</span>
</td>
</tr>
</table>
</div>
<div class="fp-body-right">
<div class="fp-notes-label">Notes:</div>
<div class="fp-notes-content">
<t t-esc="_notes_content"/>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- =====================================================
Reusable defaults block - every outer template t-calls
this BEFORE the sticker inner so `_so`, `_line`, etc.
are always defined. The inner's `_so or fallback`
pattern relies on these names existing in scope.
===================================================== -->
<template id="report_fp_wo_sticker_defaults">
<t t-set="_so" t-value="False"/>
<t t-set="_line" t-value="False"/>
<t t-set="_part" t-value="False"/>
<t t-set="_coating" t-value="False"/>
<t t-set="_process" t-value="False"/>
<t t-set="_due" t-value="False"/>
<t t-set="_qty" t-value="False"/>
<t t-set="_po_number" t-value="False"/>
<t t-set="_partner_name" t-value="False"/>
<t t-set="_mo_ref" t-value="False"/>
<t t-set="_internal_note" t-value="False"/>
<t t-set="_scan_path" t-value="False"/>
<t t-set="_notes_content" t-value="False"/>
<t t-set="_qty_total" t-value="False"/>
<!-- _multi_line = True signals "this PO has multiple part lines";
Part # prints "Multiple Line Items", line-specific fields
(SN/Thickness/Notes) auto-resolve to "-" via _line=False, and
Qty shows the SO-line sum (outer sets _qty + _qty_total=1). -->
<t t-set="_multi_line" t-value="False"/>
</template>
<!-- ========== Outer template - mrp.workorder entry ========== -->
<template id="report_fp_wo_sticker">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<t t-set="_order_id" t-value="doc.id"/>
<t t-set="_scan_id" t-value="doc.id"/>
<t t-set="_mo" t-value="doc.production_id"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
</t>
</template>
<!-- ========== Outer template - mrp.production entry ========== -->
<template id="report_fp_mo_sticker">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<!-- Shop floor talks in "WO #" regardless of Odoo's MO/WO
split. QR always encodes the numeric id so scans
resolve cleanly via /fp/wo/<id>. -->
<t t-set="_order_id" t-value="doc.name or doc.id"/>
<t t-set="_scan_id" t-value="doc.id"/>
<t t-set="_mo" t-value="doc"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
</t>
</template>
<!-- ========== Outer template - sale.order entry ==========
Prints one box sticker per order line that has a part. Lines
without x_fc_part_catalog_id (service lines, freight, etc.) are
skipped - they don't go through plating so they don't need a
box sticker.
The "WO#" header shows the SO name (e.g. SO-30019). The body
carries the part-specific fields (Part #, Customer, etc.) which
disambiguate multi-line SOs without needing a sequence suffix.
The QR encodes /fp/so-line/<line.id> - the controller can
decide whether to land on the parent SO, the line, or (later)
the spawned job. -->
<template id="report_fp_so_sticker">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="so">
<t t-set="_part_lines"
t-value="so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)"/>
<t t-if="len(_part_lines) &gt;= 2">
<!-- Multi-line PO: one consolidated sticker.
Part # prints "Multiple Line Items", Qty is the
sum of all part-line qtys. Per-box loop disabled
(_qty_total=1) - the consolidated label is the
master-skid label, not per-physical-box. -->
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<t t-set="_order_id" t-value="so.name"/>
<t t-set="_scan_id" t-value="_part_lines[:1].id"/>
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
<t t-set="_mo" t-value="False"/>
<t t-set="_so" t-value="so"/>
<t t-set="_line" t-value="False"/>
<t t-set="_part" t-value="False"/>
<t t-set="_spec" t-value="False"/>
<t t-set="_due" t-value="so.commitment_date or False"/>
<t t-set="_qty" t-value="sum(_part_lines.mapped('product_uom_qty'))"/>
<t t-set="_qty_total" t-value="1"/>
<t t-set="_partner_name" t-value="so.partner_id.name"/>
<t t-set="_mo_ref" t-value="''"/>
<t t-set="_multi_line" t-value="True"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
<t t-else="">
<t t-foreach="_part_lines" t-as="line">
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<t t-set="_order_id" t-value="so.name"/>
<t t-set="_scan_id" t-value="line.id"/>
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
<t t-set="_mo" t-value="False"/>
<t t-set="_so" t-value="so"/>
<t t-set="_line" t-value="line"/>
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
<t t-set="_spec" t-value="line.x_fc_customer_spec_id"/>
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
<t t-set="_qty" t-value="line.product_uom_qty"/>
<!-- One label per physical BOX (box_count_in on the
SO's receiving), NOT per part. Was
line.product_uom_qty, which rendered one label per
part and OOM'd on large qty (WO-30072 = 2000).
Falls back to 1 when no box count is recorded. -->
<t t-set="_box_count" t-value="int(sum(so.env['fp.receiving'].sudo().search([('sale_order_id', '=', so.id)]).mapped('box_count_in')) or 0) if 'fp.receiving' in so.env else 0"/>
<t t-set="_qty_total" t-value="_box_count if _box_count &gt; 0 else 1"/>
<t t-set="_partner_name" t-value="so.partner_id.name"/>
<t t-set="_mo_ref" t-value="''"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
</t>
</t>
</t>
</template>
<!-- ========== Outer template - sale.order Internal variant ==========
Same layout + iteration as report_fp_so_sticker, but pre-sets
_notes_content from x_fc_internal_description (Sub 2 internal
description field) so the Notes column shows the ops-facing
description instead of line.name. -->
<template id="report_fp_so_sticker_internal">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="so">
<t t-set="_part_lines"
t-value="so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)"/>
<t t-if="len(_part_lines) &gt;= 2">
<!-- Multi-line PO: one consolidated sticker.
Notes column blanked ("-") because each line has
its own internal description. -->
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<t t-set="_order_id" t-value="so.name"/>
<t t-set="_scan_id" t-value="_part_lines[:1].id"/>
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
<t t-set="_mo" t-value="False"/>
<t t-set="_so" t-value="so"/>
<t t-set="_line" t-value="False"/>
<t t-set="_part" t-value="False"/>
<t t-set="_spec" t-value="False"/>
<t t-set="_due" t-value="so.commitment_date or False"/>
<t t-set="_qty" t-value="sum(_part_lines.mapped('product_uom_qty'))"/>
<t t-set="_qty_total" t-value="1"/>
<t t-set="_partner_name" t-value="so.partner_id.name"/>
<t t-set="_mo_ref" t-value="''"/>
<t t-set="_multi_line" t-value="True"/>
<t t-set="_notes_content" t-value="'-'"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
<t t-else="">
<t t-foreach="_part_lines" t-as="line">
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<t t-set="_order_id" t-value="so.name"/>
<t t-set="_scan_id" t-value="line.id"/>
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
<t t-set="_mo" t-value="False"/>
<t t-set="_so" t-value="so"/>
<t t-set="_line" t-value="line"/>
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
<t t-set="_spec" t-value="line.x_fc_customer_spec_id"/>
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
<t t-set="_qty" t-value="line.product_uom_qty"/>
<!-- One label per physical BOX (box_count_in on the
SO's receiving), NOT per part. Was
line.product_uom_qty, which rendered one label per
part and OOM'd on large qty (WO-30072 = 2000).
Falls back to 1 when no box count is recorded. -->
<t t-set="_box_count" t-value="int(sum(so.env['fp.receiving'].sudo().search([('sale_order_id', '=', so.id)]).mapped('box_count_in')) or 0) if 'fp.receiving' in so.env else 0"/>
<t t-set="_qty_total" t-value="_box_count if _box_count &gt; 0 else 1"/>
<t t-set="_partner_name" t-value="so.partner_id.name"/>
<t t-set="_mo_ref" t-value="''"/>
<!-- Internal override: read x_fc_internal_description -->
<t t-set="_notes_content" t-value="('x_fc_internal_description' in line._fields
and line.x_fc_internal_description) or '-'"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,478 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Fusion Plating - Work Order traveller (Portrait + Landscape).
Printed shop-floor sheet with step info, bath/tank, chemistry targets,
and sign-off rows.
-->
<odoo>
<!-- ============================================================= -->
<!-- PORTRAIT -->
<!-- ============================================================= -->
<template id="report_fp_work_order_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<div class="page">
<h4>
Work Order Traveller #
<span t-field="doc.name"/>
</h4>
<!-- Header info -->
<table class="bordered">
<thead>
<tr>
<th class="info-header">MO #</th>
<th class="info-header">STEP</th>
<th class="info-header">WORK CENTRE</th>
<th class="info-header">PRIORITY</th>
<th class="info-header">STATE</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.production_id.name"/></td>
<td class="text-center"><span t-esc="doc.x_fc_step_display or '-'"/></td>
<td class="text-center"><span t-field="doc.workcenter_id"/></td>
<td class="text-center">
<t t-set="prio" t-value="dict(doc._fields['x_fc_priority'].selection).get(doc.x_fc_priority, 'Normal')"/>
<span t-esc="prio"/>
</td>
<td class="text-center"><span t-field="doc.state"/></td>
</tr>
</tbody>
</table>
<!-- Customer / Part -->
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 50%;">CUSTOMER</th>
<th class="info-header" style="width: 50%;">PRODUCT</th>
</tr>
</thead>
<tbody>
<tr>
<td><span t-field="doc.x_fc_customer_id"/></td>
<td><span t-field="doc.product_id"/></td>
</tr>
</tbody>
</table>
<!-- Sub 2: Part number + dual descriptions (internal report).
Operators still see the service SKU under PRODUCT above.
Resolves the sale.order.line via the MO's tagged source
lines (x_fc_sale_order_line_ids), falling back to the
SO's first line that matches this WO's product. -->
<t t-set="_mo" t-value="doc.production_id"/>
<t t-set="_so_lines" t-value="_mo.x_fc_sale_order_line_ids if 'x_fc_sale_order_line_ids' in _mo._fields else _mo.env['sale.order.line']"/>
<t t-set="_line" t-value="_so_lines.filtered(lambda l: l.product_id == doc.product_id)[:1] or _so_lines[:1]"/>
<t t-if="(not _line) and doc.x_fc_sale_order_id">
<t t-set="_line" t-value="doc.x_fc_sale_order_id.order_line.filtered(lambda l: l.product_id == doc.product_id)[:1] or doc.x_fc_sale_order_id.order_line[:1]"/>
</t>
<table class="bordered">
<thead>
<tr><th colspan="2" class="fp-header-primary">PART &amp; DESCRIPTIONS</th></tr>
</thead>
<tbody>
<tr>
<td style="width: 30%; background-color: #f5f5f5; font-weight: bold;">Part Number</td>
<td>
<t t-if="_line and 'x_fc_part_catalog_id' in _line._fields and _line.x_fc_part_catalog_id">
<strong>
<span t-esc="_line.x_fc_part_catalog_id.part_number"/>
<t t-if="_line.x_fc_part_catalog_id.revision">
<span> (Rev <span t-esc="_line.x_fc_part_catalog_id.revision"/>)</span>
</t>
</strong>
</t>
<t t-else="">-</t>
</td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Customer-Facing Description</td>
<td>
<t t-if="_line"><span t-esc="_line.name or '-'"/></t>
<t t-else="">-</t>
</td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Internal Description / Workflow</td>
<td>
<t t-if="_line and 'x_fc_internal_description' in _line._fields and _line.x_fc_internal_description">
<span t-esc="_line.x_fc_internal_description" style="white-space: pre-wrap;"/>
</t>
<t t-else="">-</t>
</td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Service SKU</td>
<td><span t-esc="doc.product_id.default_code or '-'"/></td>
</tr>
</tbody>
</table>
<!-- Sale / Portal links -->
<table class="bordered">
<thead>
<tr>
<th class="info-header" style="width: 33%;">SALE ORDER</th>
<th class="info-header" style="width: 34%;">PORTAL JOB</th>
<th class="info-header" style="width: 33%;">QTY TO PRODUCE</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-esc="doc.x_fc_sale_order_name or '-'"/></td>
<td class="text-center"><span t-field="doc.x_fc_portal_job_id"/></td>
<td class="text-center"><span t-field="doc.qty_production"/></td>
</tr>
</tbody>
</table>
<!-- Process parameters -->
<table class="bordered">
<thead>
<tr><th colspan="2" class="fp-header-primary">PROCESS PARAMETERS</th></tr>
</thead>
<tbody>
<tr>
<td style="width: 35%; background-color: #f5f5f5; font-weight: bold;">Bath</td>
<td><span t-field="doc.x_fc_bath_id"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Tank</td>
<td><span t-field="doc.x_fc_tank_id"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Rack / Fixture Ref</td>
<td><span t-esc="doc.x_fc_rack_ref or '-'"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Target Thickness</td>
<td>
<t t-if="doc.x_fc_thickness_target">
<span t-esc="doc.x_fc_thickness_target"/>
<span t-esc="dict(doc._fields['x_fc_thickness_uom'].selection).get(doc.x_fc_thickness_uom, '')"/>
</t>
<t t-else="">-</t>
</td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Dwell Time</td>
<td>
<t t-if="doc.x_fc_dwell_time_minutes">
<span t-esc="doc.x_fc_dwell_time_minutes"/> min
</t>
<t t-else="">-</t>
</td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Facility</td>
<td><span t-field="doc.x_fc_facility_id"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Expected Duration</td>
<td><span t-esc="doc.duration_expected"/> min</td>
</tr>
</tbody>
</table>
<!-- Operation instructions -->
<t t-if="doc.operation_id and doc.operation_id.note">
<table class="bordered">
<thead>
<tr><th class="fp-header-primary">OPERATION INSTRUCTIONS</th></tr>
</thead>
<tbody>
<tr><td><div t-field="doc.operation_id.note"/></td></tr>
</tbody>
</table>
</t>
<!-- Bath chemistry targets snapshot -->
<t t-if="doc.x_fc_bath_id and doc.x_fc_bath_id.target_line_ids">
<table class="bordered">
<thead>
<tr>
<th>PARAMETER</th>
<th>MIN</th>
<th>MAX</th>
<th>UOM</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.x_fc_bath_id.target_line_ids" t-as="p">
<tr>
<td><span t-field="p.parameter_id"/></td>
<td class="text-center"><span t-esc="p.target_min or '-'"/></td>
<td class="text-center"><span t-esc="p.target_max or '-'"/></td>
<td class="text-center"><span t-esc="p.uom or '-'"/></td>
</tr>
</t>
</tbody>
</table>
</t>
<!-- Sign-off -->
<div style="margin-top: 20px;">
<table class="bordered">
<thead>
<tr>
<th colspan="3" class="fp-header-primary">OPERATOR SIGN-OFF</th>
</tr>
<tr>
<th style="width: 40%;">OPERATOR</th>
<th style="width: 30%;">DATE / TIME</th>
<th style="width: 30%;">INITIALS</th>
</tr>
</thead>
<tbody>
<tr><td class="sig-line"/><td class="sig-line"/><td class="sig-line"/></tr>
<tr><td class="sig-line"/><td class="sig-line"/><td class="sig-line"/></tr>
</tbody>
</table>
</div>
<!-- Notes -->
<div style="margin-top: 15px;">
<strong>Notes / Observations:</strong>
<div style="border: 1px solid #000; min-height: 80px; margin-top: 5px;"/>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
<!-- ============================================================= -->
<!-- LANDSCAPE -->
<!-- ============================================================= -->
<template id="report_fp_work_order_landscape">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Work Order Traveller #
<span t-field="doc.name"/>
</h2>
<!-- Header info (wide) -->
<table class="bordered info-table">
<thead>
<tr>
<th>MO #</th>
<th>STEP</th>
<th>WORK CENTRE</th>
<th>FACILITY</th>
<th>PRIORITY</th>
<th>STATE</th>
<th>EXPECTED DURATION</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center"><span t-field="doc.production_id.name"/></td>
<td class="text-center"><span t-esc="doc.x_fc_step_display or '-'"/></td>
<td class="text-center"><span t-field="doc.workcenter_id"/></td>
<td class="text-center"><span t-field="doc.x_fc_facility_id"/></td>
<td class="text-center">
<t t-set="prio" t-value="dict(doc._fields['x_fc_priority'].selection).get(doc.x_fc_priority, 'Normal')"/>
<span t-esc="prio"/>
</td>
<td class="text-center"><span t-field="doc.state"/></td>
<td class="text-center"><span t-esc="doc.duration_expected"/> min</td>
</tr>
</tbody>
</table>
<!-- Customer / Links -->
<table class="bordered info-table">
<thead>
<tr>
<th>CUSTOMER</th>
<th>PRODUCT</th>
<th>SALE ORDER</th>
<th>PORTAL JOB</th>
<th>QTY</th>
</tr>
</thead>
<tbody>
<tr>
<td><span t-field="doc.x_fc_customer_id"/></td>
<td><span t-field="doc.product_id"/></td>
<td class="text-center"><span t-esc="doc.x_fc_sale_order_name or '-'"/></td>
<td class="text-center"><span t-field="doc.x_fc_portal_job_id"/></td>
<td class="text-center"><span t-field="doc.qty_production"/></td>
</tr>
</tbody>
</table>
<!-- Sub 2: Part number + dual descriptions (internal report).
Operators still see the service SKU under PRODUCT above. -->
<t t-set="_mo" t-value="doc.production_id"/>
<t t-set="_so_lines" t-value="_mo.x_fc_sale_order_line_ids if 'x_fc_sale_order_line_ids' in _mo._fields else _mo.env['sale.order.line']"/>
<t t-set="_line" t-value="_so_lines.filtered(lambda l: l.product_id == doc.product_id)[:1] or _so_lines[:1]"/>
<t t-if="(not _line) and doc.x_fc_sale_order_id">
<t t-set="_line" t-value="doc.x_fc_sale_order_id.order_line.filtered(lambda l: l.product_id == doc.product_id)[:1] or doc.x_fc_sale_order_id.order_line[:1]"/>
</t>
<table class="bordered info-table">
<thead>
<tr>
<th style="width: 22%;">PART NUMBER</th>
<th style="width: 30%;">CUSTOMER-FACING DESCRIPTION</th>
<th style="width: 34%;">INTERNAL DESCRIPTION / WORKFLOW</th>
<th style="width: 14%;">SERVICE SKU</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<t t-if="_line and 'x_fc_part_catalog_id' in _line._fields and _line.x_fc_part_catalog_id">
<strong>
<span t-esc="_line.x_fc_part_catalog_id.part_number"/>
<t t-if="_line.x_fc_part_catalog_id.revision">
<span> (Rev <span t-esc="_line.x_fc_part_catalog_id.revision"/>)</span>
</t>
</strong>
</t>
<t t-else="">-</t>
</td>
<td>
<t t-if="_line"><span t-esc="_line.name or '-'"/></t>
<t t-else="">-</t>
</td>
<td>
<t t-if="_line and 'x_fc_internal_description' in _line._fields and _line.x_fc_internal_description">
<span t-esc="_line.x_fc_internal_description" style="white-space: pre-wrap;"/>
</t>
<t t-else="">-</t>
</td>
<td class="text-center"><span t-esc="doc.product_id.default_code or '-'"/></td>
</tr>
</tbody>
</table>
<!-- Process Parameters / Chemistry Targets side-by-side -->
<div class="row">
<div class="col-6">
<table class="bordered">
<thead>
<tr><th colspan="2" class="fp-header-primary">PROCESS PARAMETERS</th></tr>
</thead>
<tbody>
<tr>
<td style="width: 40%; background-color: #f5f5f5; font-weight: bold;">Bath</td>
<td><span t-field="doc.x_fc_bath_id"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Tank</td>
<td><span t-field="doc.x_fc_tank_id"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Rack / Fixture</td>
<td><span t-esc="doc.x_fc_rack_ref or '-'"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Target Thickness</td>
<td>
<t t-if="doc.x_fc_thickness_target">
<span t-esc="doc.x_fc_thickness_target"/>
<span t-esc="dict(doc._fields['x_fc_thickness_uom'].selection).get(doc.x_fc_thickness_uom, '')"/>
</t>
<t t-else="">-</t>
</td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Dwell Time</td>
<td>
<t t-if="doc.x_fc_dwell_time_minutes">
<span t-esc="doc.x_fc_dwell_time_minutes"/> min
</t>
<t t-else="">-</t>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-6">
<t t-if="doc.x_fc_bath_id and doc.x_fc_bath_id.target_line_ids">
<table class="bordered">
<thead>
<tr>
<th colspan="4" class="fp-header-primary">CHEMISTRY TARGETS</th>
</tr>
<tr>
<th>PARAM</th>
<th>MIN</th>
<th>MAX</th>
<th>UOM</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.x_fc_bath_id.target_line_ids" t-as="p">
<tr>
<td><span t-field="p.parameter_id"/></td>
<td class="text-center"><span t-esc="p.target_min or '-'"/></td>
<td class="text-center"><span t-esc="p.target_max or '-'"/></td>
<td class="text-center"><span t-esc="p.uom or '-'"/></td>
</tr>
</t>
</tbody>
</table>
</t>
</div>
</div>
<!-- Operation instructions -->
<t t-if="doc.operation_id and doc.operation_id.note">
<table class="bordered">
<thead>
<tr><th class="fp-header-primary">OPERATION INSTRUCTIONS</th></tr>
</thead>
<tbody>
<tr><td><div t-field="doc.operation_id.note"/></td></tr>
</tbody>
</table>
</t>
<!-- Sign-off -->
<table class="bordered" style="margin-top: 15px;">
<thead>
<tr>
<th colspan="5" class="fp-header-primary">OPERATOR SIGN-OFF</th>
</tr>
<tr>
<th style="width: 25%;">OPERATOR</th>
<th style="width: 15%;">DATE</th>
<th style="width: 15%;">TIME IN</th>
<th style="width: 15%;">TIME OUT</th>
<th style="width: 30%;">INITIALS / NOTES</th>
</tr>
</thead>
<tbody>
<tr><td class="sig-line"/><td class="sig-line"/><td class="sig-line"/><td class="sig-line"/><td class="sig-line"/></tr>
<tr><td class="sig-line"/><td class="sig-line"/><td class="sig-line"/><td class="sig-line"/><td class="sig-line"/></tr>
</tbody>
</table>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,107 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Incident Report
-->
<odoo>
<template id="report_incident">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Incident Report
<span t-field="doc.name"/>
</h2>
<!-- Header Info -->
<table class="bordered info-table">
<thead><tr>
<th>INCIDENT #</th>
<th>DATE</th>
<th>FACILITY</th>
<th>TYPE</th>
<th>STATUS</th>
<th>REPORTED BY</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.incident_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.facility_id"/></td>
<td class="text-center"><span t-field="doc.incident_type"/></td>
<td class="text-center"><span t-field="doc.state"/></td>
<td class="text-center"><span t-field="doc.reported_by_id"/></td>
</tr></tbody>
</table>
<!-- Employee / Location / WSIB -->
<table class="bordered info-table">
<thead><tr>
<th>EMPLOYEE INVOLVED</th>
<th>LOCATION</th>
<th>WSIB REPORTABLE</th>
<th>WSIB FORM 7 SUBMITTED</th>
<th>LOST-TIME DAYS</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.employee_id"/></td>
<td class="text-center"><span t-field="doc.location"/></td>
<td class="text-center">
<t t-if="doc.wsib_reportable">Yes</t>
<t t-else="">No</t>
</td>
<td class="text-center">
<t t-if="doc.wsib_form_7_submitted">Yes</t>
<t t-else="">No</t>
</td>
<td class="text-center"><span t-field="doc.lost_time_days"/></td>
</tr></tbody>
</table>
<!-- Description -->
<table class="bordered">
<tr class="section-row"><td>DESCRIPTION</td></tr>
<tr><td><t t-out="doc.description"/></td></tr>
</table>
<!-- Immediate Action -->
<t t-if="doc.immediate_action">
<table class="bordered">
<tr class="section-row"><td>IMMEDIATE ACTION</td></tr>
<tr><td><t t-out="doc.immediate_action"/></td></tr>
</table>
</t>
<!-- Investigation -->
<t t-if="doc.investigation">
<table class="bordered">
<tr class="section-row"><td>INVESTIGATION</td></tr>
<tr><td><t t-out="doc.investigation"/></td></tr>
</table>
</t>
<!-- Root Cause -->
<t t-if="doc.root_cause">
<table class="bordered">
<tr class="section-row"><td>ROOT CAUSE</td></tr>
<tr><td><t t-out="doc.root_cause"/></td></tr>
</table>
</t>
<!-- Corrective Action -->
<t t-if="doc.corrective_action">
<table class="bordered">
<tr class="section-row"><td>CORRECTIVE ACTION</td></tr>
<tr><td><t t-out="doc.corrective_action"/></td></tr>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,121 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Non-Conformance Report
-->
<odoo>
<template id="report_ncr">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Non-Conformance Report
<span t-field="doc.name"/>
</h2>
<!-- Header Info -->
<table class="bordered info-table">
<thead><tr>
<th>NCR #</th>
<th>STATUS</th>
<th>FACILITY</th>
<th>SEVERITY</th>
<th>SOURCE</th>
<th>REPORTED</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.state"/></td>
<td class="text-center"><span t-field="doc.facility_id"/></td>
<td class="text-center"><span t-field="doc.severity"/></td>
<td class="text-center"><span t-field="doc.source"/></td>
<td class="text-center"><span t-field="doc.reported_date" t-options="{'widget': 'date'}"/></td>
</tr></tbody>
</table>
<!-- Secondary Info -->
<table class="bordered info-table">
<thead><tr>
<th>REPORTED BY</th>
<th>PART / LOT</th>
<th>QTY AFFECTED</th>
<th>BATH</th>
<th>CUSTOMER</th>
<th>DISPOSITION</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.reported_by_id"/></td>
<td class="text-center"><span t-field="doc.part_ref"/></td>
<td class="text-center"><span t-field="doc.quantity_affected"/></td>
<td class="text-center"><span t-field="doc.bath_id"/></td>
<td class="text-center"><span t-field="doc.customer_partner_id"/></td>
<td class="text-center"><span t-field="doc.disposition"/></td>
</tr></tbody>
</table>
<!-- Description -->
<table class="bordered">
<tr class="section-row"><td>DESCRIPTION</td></tr>
<tr><td><t t-out="doc.description"/></td></tr>
</table>
<!-- Root Cause -->
<t t-if="doc.root_cause">
<table class="bordered">
<tr class="section-row"><td>ROOT CAUSE</td></tr>
<tr><td><t t-out="doc.root_cause"/></td></tr>
</table>
</t>
<!-- Containment -->
<t t-if="doc.containment">
<table class="bordered">
<tr class="section-row"><td>CONTAINMENT ACTIONS</td></tr>
<tr><td><t t-out="doc.containment"/></td></tr>
</table>
</t>
<!-- Linked CAPAs -->
<t t-if="doc.capa_ids">
<table class="bordered">
<thead><tr>
<th>LINKED CAPA</th>
<th>TYPE</th>
<th>STATUS</th>
<th>OWNER</th>
<th>DUE DATE</th>
</tr></thead>
<tbody>
<t t-foreach="doc.capa_ids" t-as="capa">
<tr>
<td><span t-field="capa.name"/></td>
<td class="text-center"><span t-field="capa.type"/></td>
<td class="text-center"><span t-field="capa.state"/></td>
<td class="text-center"><span t-field="capa.owner_id"/></td>
<td class="text-center"><span t-field="capa.due_date" t-options="{'widget': 'date'}"/></td>
</tr>
</t>
</tbody>
</table>
</t>
<!-- Closed Date -->
<t t-if="doc.closed_date">
<table class="bordered info-table">
<thead><tr><th>CLOSED ON</th></tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.closed_date" t-options="{'widget': 'date'}"/></td>
</tr></tbody>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Spill Register Report
-->
<odoo>
<template id="report_spill">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Spill Report
<span t-field="doc.name"/>
</h2>
<!-- Header Info -->
<table class="bordered info-table">
<thead><tr>
<th>SPILL #</th>
<th>DATE</th>
<th>FACILITY</th>
<th>SUBSTANCE</th>
<th>QUANTITY</th>
<th>UoM</th>
<th>STATUS</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.spill_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.facility_id"/></td>
<td class="text-center"><span t-field="doc.substance"/></td>
<td class="text-center"><span t-field="doc.quantity"/></td>
<td class="text-center"><span t-field="doc.uom"/></td>
<td class="text-center"><span t-field="doc.state"/></td>
</tr></tbody>
</table>
<!-- Location & Notification -->
<table class="bordered info-table">
<thead><tr>
<th>LOCATION</th>
<th>REPORTED BY</th>
<th>REGULATOR NOTIFIED</th>
<th>NOTIFICATION DATE</th>
<th>CAPA REF</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.location"/></td>
<td class="text-center"><span t-field="doc.reported_by_id"/></td>
<td class="text-center">
<t t-if="doc.regulator_notified">Yes</t>
<t t-else="">No</t>
</td>
<td class="text-center"><span t-field="doc.regulator_notification_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.capa_ref"/></td>
</tr></tbody>
</table>
<!-- Containment Action -->
<t t-if="doc.containment_action">
<table class="bordered">
<tr class="section-row"><td>CONTAINMENT ACTION</td></tr>
<tr><td><span t-field="doc.containment_action"/></td></tr>
</table>
</t>
<!-- Root Cause -->
<t t-if="doc.root_cause">
<table class="bordered">
<tr class="section-row"><td>ROOT CAUSE</td></tr>
<tr><td><span t-field="doc.root_cause"/></td></tr>
</table>
</t>
<!-- Corrective Action -->
<t t-if="doc.corrective_action">
<table class="bordered">
<tr class="section-row"><td>CORRECTIVE ACTION</td></tr>
<tr><td><span t-field="doc.corrective_action"/></td></tr>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Waste Manifest Report
-->
<odoo>
<template id="report_waste_manifest">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<h2 style="text-align: left;">
Waste Manifest
<span t-field="doc.name"/>
</h2>
<!-- Header Info -->
<table class="bordered info-table">
<thead><tr>
<th>MANIFEST REF</th>
<th>WASTE STREAM</th>
<th>FACILITY</th>
<th>SHIP DATE</th>
<th>STATUS</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
<td class="text-center"><span t-field="doc.waste_stream_id"/></td>
<td class="text-center"><span t-field="doc.facility_id"/></td>
<td class="text-center"><span t-field="doc.ship_date" t-options="{'widget': 'date'}"/></td>
<td class="text-center"><span t-field="doc.state"/></td>
</tr></tbody>
</table>
<!-- Shipment Details -->
<table class="bordered info-table">
<thead><tr>
<th>QUANTITY</th>
<th>UoM</th>
<th>CARRIER</th>
<th>RECEIVER</th>
<th>MANIFEST #</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.quantity"/></td>
<td class="text-center"><span t-field="doc.uom"/></td>
<td class="text-center"><span t-field="doc.carrier_id"/></td>
<td class="text-center"><span t-field="doc.receiver_id"/></td>
<td class="text-center"><span t-field="doc.manifest_number"/></td>
</tr></tbody>
</table>
<!-- Notes -->
<t t-if="doc.notes">
<table class="bordered">
<tr class="section-row"><td>NOTES</td></tr>
<tr><td><t t-out="doc.notes"/></td></tr>
</table>
</t>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,229 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Work Order Margin Report - Manufacturing Order
-->
<odoo>
<template id="report_wo_margin">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="d">
<t t-call="web.external_layout">
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<!-- Extra styles for margin report -->
<style>
.fp-margin-header { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.fp-margin-box {
border: 1px solid #ccc; border-radius: 4px;
padding: 8px 14px; text-align: center; min-width: 110px;
flex: 1;
}
.fp-margin-box .label { font-size: 8pt; color: #666; text-transform: uppercase; margin-bottom: 2px; }
.fp-margin-box .amount { font-size: 13pt; font-weight: bold; }
.fp-margin-green { color: #2e7d32; }
.fp-margin-yellow { color: #f57f17; }
.fp-margin-red { color: #c62828; }
</style>
<h2>
Work Order Margin Report
<span style="font-size: 14pt; color: #333;">
- <t t-out="d['mo'].name"/>
</span>
</h2>
<!-- MO Info Line -->
<table class="bordered info-table" style="margin-bottom: 14px;">
<thead><tr>
<th>MO REF</th>
<th>PRODUCT</th>
<th>QUANTITY</th>
<th>STATE</th>
<th>SALE ORDER</th>
<th>DATE PLANNED</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><t t-out="d['mo'].name"/></td>
<td><t t-out="d['mo'].product_id.display_name"/></td>
<td class="text-center">
<t t-out="'%.2f' % d['mo'].product_qty"/>
<t t-out="d['mo'].product_uom_id.name"/>
</td>
<td class="text-center"><t t-out="d['mo'].state"/></td>
<td class="text-center">
<t t-if="d['mo'].sale_order_id">
<t t-out="d['mo'].sale_order_id.name"/>
</t>
<t t-else="">-</t>
</td>
<td class="text-center">
<t t-if="d['mo'].date_start">
<t t-out="d['mo'].date_start" t-options="{'widget': 'date'}"/>
</t>
<t t-else="">-</t>
</td>
</tr></tbody>
</table>
<!-- ============================================= -->
<!-- HEADER SUMMARY BOXES -->
<!-- ============================================= -->
<div class="fp-margin-header">
<div class="fp-margin-box">
<div class="label">Revenue</div>
<div class="amount">$ <t t-out="'%.2f' % d['revenue']"/></div>
</div>
<div class="fp-margin-box">
<div class="label">Part Labour Cost</div>
<div class="amount">$ <t t-out="'%.2f' % d['part_labour_cost']"/></div>
</div>
<div class="fp-margin-box">
<div class="label">Station Labour Cost</div>
<div class="amount">$ <t t-out="'%.2f' % d['station_labour_cost']"/></div>
</div>
<div class="fp-margin-box">
<div class="label">Station Operation Cost</div>
<div class="amount">$ <t t-out="'%.2f' % d['station_operation_cost']"/></div>
</div>
<div class="fp-margin-box">
<div class="label">Inventory Cost</div>
<div class="amount">$ <t t-out="'%.2f' % d['inventory_cost']"/></div>
</div>
<div class="fp-margin-box">
<div class="label">Outsourcing Cost</div>
<div class="amount">$ <t t-out="'%.2f' % d['outsourcing_cost']"/></div>
</div>
<div class="fp-margin-box">
<div class="label">Total Cost</div>
<div class="amount">$ <t t-out="'%.2f' % d['total_cost']"/></div>
</div>
<div class="fp-margin-box">
<div class="label">Gross Profit</div>
<div class="amount"
t-attf-style="color: {{ 'green' if d['gross_profit'] >= 0 else 'red' }};">
$ <t t-out="'%.2f' % d['gross_profit']"/>
</div>
</div>
<div class="fp-margin-box">
<div class="label">Margin %</div>
<div t-attf-class="amount {{ 'fp-margin-green' if d['margin_pct'] > 15 else ('fp-margin-red' if d['margin_pct'] &lt; 5 else 'fp-margin-yellow') }}">
<t t-out="'%.1f' % d['margin_pct']"/> %
</div>
</div>
</div>
<!-- ============================================= -->
<!-- MARGIN PER PART NUMBER -->
<!-- ============================================= -->
<h2 style="font-size: 14pt; margin-top: 20px;">Margin Per Part Number</h2>
<table class="bordered">
<thead><tr>
<th>Part Number</th>
<th class="text-center">Count</th>
<th class="text-end">SO Total $</th>
<th class="text-end">SO $/Part</th>
<th class="text-end">Unit Labour Cost</th>
<th class="text-end">Part Labour Cost</th>
<th class="text-end">Station Labour Cost</th>
<th class="text-end">Station Op Cost</th>
<th class="text-end">Outsourcing Cost</th>
<th class="text-center">Margin %</th>
</tr></thead>
<tbody>
<t t-foreach="d['part_margins']" t-as="pm">
<tr>
<td><t t-out="pm['part_number']"/></td>
<td class="text-center"><t t-out="pm['count']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % pm['so_total']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % pm['so_per_part']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % pm['unit_labour']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % pm['labour_cost']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % pm['station_labour_cost']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % pm['station_operation_cost']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % pm['outsourcing_cost']"/></td>
<td class="text-center"
t-attf-style="color: {{ 'green' if pm['margin_pct'] > 15 else ('red' if pm['margin_pct'] &lt; 5 else '#f57f17') }}; font-weight: bold;">
<t t-out="'%.1f' % pm['margin_pct']"/> %
</td>
</tr>
</t>
<t t-if="not d['part_margins']">
<tr><td colspan="10" class="text-center" style="font-style: italic;">No work order data available.</td></tr>
</t>
</tbody>
</table>
<!-- ============================================= -->
<!-- COST PER STATION -->
<!-- ============================================= -->
<h2 style="font-size: 14pt; margin-top: 20px;">Cost Per Station</h2>
<table class="bordered">
<thead><tr>
<th>Station</th>
<th class="text-end">Labour Rate ($/hr)</th>
<th class="text-end">Part Labour Time</th>
<th class="text-end">Part Labour Cost</th>
<th class="text-end">Station Labour Cost</th>
<th class="text-end">Op Rate ($/hr)</th>
<th class="text-end">Station Dwell Time</th>
<th class="text-end">Operation Cost</th>
<th class="text-end">Total Cost</th>
<th class="text-center">Percentage</th>
</tr></thead>
<tbody>
<t t-foreach="d['station_costs']" t-as="sc">
<tr>
<td><t t-out="sc['station']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % sc['labour_rate']"/></td>
<td class="text-end">
<t t-out="'%.0f' % sc['labour_time']"/> min
(<t t-out="'%.2f' % sc['labour_hours']"/> hr)
</td>
<td class="text-end">$ <t t-out="'%.2f' % sc['labour_cost']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % sc['labour_cost']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % sc['operation_rate']"/></td>
<td class="text-end">
<t t-out="'%.0f' % sc['dwell_time']"/> min
(<t t-out="'%.2f' % sc['dwell_hours']"/> hr)
</td>
<td class="text-end">$ <t t-out="'%.2f' % sc['operation_cost']"/></td>
<td class="text-end">$ <t t-out="'%.2f' % sc['total_cost']"/></td>
<td class="text-center" style="font-weight: bold;">
<t t-out="'%.1f' % sc['percentage']"/> %
</td>
</tr>
</t>
<t t-if="not d['station_costs']">
<tr><td colspan="10" class="text-center" style="font-style: italic;">No station data available.</td></tr>
</t>
</tbody>
<!-- Totals Row -->
<tfoot t-if="d['station_costs']">
<tr class="section-row">
<td><strong>TOTAL</strong></td>
<td/>
<td/>
<td class="text-end"><strong>$ <t t-out="'%.2f' % sum(s['labour_cost'] for s in d['station_costs'])"/></strong></td>
<td class="text-end"><strong>$ <t t-out="'%.2f' % sum(s['labour_cost'] for s in d['station_costs'])"/></strong></td>
<td/>
<td/>
<td class="text-end"><strong>$ <t t-out="'%.2f' % sum(s['operation_cost'] for s in d['station_costs'])"/></strong></td>
<td class="text-end"><strong>$ <t t-out="'%.2f' % sum(s['total_cost'] for s in d['station_costs'])"/></strong></td>
<td class="text-center"><strong>100 %</strong></td>
</tr>
</tfoot>
</table>
<!-- Report Footer -->
<p style="font-size: 8pt; color: #888; margin-top: 20px; text-align: right;">
Generated by Fusion Plating - Nexa Systems Inc.
</p>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_report_wo_margin_operator,report.wo.margin.operator,model_report_fusion_plating_reports_report_wo_margin,fusion_plating.group_fp_technician,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_report_wo_margin_operator report.wo.margin.operator model_report_fusion_plating_reports_report_wo_margin fusion_plating.group_fp_technician 1 0 0 0