fix(fusion_plating): bug review fixes + progress/net-terms invoicing + formal CoC rebuild

Bug review fixes (found by code review + live QWeb error):
- report_fp_sale.xml: product_uom → product_uom_id (Odoo 19 renamed;
  was raising KeyError during PDF render, blocking all sale-order prints)
- mrp_production.button_mark_done: add idempotency guard on delivery
  auto-create (was duplicating on every re-close)
- fp.certificate._compute_batch_ids: use empty recordset instead of
  False for Many2many computed fields
- fp_notification_template._collect_attachments: collapse attach_quotation
  + attach_sale_order into a single render so email doesn't double-attach
  the same PDF
- fp.operator.certification: SQL unique on computed state was unreliable;
  added explicit `revoked` boolean, made state pure-compute, replaced
  SQL constraint with @api.constrains that checks active-only uniqueness;
  has_active_cert now reads revoked + expires_date directly (no stale
  stored state between nightly recomputes)

Two missing invoice strategies implemented + 1 pre-existing deposit bug fix:
- Progress Billing: new x_fc_progress_initial_percent field on sale.order;
  _create_progress_initial_invoice bills the configured % on SO confirm
  via down-payment wizard, _create_final_balance_invoice bills the
  remainder on delivery
- Net Terms: no invoice on confirm; full invoice auto-created when
  fusion.plating.delivery.action_mark_delivered fires
- Fix for deposit (pre-existing, silent): sale.advance.payment.inv
  reads active_ids at wizard-create time, not on create_invoices();
  context was being set on the wrong call, so every deposit attempt
  raised "Expected singleton" and message-posted to chatter instead
  of actually invoicing
- New fusion_plating_invoicing/models/fp_delivery.py hooks
  action_mark_delivered to dispatch final invoice for progress/net_terms
- fp.direct.order.wizard + SO form surface the progress_initial_percent
  field (conditional on strategy)

Report styling cleanup:
- Hide DISCOUNT column from sale + invoice landscape reports unless at
  least one line has a non-zero discount; colspan auto-adjusts
- Replace hardcoded #0066a1 in all reports with company.primary_color
  driven by doc.company_id → company → user.company_id fallback chain,
  with #1d1f1e as ultimate fallback; new .fp-header-primary class
  exposes the colour for inline section headers (CARGO DESCRIPTION,
  PAYMENT DETAILS, OPERATOR SIGN-OFF, etc.) so they retint with the
  company theme without template edits

Certificate of Conformance — formal ENTECH-style rebuild:
- New res.company fields: x_fc_owner_user_id (default signer, sig from
  hr.employee.signature), x_fc_coc_signature_override (manual upload),
  x_fc_{nadcap,as9100,cgp}_logo + _active toggles for accreditation
  badges
- New res.config.settings section "Fusion Plating" exposing the above
  as configurable blocks; manager-only menu under Configuration →
  Fusion Plating Settings
- New fp.certificate fields: nc_quantity, customer_job_no,
  contact_partner_id (child contact for Name / Email / Phone block)
- New report_coc_en + report_coc_fr templates (primary): custom header
  (company contact | accreditations | company logo), bilingual labels
  per variant, customer info block with customer logo, 3-column cert
  info table, 6-column line-item table (Part # | Process | Customer
  PO | Shipped | NC Qty | Customer Job No.), signature image + bordered
  certification statement, footer "Fusion Plating by Nexa Systems"
- Legacy report_coc + report_coc_portrait kept for existing portal-job
  bindings (no behaviour change)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-17 01:18:22 -04:00
parent 6658544f85
commit a623c6684d
27 changed files with 822 additions and 237 deletions

View File

@@ -45,20 +45,17 @@ class FpOperatorCertification(models.Model):
'ir.attachment', string='Training Record',
)
notes = fields.Text(string='Notes')
revoked = fields.Boolean(string='Revoked', tracking=True)
revoked_reason = fields.Text(string='Revoked Reason')
state = fields.Selection(
[('active', 'Active'),
('expired', 'Expired'),
('revoked', 'Revoked')],
string='Status', default='active', required=True,
compute='_compute_state', store=True, readonly=False, tracking=True,
string='Status',
compute='_compute_state', store=True, tracking=True,
# NOT readonly=False — this is purely derived from revoked + expires_date
# so the nightly recompute never fights with manual edits.
)
revoked_reason = fields.Text(string='Revoked Reason')
_sql_constraints = [
('fp_operator_cert_unique',
'unique(employee_id, process_type_id, state)',
'An operator cannot hold two active certifications for the same process.'),
]
@api.depends('employee_id', 'process_type_id')
def _compute_name(self):
@@ -68,33 +65,64 @@ class FpOperatorCertification(models.Model):
else:
rec.name = ''
@api.depends('expires_date')
@api.depends('expires_date', 'revoked')
def _compute_state(self):
today = fields.Date.today()
for rec in self:
if rec.state == 'revoked':
if rec.revoked:
rec.state = 'revoked'
elif rec.expires_date and rec.expires_date < today:
rec.state = 'expired'
else:
rec.state = 'active'
@api.constrains('employee_id', 'process_type_id', 'revoked', 'expires_date')
def _check_single_active(self):
"""At most one active certification per (employee, process_type)."""
today = fields.Date.today()
for rec in self:
if rec.revoked:
continue
if rec.expires_date and rec.expires_date < today:
rec.state = 'expired'
elif rec.state != 'active':
rec.state = 'active'
continue
# This record is active — look for another active sibling
dupes = self.search_count([
('id', '!=', rec.id),
('employee_id', '=', rec.employee_id.id),
('process_type_id', '=', rec.process_type_id.id),
('revoked', '=', False),
'|', ('expires_date', '=', False),
('expires_date', '>=', today),
])
if dupes:
from odoo.exceptions import ValidationError
raise ValidationError(_(
'Operator %s already has an active certification for "%s". '
'Revoke or expire the existing one before adding another.'
) % (rec.employee_id.name, rec.process_type_id.name))
def action_revoke(self):
for rec in self:
rec.state = 'revoked'
rec.revoked = True
rec.message_post(body=_('Certification revoked.'))
@api.model
def has_active_cert(self, employee_id, process_type_id):
"""Utility — True if this employee holds a current certification
for this process type (or one of its ancestors in the category tree).
"""Utility — True if this employee holds a current certification.
Checks revoked + expires_date directly instead of the computed
`state` column, so even a certification that expired yesterday
is caught immediately (no wait for nightly recompute).
"""
if not employee_id or not process_type_id:
return False
today = fields.Date.today()
return bool(self.search_count([
('employee_id', '=', employee_id),
('process_type_id', '=', process_type_id),
('state', '=', 'active'),
('revoked', '=', False),
'|', ('expires_date', '=', False),
('expires_date', '>=', today),
]))

View File

@@ -28,3 +28,38 @@ class ResCompany(models.Model):
def _compute_x_fc_facility_count(self):
for rec in self:
rec.x_fc_facility_count = len(rec.x_fc_facility_ids)
# =====================================================================
# CoC / Certificate report settings
# =====================================================================
x_fc_owner_user_id = fields.Many2one(
'res.users',
string='Certificate Owner (Default Signer)',
help='Quality manager / owner whose signature appears on Certificates '
'of Conformance by default. Signature is pulled from their linked '
'HR Employee record.',
)
x_fc_coc_signature_override = fields.Binary(
string='Signature Override Image',
help='Optional. Upload a pre-scanned signature image to use on '
'Certificates of Conformance. Overrides the Owner user\'s '
'employee signature when set. Useful if the owner doesn\'t have '
'an HR record or wants a different signature for plating certs.',
)
# --- Accreditation logos shown in CoC header ---
x_fc_nadcap_logo = fields.Binary(string='Nadcap Logo')
x_fc_nadcap_active = fields.Boolean(
string='Nadcap Accredited',
help='Show the Nadcap logo on certificates.',
)
x_fc_as9100_logo = fields.Binary(string='AS9100 / ISO 9001 Logo')
x_fc_as9100_active = fields.Boolean(
string='AS9100 / ISO 9001 Certified',
help='Show the AS9100 / ISO 9001 logo on certificates.',
)
x_fc_cgp_logo = fields.Binary(string='Controlled Goods Program Logo')
x_fc_cgp_active = fields.Boolean(
string='CGP Registered',
help='Show the Controlled Goods Program logo on certificates.',
)

View File

@@ -465,16 +465,21 @@ class MrpProduction(models.Model):
[('name', '=', mo.origin)], limit=1,
)
# Auto-create draft delivery record
# Auto-create draft delivery record (idempotent — skip if one
# already exists for this job_ref)
if Delivery is not None:
Delivery.create({
'partner_id': job.partner_id.id,
'job_ref': job.name,
'source_facility_id': (
mo.x_fc_facility_id.id if mo.x_fc_facility_id else False
),
'state': 'draft',
})
existing_delivery = Delivery.search(
[('job_ref', '=', job.name)], limit=1,
)
if not existing_delivery:
Delivery.create({
'partner_id': job.partner_id.id,
'job_ref': job.name,
'source_facility_id': (
mo.x_fc_facility_id.id if mo.x_fc_facility_id else False
),
'state': 'draft',
})
# Auto-create draft Certificate of Conformance
if Certificate is not None:

View File

@@ -33,6 +33,7 @@ Includes Fischerscope thickness measurement data capture.
'data': [
'security/ir.model.access.csv',
'data/fp_certificate_sequence_data.xml',
'views/res_config_settings_views.xml',
'views/fp_certificate_views.xml',
'views/fp_certificates_menu.xml',
],

View File

@@ -5,3 +5,4 @@
from . import fp_thickness_reading
from . import fp_certificate
from . import res_config_settings

View File

@@ -45,6 +45,20 @@ class FpCertificate(models.Model):
po_number = fields.Char(string='Customer PO #')
entech_wo_number = fields.Char(string='Entech WO #')
quantity_shipped = fields.Integer(string='Qty Shipped')
nc_quantity = fields.Integer(
string='NC Qty',
help='Non-conforming quantity — parts that failed inspection / rework.',
)
customer_job_no = fields.Char(
string='Customer Job No.',
help="Customer's internal job / traveler reference.",
)
contact_partner_id = fields.Many2one(
'res.partner', string='Customer Contact',
domain="[('parent_id', '=', partner_id)]",
help="Specific contact person at the customer for this certificate. "
'Their name, email, and phone are printed on the CoC.',
)
issued_by_id = fields.Many2one(
'res.users', string='Issued By', default=lambda self: self.env.user,
)
@@ -73,6 +87,8 @@ class FpCertificate(models.Model):
@api.depends('production_id')
def _compute_batch_ids(self):
Batch = self.env.get('fusion.plating.batch')
Bath = self.env['fusion.plating.bath']
empty_batch = self.env['fusion.plating.batch']
for rec in self:
if Batch is not None and rec.production_id:
batches = Batch.search([
@@ -82,9 +98,9 @@ class FpCertificate(models.Model):
rec.batch_count = len(batches)
rec.bath_ids = batches.mapped('bath_id')
else:
rec.batch_ids = False
rec.batch_ids = empty_batch
rec.batch_count = 0
rec.bath_ids = False
rec.bath_ids = Bath
state = fields.Selection(
[('draft', 'Draft'), ('issued', 'Issued'), ('voided', 'Voided')],
string='Status', default='draft', tracking=True, required=True,

View File

@@ -0,0 +1,38 @@
# -*- 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 fields, models
class ResConfigSettings(models.TransientModel):
"""Expose Fusion Plating CoC settings on the standard Settings page
so they show up under a Fusion Plating section that the owner can
edit like any other Odoo settings."""
_inherit = 'res.config.settings'
x_fc_owner_user_id = fields.Many2one(
related='company_id.x_fc_owner_user_id', readonly=False,
)
x_fc_coc_signature_override = fields.Binary(
related='company_id.x_fc_coc_signature_override', readonly=False,
)
x_fc_nadcap_logo = fields.Binary(
related='company_id.x_fc_nadcap_logo', readonly=False,
)
x_fc_nadcap_active = fields.Boolean(
related='company_id.x_fc_nadcap_active', readonly=False,
)
x_fc_as9100_logo = fields.Binary(
related='company_id.x_fc_as9100_logo', readonly=False,
)
x_fc_as9100_active = fields.Boolean(
related='company_id.x_fc_as9100_active', readonly=False,
)
x_fc_cgp_logo = fields.Binary(
related='company_id.x_fc_cgp_logo', readonly=False,
)
x_fc_cgp_active = fields.Boolean(
related='company_id.x_fc_cgp_active', readonly=False,
)

View File

@@ -80,9 +80,14 @@
<field name="part_number"/>
<field name="po_number"/>
<field name="entech_wo_number"/>
<field name="customer_job_no"/>
<field name="process_description"/>
<field name="spec_reference"/>
<field name="quantity_shipped"/>
<field name="nc_quantity"/>
<field name="contact_partner_id"
options="{'no_create': True}"
invisible="not partner_id"/>
</group>
</group>
<group>

View File

@@ -41,4 +41,12 @@
action="action_fp_certificate_thickness"
sequence="30"/>
<!-- Settings menu under Configuration, manager-only -->
<menuitem id="menu_fp_settings"
name="Fusion Plating Settings"
parent="fusion_plating.menu_fp_config"
action="action_fp_settings"
sequence="1"
groups="fusion_plating.group_fusion_plating_manager"/>
</odoo>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="res_config_settings_view_form_fp" model="ir.ui.view">
<field name="name">res.config.settings.view.form.fusion.plating</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app data-string="Fusion Plating" string="Fusion Plating"
name="fusion_plating" groups="fusion_plating.group_fusion_plating_manager">
<block title="Certificate of Conformance"
name="fp_coc_settings"
help="Branding, accreditation logos, and default signer for Certificates of Conformance.">
<setting id="fp_coc_owner"
string="Certificate Owner (Default Signer)"
help="Their HR Employee signature appears on issued certificates by default.">
<field name="x_fc_owner_user_id"
options="{'no_create': True, 'no_open': True}"/>
</setting>
<setting id="fp_coc_sig_override"
string="Signature Override Image"
help="Upload a scanned signature here to override the owner user's employee signature (useful if they don't have an HR record).">
<field name="x_fc_coc_signature_override"
widget="image" class="oe_avatar"/>
</setting>
</block>
<block title="Accreditation Logos"
name="fp_accreditation_logos"
help="Upload the logos and toggle each on to display it in the CoC header. Sized automatically in the PDF.">
<setting id="fp_nadcap"
string="Nadcap Accredited"
help="Administered by PRI. Upload the official Nadcap Accredited logo.">
<field name="x_fc_nadcap_active"/>
<field name="x_fc_nadcap_logo"
widget="image" class="oe_avatar"
invisible="not x_fc_nadcap_active"/>
</setting>
<setting id="fp_as9100"
string="AS9100 / ISO 9001"
help="AS9100D / ISO 9001 certified. Upload the combined logo.">
<field name="x_fc_as9100_active"/>
<field name="x_fc_as9100_logo"
widget="image" class="oe_avatar"
invisible="not x_fc_as9100_active"/>
</setting>
<setting id="fp_cgp"
string="Controlled Goods Program (CGP)"
help="Registered with Canada's Controlled Goods Program.">
<field name="x_fc_cgp_active"/>
<field name="x_fc_cgp_logo"
widget="image" class="oe_avatar"
invisible="not x_fc_cgp_active"/>
</setting>
</block>
</app>
</xpath>
</field>
</record>
<record id="action_fp_settings" model="ir.actions.act_window">
<field name="name">Fusion Plating Settings</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">current</field>
<field name="context">{'module': 'fusion_plating'}</field>
</record>
</odoo>

View File

@@ -35,6 +35,17 @@ class SaleOrder(models.Model):
)
x_fc_deposit_percent = fields.Float(string='Deposit %',
help='Deposit percentage if strategy is Deposit.')
x_fc_progress_initial_percent = fields.Float(
string='Progress — Initial %',
default=50.0,
help='First-phase percentage for Progress Billing strategy. '
'Billed on SO confirmation; remainder billed on delivery.',
)
x_fc_final_invoice_id = fields.Many2one(
'account.move', string='Final Invoice', copy=False, readonly=True,
help='Final invoice auto-created on delivery for Progress Billing / '
'Net Terms strategies.',
)
x_fc_rush_order = fields.Boolean(string='Rush Order', tracking=True)
x_fc_delivery_method = fields.Selection(
[('local_delivery', 'Local Delivery'), ('shipping_partner', 'Shipping Partner'),

View File

@@ -70,6 +70,10 @@
<field name="x_fc_invoice_strategy"/>
<field name="x_fc_deposit_percent"
invisible="x_fc_invoice_strategy != 'deposit'"/>
<field name="x_fc_progress_initial_percent"
invisible="x_fc_invoice_strategy != 'progress'"/>
<field name="x_fc_final_invoice_id" readonly="1"
invisible="not x_fc_final_invoice_id"/>
</group>
<group string="Delivery">
<field name="x_fc_rush_order"/>

View File

@@ -80,6 +80,9 @@ class FpDirectOrderWizard(models.TransientModel):
string='Invoice Strategy',
)
deposit_percent = fields.Float(string='Deposit %')
progress_initial_percent = fields.Float(
string='Progress — Initial %', default=50.0,
)
notes = fields.Text(string='Internal Notes')
@@ -184,6 +187,7 @@ class FpDirectOrderWizard(models.TransientModel):
'x_fc_po_received': True,
'x_fc_invoice_strategy': self.invoice_strategy,
'x_fc_deposit_percent': self.deposit_percent,
'x_fc_progress_initial_percent': self.progress_initial_percent,
'origin': 'Direct Order',
'note': self.notes or False,
'order_line': [(0, 0, {

View File

@@ -67,6 +67,8 @@
<field name="invoice_strategy"/>
<field name="deposit_percent"
invisible="invoice_strategy != 'deposit'"/>
<field name="progress_initial_percent"
invisible="invoice_strategy != 'progress'"/>
</group>
</group>

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Invoicing',
'version': '19.0.1.0.0',
'version': '19.0.2.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.',
'description': """
@@ -29,6 +29,7 @@ Provides:
'currency': 'CAD',
'depends': [
'fusion_plating_configurator',
'fusion_plating_logistics',
'sale_management',
'account',
],

View File

@@ -7,3 +7,4 @@ from . import fp_invoice_strategy_default
from . import res_partner
from . import sale_order
from . import account_move
from . import fp_delivery

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class FpDelivery(models.Model):
"""Fire the 'final balance' invoice for Progress Billing / Net Terms
when a delivery is marked delivered.
"""
_inherit = 'fusion.plating.delivery'
def action_mark_delivered(self):
res = super().action_mark_delivered()
SaleOrder = self.env['sale.order']
MrpProduction = self.env['mrp.production']
for delivery in self:
# Resolve the sale order via delivery.job_ref → MO.name → MO.origin
so = False
if delivery.job_ref:
mo = MrpProduction.search(
[('name', '=', delivery.job_ref)], limit=1,
)
if mo and mo.origin:
so = SaleOrder.search(
[('name', '=', mo.origin)], limit=1,
)
if not so:
# Fallback: find by partner + recently-confirmed with matching strategy
continue
strategy = so.x_fc_invoice_strategy
if strategy not in ('progress', 'net_terms'):
continue
# Skip if already billed in full
if so.invoice_status == 'invoiced':
continue
so._create_final_balance_invoice()
return res

View File

@@ -43,7 +43,6 @@ class SaleOrder(models.Model):
) % (order.partner_id.name,
order.partner_id.x_fc_account_hold_reason or 'No reason specified'))
else:
# Manager gets a warning in chatter but can proceed
order.message_post(
body=_(
'Warning: Customer "%s" is on account hold (reason: %s). '
@@ -54,35 +53,45 @@ class SaleOrder(models.Model):
res = super().action_confirm()
# --- Invoice strategy automation ---
# --- Invoice strategy automation (on confirm) ---
for order in self:
strategy = order.x_fc_invoice_strategy
if not strategy:
continue
if strategy == 'deposit' and order.x_fc_deposit_percent:
order._create_deposit_invoice()
elif strategy == 'cod_prepay':
order._create_full_invoice()
elif strategy == 'progress' and order.x_fc_progress_initial_percent:
order._create_progress_initial_invoice()
# 'net_terms' — no action on confirm; invoiced when delivery is marked delivered
return res
# ------------------------------------------------------------------
# Strategy implementations
# ------------------------------------------------------------------
def _create_deposit_invoice(self):
"""Create a deposit (down payment) invoice for the deposit percentage."""
"""Deposit strategy: down-payment invoice for the deposit %."""
self.ensure_one()
percent = self.x_fc_deposit_percent
if not percent or percent <= 0:
return
try:
# Use Odoo's standard down payment mechanism
wizard = self.env['sale.advance.payment.inv'].create({
# The wizard's sale_order_ids default reads active_ids AT CREATE
# time — context must be set on .with_context(), not on the
# subsequent create_invoices() call.
wizard = self.env['sale.advance.payment.inv'].with_context(
active_ids=self.ids,
active_model='sale.order',
active_id=self.id,
).create({
'advance_payment_method': 'percentage',
'amount': percent,
})
wizard.with_context(active_ids=self.ids, active_model='sale.order').create_invoices()
wizard.create_invoices()
self.message_post(
body=_('Deposit invoice (%.0f%%) created automatically — strategy: Deposit.') % percent,
body=_('Deposit invoice (%.0f%%) created — strategy: Deposit.') % percent,
)
except Exception as e:
_logger.warning('Failed to create deposit invoice for SO %s: %s', self.name, e)
@@ -91,16 +100,83 @@ class SaleOrder(models.Model):
)
def _create_full_invoice(self):
"""Create a full invoice immediately (COD/Prepay strategy)."""
"""COD / Prepay: invoice the entire order immediately."""
self.ensure_one()
try:
invoices = self._create_invoices()
if invoices:
self.message_post(
body=_('Full invoice created automatically — strategy: COD / Prepay.'),
body=_('Full invoice created — strategy: COD / Prepay.'),
)
except Exception as e:
_logger.warning('Failed to create COD invoice for SO %s: %s', self.name, e)
self.message_post(
body=_('Failed to auto-create invoice: %s. Create manually.') % str(e),
)
def _create_progress_initial_invoice(self):
"""Progress Billing — first invoice at SO confirm.
Uses Odoo's down-payment mechanism to bill the initial percentage.
The remainder is billed on delivery via `_create_final_balance_invoice`.
"""
self.ensure_one()
percent = self.x_fc_progress_initial_percent
if not percent or percent <= 0:
return
try:
wizard = self.env['sale.advance.payment.inv'].with_context(
active_ids=self.ids,
active_model='sale.order',
active_id=self.id,
).create({
'advance_payment_method': 'percentage',
'amount': percent,
})
wizard.create_invoices()
self.message_post(
body=_(
'Progress invoice — initial %.0f%% created — strategy: Progress Billing. '
'Final balance will be invoiced on delivery.'
) % percent,
)
except Exception as e:
_logger.warning('Failed progress-initial invoice for SO %s: %s', self.name, e)
self.message_post(
body=_('Failed to auto-create progress invoice: %s') % str(e),
)
def _create_final_balance_invoice(self):
"""Create the closing invoice for Progress Billing / Net Terms.
Called when delivery is marked delivered. Uses the standard
`_create_invoices()` method which bills the remainder (net of any
previously-posted down payments).
"""
self.ensure_one()
if self.x_fc_final_invoice_id:
return self.x_fc_final_invoice_id # Already invoiced — don't double
if self.invoice_status == 'invoiced':
return False # Nothing more to bill
try:
invoices = self._create_invoices(final=True)
if invoices:
self.x_fc_final_invoice_id = invoices[:1].id
strategy_label = dict(
self._fields['x_fc_invoice_strategy'].selection
).get(self.x_fc_invoice_strategy, self.x_fc_invoice_strategy)
self.message_post(
body=_(
'Final invoice created on delivery — strategy: %s.'
) % strategy_label,
)
return invoices
except Exception as e:
_logger.warning('Failed final invoice for SO %s: %s', self.name, e)
self.message_post(
body=_(
'Failed to auto-create final invoice: %s. '
'Create manually from the SO.'
) % str(e),
)
return False

View File

@@ -190,13 +190,9 @@ class FpNotificationTemplate(models.Model):
_logger.warning('Failed to render %s: %s', xmlid, exc)
return None
if self.attach_quotation and sale_order:
att = _render_report(
'fusion_plating_reports.action_report_fp_sale_portrait', sale_order,
)
if att:
ids.append(att)
if self.attach_sale_order and sale_order:
# Both attach_quotation and attach_sale_order point at the same
# report today — render once to avoid double attachment.
if (self.attach_quotation or self.attach_sale_order) and sale_order:
att = _render_report(
'fusion_plating_reports.action_report_fp_sale_portrait', sale_order,
)

View File

@@ -38,7 +38,7 @@
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
</record>
<!-- Certificate of Conformance — Portrait -->
<!-- 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>
@@ -50,6 +50,34 @@
<field name="binding_type">report</field>
</record>
<!-- ============================================================= -->
<!-- Formal Certificate of Conformance — English -->
<!-- ============================================================= -->
<record id="action_report_coc_en" model="ir.actions.report">
<field name="name">Certificate of Conformance (English)</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 EN - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_certificates.model_fp_certificate"/>
<field name="binding_type">report</field>
</record>
<!-- ============================================================= -->
<!-- Formal Certificate of Conformance — French -->
<!-- ============================================================= -->
<record id="action_report_coc_fr" model="ir.actions.report">
<field name="name">Certificat de Conformité (Français)</field>
<field name="model">fp.certificate</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_coc_fr</field>
<field name="report_file">fusion_plating_reports.report_coc_fr</field>
<field name="print_report_name">'CoC FR - %s' % object.name</field>
<field name="binding_model_id" ref="fusion_plating_certificates.model_fp_certificate"/>
<field name="binding_type">report</field>
</record>
<!-- ============================================================= -->
<!-- 2. Non-Conformance Report -->
<!-- ============================================================= -->

View File

@@ -3,18 +3,30 @@
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).
The primary colour is driven by the active company's
res.company.primary_color field (Settings → Company → Report Layout),
falling back to #1d1f1e when the company has no brand colour set.
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 primary-coloured
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; margin-bottom: 10px; }
.fp-report table.bordered, .fp-report table.bordered th, .fp-report table.bordered td { border: 1px solid #000; }
.fp-report th { background-color: #0066a1; color: white; padding: 6px 8px; font-weight: bold; text-align: center; font-size: 9pt; }
.fp-report th { background-color: <t t-out="fp_primary"/>; color: white; padding: 6px 8px; font-weight: bold; text-align: center; font-size: 9pt; }
.fp-report td { padding: 6px 8px; vertical-align: top; font-size: 10pt; }
.fp-report .text-center { text-align: center; }
.fp-report .text-end { text-align: right; }
@@ -23,12 +35,13 @@
.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: #0066a1; margin: 0 0 15px 0; font-size: 16pt; }
.fp-report h4 { color: <t t-out="fp_primary"/>; margin: 0 0 15px 0; font-size: 16pt; }
.fp-report .totals-table { border: 1px solid #000; border-collapse: collapse; }
.fp-report .totals-table td { border: 1px solid #000; padding: 6px 8px; }
.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 #0066a1; background-color: #eaf2f8; padding: 10px; margin: 10px 0; }
.fp-report .highlight-box { border: 2px solid <t t-out="fp_primary"/>; background-color: #eaf2f8; padding: 10px; margin: 10px 0; }
.fp-report .fp-header-primary { background-color: <t t-out="fp_primary"/>; color: white; }
.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; }
@@ -43,11 +56,13 @@
<!-- 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: 11pt; color: #000; }
.fp-landscape table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
.fp-landscape table.bordered, .fp-landscape table.bordered th, .fp-landscape table.bordered td { border: 1px solid #000; }
.fp-landscape th { background-color: #0066a1; color: white; padding: 8px 10px; font-weight: bold; font-size: 10pt; }
.fp-landscape th { background-color: <t t-out="fp_primary"/>; color: white; padding: 8px 10px; font-weight: bold; font-size: 10pt; }
.fp-landscape td { padding: 6px 8px; vertical-align: top; font-size: 10pt; }
.fp-landscape .text-center { text-align: center; }
.fp-landscape .text-end { text-align: right; }
@@ -56,12 +71,13 @@
.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: #0066a1; margin: 10px 0; font-size: 18pt; }
.fp-landscape h2 { color: <t t-out="fp_primary"/>; margin: 10px 0; font-size: 18pt; }
.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 #0066a1; background-color: #eaf2f8; padding: 10px; margin: 10px 0; }
.fp-landscape .highlight-box { border: 2px solid <t t-out="fp_primary"/>; background-color: #eaf2f8; padding: 10px; margin: 10px 0; }
.fp-landscape .fp-header-primary { background-color: <t t-out="fp_primary"/>; color: white; }
.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; }

View File

@@ -2,15 +2,355 @@
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Certificate of Conformance — Portal Job (Portrait + Landscape).
The original `report_coc` id is kept as the landscape variant so existing
bindings keep working; a new `report_coc_portrait` variant is added.
Fusion Plating — Certificate of Conformance
Four variants:
- report_coc_en English, portrait, ENTECH-style formal cert (primary)
- report_coc_fr French, portrait, mirror of EN
- report_coc_portrait Legacy portrait (kept for existing bindings)
- report_coc Legacy landscape (kept for existing bindings)
Settings sourced from res.company (Settings → Fusion Plating):
- x_fc_owner_user_id.employee_ids[:1].signature Default signer image
- x_fc_coc_signature_override Optional override image
- x_fc_{nadcap,as9100,cgp}_logo + _active Accreditation badges
-->
<odoo>
<!-- ============================================================= -->
<!-- PORTRAIT -->
<!-- ============================================================= -->
<!-- ================================================================== -->
<!-- Shared CoC body macro (English / French switched via LANG) -->
<!-- ================================================================== -->
<template id="coc_body">
<t t-set="is_fr" t-value="LANG == 'fr'"/>
<t t-set="owner_sig" t-value="False"/>
<t t-if="company.x_fc_owner_user_id">
<t t-set="_emp" t-value="company.x_fc_owner_user_id.employee_ids[:1]"/>
<t t-if="_emp">
<t t-set="owner_sig" t-value="_emp.signature"/>
</t>
</t>
<t t-set="signature_img" t-value="company.x_fc_coc_signature_override or owner_sig"/>
<t t-set="signer_name" t-value="doc.certified_by_id.name or (company.x_fc_owner_user_id.name if company.x_fc_owner_user_id else '')"/>
<style>
.fp-coc { font-family: Arial, sans-serif; font-size: 10pt; color: #000; }
.fp-coc h1 { text-align: center; font-size: 22pt; margin: 0 0 8px 0; font-weight: bold; }
.fp-coc hr.heavy { border: 0; border-top: 2px solid #000; margin: 6px 0; }
.fp-coc table { width: 100%; border-collapse: collapse; }
.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: 6px 8px; font-size: 9pt; text-align: center; }
.fp-coc td { padding: 6px 8px; vertical-align: top; font-size: 9pt; }
.fp-coc .text-center { text-align: center; }
.fp-coc .text-end { text-align: right; }
.fp-coc .hdr-company { font-size: 9pt; line-height: 1.35; }
.fp-coc .hdr-company strong { font-size: 11pt; }
.fp-coc .cert-statement-box { border: 1px solid #000; padding: 12px; font-size: 9pt; }
.fp-coc .cert-statement-box h4 { margin: 0 0 6px 0; font-size: 10pt; font-weight: bold; }
.fp-coc .signature-img { max-height: 2.5cm; max-width: 8cm; }
.fp-coc .accreditations { text-align: center; }
.fp-coc .accreditations img { max-height: 2.2cm; margin: 0 6px; vertical-align: middle; }
.fp-coc .logo-box { text-align: right; }
.fp-coc .logo-box img { max-height: 2.5cm; max-width: 4cm; }
.fp-coc .fp-footer-brand { font-size: 8pt; color: #666; text-align: center; margin-top: 14px; }
.fp-coc .small-label { font-size: 8pt; opacity: 0.7; }
</style>
<div class="fp-coc">
<!-- ============================= -->
<!-- HEADER -->
<!-- ============================= -->
<h1 t-if="not is_fr">Certificate of Conformance</h1>
<h1 t-if="is_fr">Certificat de Conformité</h1>
<table>
<tr>
<td style="width: 33%;" class="hdr-company">
<strong t-field="company.name"/><br/>
<t t-if="company.email"><t t-esc="company.email"/><br/></t>
<t t-if="company.phone"><t t-esc="company.phone"/><br/></t>
<t t-if="company.partner_id.street"><t t-esc="company.partner_id.street"/><br/></t>
<t t-if="company.partner_id.street2"><t t-esc="company.partner_id.street2"/><br/></t>
<span t-if="company.partner_id.city"><t t-esc="company.partner_id.city"/></span>
<span t-if="company.partner_id.state_id">, <t t-esc="company.partner_id.state_id.name"/></span>
<span t-if="company.partner_id.zip"> <t t-esc="company.partner_id.zip"/></span><br/>
<t t-if="company.partner_id.country_id"><t t-esc="company.partner_id.country_id.name.upper()"/></t>
</td>
<td style="width: 34%;" class="accreditations">
<t t-if="company.x_fc_nadcap_active and company.x_fc_nadcap_logo">
<img t-att-src="'data:image/png;base64,%s' % company.x_fc_nadcap_logo.decode()"
alt="Nadcap Accredited"/>
</t>
<t t-if="company.x_fc_as9100_active and company.x_fc_as9100_logo">
<img t-att-src="'data:image/png;base64,%s' % company.x_fc_as9100_logo.decode()"
alt="AS9100 / ISO 9001"/>
</t>
<t t-if="company.x_fc_cgp_active and company.x_fc_cgp_logo">
<img t-att-src="'data:image/png;base64,%s' % company.x_fc_cgp_logo.decode()"
alt="Controlled Goods Program"/>
</t>
</td>
<td style="width: 33%;" class="logo-box">
<img t-if="company.logo"
t-att-src="'data:image/png;base64,%s' % company.logo.decode()"
alt=""/>
</td>
</tr>
</table>
<hr class="heavy"/>
<!-- ============================= -->
<!-- CUSTOMER BLOCK -->
<!-- ============================= -->
<table style="margin-top: 6px;">
<tr>
<td style="width: 40%; vertical-align: top;">
<div>
<strong t-if="not is_fr">Customer Name: </strong>
<strong t-if="is_fr">Nom du client : </strong>
<t t-esc="doc.partner_id.name or ''"/>
</div>
<div style="margin-top: 4px;">
<strong t-if="not is_fr">Customer Address:</strong>
<strong t-if="is_fr">Adresse du client :</strong>
<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: 40%; vertical-align: top;">
<t t-set="contact" t-value="doc.contact_partner_id or doc.partner_id"/>
<div>
<strong t-if="not is_fr">Contact Name: </strong>
<strong t-if="is_fr">Nom du contact : </strong>
<t t-esc="contact.name or ''"/>
</div>
<div>
<strong t-if="not is_fr">Customer Email: </strong>
<strong t-if="is_fr">Courriel : </strong>
<t t-esc="contact.email or '-'"/>
</div>
<div>
<strong t-if="not is_fr">Customer Phone: </strong>
<strong t-if="is_fr">Téléphone : </strong>
<t t-esc="contact.phone or '-'"/>
</div>
</td>
<td style="width: 20%; vertical-align: top;" class="logo-box">
<img t-if="doc.partner_id.image_1920"
t-att-src="'data:image/png;base64,%s' % doc.partner_id.image_1920.decode()"
alt=""/>
</td>
</tr>
</table>
<hr class="heavy"/>
<!-- ============================= -->
<!-- CERTIFICATION INFO -->
<!-- ============================= -->
<table class="bordered" style="margin-top: 10px;">
<thead>
<tr>
<th style="width: 33%;">
<span t-if="not is_fr">Date of Certification</span>
<span t-if="is_fr">Date du certificat</span>
</th>
<th style="width: 34%;">
<span t-if="not is_fr">Generated By</span>
<span t-if="is_fr">Créé par</span>
</th>
<th style="width: 33%;">
<span t-if="not is_fr">Work Order #</span>
<span t-if="is_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 / QUANTITIES TABLE -->
<!-- ============================= -->
<div class="text-end small-label" style="margin-top: 12px;">
<strong t-if="not is_fr">Quantities</strong>
<strong t-if="is_fr">Quantités</strong>
</div>
<table class="bordered">
<thead>
<tr>
<th style="width: 20%;">
<span t-if="not is_fr">Part Number / Line Item</span>
<span t-if="is_fr">No. de pièce / Ligne</span>
</th>
<th style="width: 32%;">
<span t-if="not is_fr">Process</span>
<span t-if="is_fr">Procédé</span>
</th>
<th style="width: 16%;">
<span t-if="not is_fr">Customer PO</span>
<span t-if="is_fr">Bon de commande</span>
</th>
<th style="width: 10%;">
<span t-if="not is_fr">Shipped</span>
<span t-if="is_fr">Expédié</span>
</th>
<th style="width: 10%;">
<span t-if="not is_fr">NC Qty</span>
<span t-if="is_fr">Qté NC</span>
</th>
<th style="width: 12%;">
<span t-if="not is_fr">Customer Job No.</span>
<span t-if="is_fr">Bon de travail client</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center">
<t t-esc="doc.part_number or '-'"/>
</td>
<td>
<t t-esc="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>
<!-- ============================= -->
<!-- SIGNATURE + STATEMENT -->
<!-- ============================= -->
<table style="margin-top: 28px;">
<tr>
<td style="width: 50%; vertical-align: top;">
<div>
<strong t-if="not is_fr">Certified By:</strong>
<strong t-if="is_fr">Certifié par :</strong>
</div>
<div style="min-height: 3cm; margin-top: 8px;">
<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: 6px; border-top: 1px solid #000; padding-top: 4px;">
<strong t-if="not is_fr">Name: </strong>
<strong t-if="is_fr">Nom : </strong>
<t t-esc="signer_name or ''"/>
</div>
</td>
<td style="width: 50%; vertical-align: top; padding-left: 16px;">
<div class="cert-statement-box">
<h4 t-if="not is_fr">Certification Statement</h4>
<h4 t-if="is_fr">Énoncé de conformité</h4>
<p t-if="not is_fr" 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 t-if="is_fr" style="margin: 0;">
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>
<!-- ============================= -->
<!-- FOOTER -->
<!-- ============================= -->
<div class="fp-footer-brand">
<t t-if="not is_fr">
Cert Created At:
<span t-esc="context_timestamp(doc.create_date).strftime('%Y-%m-%d %H:%M') if doc.create_date else ''"/>
· Fusion Plating by Nexa Systems
</t>
<t t-if="is_fr">
Certificat créé le :
<span t-esc="context_timestamp(doc.create_date).strftime('%Y-%m-%d %H:%M') if doc.create_date else ''"/>
· Fusion Plating par Nexa Systems
</t>
</div>
</div>
</template>
<!-- ================================================================== -->
<!-- English CoC -->
<!-- ================================================================== -->
<template id="report_coc_en">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.basic_layout">
<t t-set="company" t-value="(doc.portal_job_id.company_id if doc.portal_job_id else False) or (doc.sale_order_id.company_id if doc.sale_order_id else False) or env.company"/>
<t t-set="LANG" t-value="'en'"/>
<div class="page">
<t t-call="fusion_plating_reports.coc_body"/>
</div>
</t>
</t>
</t>
</template>
<!-- ================================================================== -->
<!-- French CoC -->
<!-- ================================================================== -->
<template id="report_coc_fr">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.basic_layout">
<t t-set="company" t-value="(doc.portal_job_id.company_id if doc.portal_job_id else False) or (doc.sale_order_id.company_id if doc.sale_order_id else False) or env.company"/>
<t t-set="LANG" t-value="'fr'"/>
<div class="page">
<t t-call="fusion_plating_reports.coc_body"/>
</div>
</t>
</t>
</t>
</template>
<!-- ================================================================== -->
<!-- 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">
@@ -18,13 +358,10 @@
<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>
<!-- Job info -->
<table class="bordered">
<thead>
<tr>
@@ -43,81 +380,6 @@
</tr>
</tbody>
</table>
<!-- Customer block -->
<table class="bordered">
<thead>
<tr><th colspan="2" style="background-color: #0066a1; color: white;">CUSTOMER</th></tr>
</thead>
<tbody>
<tr>
<td style="width: 30%; background-color: #f5f5f5; font-weight: bold;">Name</td>
<td><span t-field="doc.partner_id.name"/></td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Address</td>
<td>
<span t-field="doc.partner_id" t-options="{'widget': 'contact', 'fields': ['address'], 'no_marker': True}"/>
</td>
</tr>
<tr>
<td style="background-color: #f5f5f5; font-weight: bold;">Tracking Reference</td>
<td><span t-esc="doc.tracking_ref or '-'"/></td>
</tr>
</tbody>
</table>
<!-- Processes -->
<t t-if="doc.process_type_ids">
<table class="bordered">
<thead>
<tr><th style="background-color: #0066a1; color: white;">PROCESSES APPLIED</th></tr>
</thead>
<tbody>
<tr>
<td>
<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>
</t>
<!-- Certification statement -->
<div class="highlight-box">
This certifies that the above items were processed in accordance with
applicable specifications and meet all requirements as stated in the
purchase order. All work was performed in compliance with the quality
management system.
</div>
<!-- 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>
<!-- Signature block -->
<div class="row" style="margin-top: 30px;">
<div class="col-6">
<div class="sig-box">
<div class="sig-line"/>
<div class="small-muted">Quality Manager (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>
@@ -125,9 +387,9 @@
</t>
</template>
<!-- ============================================================= -->
<!-- LANDSCAPE (legacy id `report_coc` kept for existing bindings) -->
<!-- ============================================================= -->
<!-- ================================================================== -->
<!-- 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">
@@ -139,8 +401,6 @@
Certificate of Conformance
<span t-field="doc.name"/>
</h2>
<!-- Job Info -->
<table class="bordered info-table">
<thead><tr>
<th>JOB REF</th>
@@ -149,7 +409,6 @@
<th>RECEIVED</th>
<th>SHIP DATE</th>
<th>TRACKING REF</th>
<th>STATUS</th>
</tr></thead>
<tbody><tr>
<td class="text-center"><span t-field="doc.name"/></td>
@@ -158,76 +417,8 @@
<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>
<td class="text-center"><span t-field="doc.state"/></td>
</tr></tbody>
</table>
<!-- Customer Address -->
<table class="bordered">
<thead><tr>
<th colspan="2">CUSTOMER DETAILS</th>
</tr></thead>
<tbody>
<tr>
<td style="width:30%; font-weight:bold;">Name</td>
<td><span t-field="doc.partner_id.name"/></td>
</tr>
<tr>
<td style="font-weight:bold;">Address</td>
<td>
<span t-field="doc.partner_id" t-options="{'widget': 'contact', 'fields': ['address'], 'no_marker': True}"/>
</td>
</tr>
</tbody>
</table>
<!-- Processes -->
<table class="bordered" t-if="doc.process_type_ids">
<thead><tr>
<th>PROCESSES APPLIED</th>
</tr></thead>
<tbody><tr>
<td>
<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>
<!-- Certification Statement -->
<table class="bordered">
<tr class="section-row"><td>CERTIFICATION</td></tr>
<tr><td style="padding: 16px 12px; font-size: 11pt;">
This certifies that the above items were processed in accordance
with applicable specifications and meet all requirements as stated
in the purchase order. All work was performed in compliance with
the quality management system.
</td></tr>
</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>
<!-- Signature Block -->
<table class="bordered" style="margin-top: 30px;">
<tbody>
<tr>
<td style="width:50%; height: 60px; vertical-align: bottom; font-weight: bold;">
Quality Manager Signature: ___________________________
</td>
<td style="width:50%; height: 60px; vertical-align: bottom; font-weight: bold;">
Date: ___________________________
</td>
</tr>
</tbody>
</table>
</div>
</div>
</t>

View File

@@ -111,7 +111,7 @@
<table class="bordered">
<thead>
<tr>
<th colspan="4" style="background-color: #0066a1; color: white;">CARGO DESCRIPTION</th>
<th colspan="4" class="fp-header-primary">CARGO DESCRIPTION</th>
</tr>
<tr>
<th style="width: 12%;">PACKAGES</th>
@@ -283,7 +283,7 @@
<table class="bordered">
<thead>
<tr>
<th colspan="6" style="background-color: #0066a1; color: white;">CARGO DESCRIPTION</th>
<th colspan="6" class="fp-header-primary">CARGO DESCRIPTION</th>
</tr>
<tr>
<th style="width: 10%;">PACKAGES</th>

View File

@@ -266,7 +266,9 @@
</tbody>
</table>
<!-- Lines -->
<!-- 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.invoice_line_ids)"/>
<t t-set="col_count" t-value="8 if has_discount else 7"/>
<table class="bordered">
<thead>
<tr>
@@ -275,7 +277,7 @@
<th style="width: 8%;">QTY</th>
<th style="width: 8%;">UOM</th>
<th style="width: 12%;">UNIT PRICE</th>
<th style="width: 10%;">DISCOUNT</th>
<th t-if="has_discount" style="width: 10%;">DISCOUNT</th>
<th style="width: 10%;">TAXES</th>
<th style="width: 10%;">AMOUNT</th>
</tr>
@@ -283,10 +285,10 @@
<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="8"><strong t-field="line.name"/></td></tr>
<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 colspan="8"><span t-field="line.name"/></td></tr>
<tr class="note-row"><td t-att-colspan="col_count"><span t-field="line.name"/></td></tr>
</t>
<t t-elif="not line.display_type">
<tr>
@@ -305,7 +307,7 @@
<td class="text-end">
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
</td>
<td class="text-center">
<td t-if="has_discount" class="text-center">
<t t-if="line.discount"><span t-esc="line.discount"/>%</t>
<t t-else="">-</t>
</td>

View File

@@ -59,7 +59,7 @@
<table class="bordered">
<thead>
<tr>
<th colspan="2" style="background-color: #0066a1; color: white;">PAYMENT DETAILS</th>
<th colspan="2" class="fp-header-primary">PAYMENT DETAILS</th>
</tr>
</thead>
<tbody>
@@ -93,7 +93,7 @@
<table class="bordered">
<thead>
<tr>
<th colspan="3" style="background-color: #0066a1; color: white;">APPLIED TO INVOICES</th>
<th colspan="3" class="fp-header-primary">APPLIED TO INVOICES</th>
</tr>
<tr>
<th style="width: 34%;">INVOICE #</th>
@@ -220,7 +220,7 @@
<table class="bordered">
<thead>
<tr>
<th colspan="5" style="background-color: #0066a1; color: white;">APPLIED TO INVOICES</th>
<th colspan="5" class="fp-header-primary">APPLIED TO INVOICES</th>
</tr>
<tr>
<th style="width: 20%;">INVOICE #</th>

View File

@@ -131,7 +131,7 @@
<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"/></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>
@@ -327,7 +327,9 @@
</table>
</t>
<!-- Order lines -->
<!-- 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>
@@ -336,7 +338,7 @@
<th style="width: 8%;">QTY</th>
<th style="width: 8%;">UOM</th>
<th style="width: 12%;">UNIT PRICE</th>
<th style="width: 10%;">DISCOUNT</th>
<th t-if="has_discount" style="width: 10%;">DISCOUNT</th>
<th style="width: 10%;">TAXES</th>
<th style="width: 10%;">AMOUNT</th>
</tr>
@@ -344,10 +346,10 @@
<tbody>
<t t-foreach="doc.order_line" t-as="line">
<t t-if="line.display_type == 'line_section'">
<tr class="section-row"><td colspan="8"><strong t-field="line.name"/></td></tr>
<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 colspan="8"><span t-field="line.name"/></td></tr>
<tr class="note-row"><td t-att-colspan="col_count"><span t-field="line.name"/></td></tr>
</t>
<t t-elif="not line.display_type">
<tr>
@@ -362,11 +364,11 @@
<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"/></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-center">
<td t-if="has_discount" class="text-center">
<t t-if="line.discount"><span t-esc="line.discount"/>%</t>
<t t-else="">-</t>
</td>

View File

@@ -86,7 +86,7 @@
<!-- Process parameters -->
<table class="bordered">
<thead>
<tr><th colspan="2" style="background-color: #0066a1; color: white;">PROCESS PARAMETERS</th></tr>
<tr><th colspan="2" class="fp-header-primary">PROCESS PARAMETERS</th></tr>
</thead>
<tbody>
<tr>
@@ -135,7 +135,7 @@
<t t-if="doc.operation_id and doc.operation_id.note">
<table class="bordered">
<thead>
<tr><th style="background-color: #0066a1; color: white;">OPERATION INSTRUCTIONS</th></tr>
<tr><th class="fp-header-primary">OPERATION INSTRUCTIONS</th></tr>
</thead>
<tbody>
<tr><td><div t-field="doc.operation_id.note"/></td></tr>
@@ -172,7 +172,7 @@
<table class="bordered">
<thead>
<tr>
<th colspan="3" style="background-color: #0066a1; color: white;">OPERATOR SIGN-OFF</th>
<th colspan="3" class="fp-header-primary">OPERATOR SIGN-OFF</th>
</tr>
<tr>
<th style="width: 40%;">OPERATOR</th>
@@ -272,7 +272,7 @@
<div class="col-6">
<table class="bordered">
<thead>
<tr><th colspan="2" style="background-color: #0066a1; color: white;">PROCESS PARAMETERS</th></tr>
<tr><th colspan="2" class="fp-header-primary">PROCESS PARAMETERS</th></tr>
</thead>
<tbody>
<tr>
@@ -314,7 +314,7 @@
<table class="bordered">
<thead>
<tr>
<th colspan="4" style="background-color: #0066a1; color: white;">CHEMISTRY TARGETS</th>
<th colspan="4" class="fp-header-primary">CHEMISTRY TARGETS</th>
</tr>
<tr>
<th>PARAM</th>
@@ -342,7 +342,7 @@
<t t-if="doc.operation_id and doc.operation_id.note">
<table class="bordered">
<thead>
<tr><th style="background-color: #0066a1; color: white;">OPERATION INSTRUCTIONS</th></tr>
<tr><th class="fp-header-primary">OPERATION INSTRUCTIONS</th></tr>
</thead>
<tbody>
<tr><td><div t-field="doc.operation_id.note"/></td></tr>
@@ -354,7 +354,7 @@
<table class="bordered" style="margin-top: 15px;">
<thead>
<tr>
<th colspan="5" style="background-color: #0066a1; color: white;">OPERATOR SIGN-OFF</th>
<th colspan="5" class="fp-header-primary">OPERATOR SIGN-OFF</th>
</tr>
<tr>
<th style="width: 25%;">OPERATOR</th>