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:
@@ -45,20 +45,17 @@ class FpOperatorCertification(models.Model):
|
|||||||
'ir.attachment', string='Training Record',
|
'ir.attachment', string='Training Record',
|
||||||
)
|
)
|
||||||
notes = fields.Text(string='Notes')
|
notes = fields.Text(string='Notes')
|
||||||
|
revoked = fields.Boolean(string='Revoked', tracking=True)
|
||||||
|
revoked_reason = fields.Text(string='Revoked Reason')
|
||||||
state = fields.Selection(
|
state = fields.Selection(
|
||||||
[('active', 'Active'),
|
[('active', 'Active'),
|
||||||
('expired', 'Expired'),
|
('expired', 'Expired'),
|
||||||
('revoked', 'Revoked')],
|
('revoked', 'Revoked')],
|
||||||
string='Status', default='active', required=True,
|
string='Status',
|
||||||
compute='_compute_state', store=True, readonly=False, tracking=True,
|
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')
|
@api.depends('employee_id', 'process_type_id')
|
||||||
def _compute_name(self):
|
def _compute_name(self):
|
||||||
@@ -68,33 +65,64 @@ class FpOperatorCertification(models.Model):
|
|||||||
else:
|
else:
|
||||||
rec.name = ''
|
rec.name = ''
|
||||||
|
|
||||||
@api.depends('expires_date')
|
@api.depends('expires_date', 'revoked')
|
||||||
def _compute_state(self):
|
def _compute_state(self):
|
||||||
today = fields.Date.today()
|
today = fields.Date.today()
|
||||||
for rec in self:
|
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
|
continue
|
||||||
if rec.expires_date and rec.expires_date < today:
|
if rec.expires_date and rec.expires_date < today:
|
||||||
rec.state = 'expired'
|
continue
|
||||||
elif rec.state != 'active':
|
# This record is active — look for another active sibling
|
||||||
rec.state = 'active'
|
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):
|
def action_revoke(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.state = 'revoked'
|
rec.revoked = True
|
||||||
rec.message_post(body=_('Certification revoked.'))
|
rec.message_post(body=_('Certification revoked.'))
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def has_active_cert(self, employee_id, process_type_id):
|
def has_active_cert(self, employee_id, process_type_id):
|
||||||
"""Utility — True if this employee holds a current certification
|
"""Utility — True if this employee holds a current certification.
|
||||||
for this process type (or one of its ancestors in the category tree).
|
|
||||||
|
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:
|
if not employee_id or not process_type_id:
|
||||||
return False
|
return False
|
||||||
|
today = fields.Date.today()
|
||||||
return bool(self.search_count([
|
return bool(self.search_count([
|
||||||
('employee_id', '=', employee_id),
|
('employee_id', '=', employee_id),
|
||||||
('process_type_id', '=', process_type_id),
|
('process_type_id', '=', process_type_id),
|
||||||
('state', '=', 'active'),
|
('revoked', '=', False),
|
||||||
|
'|', ('expires_date', '=', False),
|
||||||
|
('expires_date', '>=', today),
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,3 +28,38 @@ class ResCompany(models.Model):
|
|||||||
def _compute_x_fc_facility_count(self):
|
def _compute_x_fc_facility_count(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.x_fc_facility_count = len(rec.x_fc_facility_ids)
|
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.',
|
||||||
|
)
|
||||||
|
|||||||
@@ -465,16 +465,21 @@ class MrpProduction(models.Model):
|
|||||||
[('name', '=', mo.origin)], limit=1,
|
[('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:
|
if Delivery is not None:
|
||||||
Delivery.create({
|
existing_delivery = Delivery.search(
|
||||||
'partner_id': job.partner_id.id,
|
[('job_ref', '=', job.name)], limit=1,
|
||||||
'job_ref': job.name,
|
)
|
||||||
'source_facility_id': (
|
if not existing_delivery:
|
||||||
mo.x_fc_facility_id.id if mo.x_fc_facility_id else False
|
Delivery.create({
|
||||||
),
|
'partner_id': job.partner_id.id,
|
||||||
'state': 'draft',
|
'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
|
# Auto-create draft Certificate of Conformance
|
||||||
if Certificate is not None:
|
if Certificate is not None:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Includes Fischerscope thickness measurement data capture.
|
|||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/fp_certificate_sequence_data.xml',
|
'data/fp_certificate_sequence_data.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
'views/fp_certificate_views.xml',
|
'views/fp_certificate_views.xml',
|
||||||
'views/fp_certificates_menu.xml',
|
'views/fp_certificates_menu.xml',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,3 +5,4 @@
|
|||||||
|
|
||||||
from . import fp_thickness_reading
|
from . import fp_thickness_reading
|
||||||
from . import fp_certificate
|
from . import fp_certificate
|
||||||
|
from . import res_config_settings
|
||||||
|
|||||||
@@ -45,6 +45,20 @@ class FpCertificate(models.Model):
|
|||||||
po_number = fields.Char(string='Customer PO #')
|
po_number = fields.Char(string='Customer PO #')
|
||||||
entech_wo_number = fields.Char(string='Entech WO #')
|
entech_wo_number = fields.Char(string='Entech WO #')
|
||||||
quantity_shipped = fields.Integer(string='Qty Shipped')
|
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(
|
issued_by_id = fields.Many2one(
|
||||||
'res.users', string='Issued By', default=lambda self: self.env.user,
|
'res.users', string='Issued By', default=lambda self: self.env.user,
|
||||||
)
|
)
|
||||||
@@ -73,6 +87,8 @@ class FpCertificate(models.Model):
|
|||||||
@api.depends('production_id')
|
@api.depends('production_id')
|
||||||
def _compute_batch_ids(self):
|
def _compute_batch_ids(self):
|
||||||
Batch = self.env.get('fusion.plating.batch')
|
Batch = self.env.get('fusion.plating.batch')
|
||||||
|
Bath = self.env['fusion.plating.bath']
|
||||||
|
empty_batch = self.env['fusion.plating.batch']
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if Batch is not None and rec.production_id:
|
if Batch is not None and rec.production_id:
|
||||||
batches = Batch.search([
|
batches = Batch.search([
|
||||||
@@ -82,9 +98,9 @@ class FpCertificate(models.Model):
|
|||||||
rec.batch_count = len(batches)
|
rec.batch_count = len(batches)
|
||||||
rec.bath_ids = batches.mapped('bath_id')
|
rec.bath_ids = batches.mapped('bath_id')
|
||||||
else:
|
else:
|
||||||
rec.batch_ids = False
|
rec.batch_ids = empty_batch
|
||||||
rec.batch_count = 0
|
rec.batch_count = 0
|
||||||
rec.bath_ids = False
|
rec.bath_ids = Bath
|
||||||
state = fields.Selection(
|
state = fields.Selection(
|
||||||
[('draft', 'Draft'), ('issued', 'Issued'), ('voided', 'Voided')],
|
[('draft', 'Draft'), ('issued', 'Issued'), ('voided', 'Voided')],
|
||||||
string='Status', default='draft', tracking=True, required=True,
|
string='Status', default='draft', tracking=True, required=True,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -80,9 +80,14 @@
|
|||||||
<field name="part_number"/>
|
<field name="part_number"/>
|
||||||
<field name="po_number"/>
|
<field name="po_number"/>
|
||||||
<field name="entech_wo_number"/>
|
<field name="entech_wo_number"/>
|
||||||
|
<field name="customer_job_no"/>
|
||||||
<field name="process_description"/>
|
<field name="process_description"/>
|
||||||
<field name="spec_reference"/>
|
<field name="spec_reference"/>
|
||||||
<field name="quantity_shipped"/>
|
<field name="quantity_shipped"/>
|
||||||
|
<field name="nc_quantity"/>
|
||||||
|
<field name="contact_partner_id"
|
||||||
|
options="{'no_create': True}"
|
||||||
|
invisible="not partner_id"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
|
|||||||
@@ -41,4 +41,12 @@
|
|||||||
action="action_fp_certificate_thickness"
|
action="action_fp_certificate_thickness"
|
||||||
sequence="30"/>
|
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>
|
</odoo>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -35,6 +35,17 @@ class SaleOrder(models.Model):
|
|||||||
)
|
)
|
||||||
x_fc_deposit_percent = fields.Float(string='Deposit %',
|
x_fc_deposit_percent = fields.Float(string='Deposit %',
|
||||||
help='Deposit percentage if strategy is 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_rush_order = fields.Boolean(string='Rush Order', tracking=True)
|
||||||
x_fc_delivery_method = fields.Selection(
|
x_fc_delivery_method = fields.Selection(
|
||||||
[('local_delivery', 'Local Delivery'), ('shipping_partner', 'Shipping Partner'),
|
[('local_delivery', 'Local Delivery'), ('shipping_partner', 'Shipping Partner'),
|
||||||
|
|||||||
@@ -70,6 +70,10 @@
|
|||||||
<field name="x_fc_invoice_strategy"/>
|
<field name="x_fc_invoice_strategy"/>
|
||||||
<field name="x_fc_deposit_percent"
|
<field name="x_fc_deposit_percent"
|
||||||
invisible="x_fc_invoice_strategy != 'deposit'"/>
|
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>
|
||||||
<group string="Delivery">
|
<group string="Delivery">
|
||||||
<field name="x_fc_rush_order"/>
|
<field name="x_fc_rush_order"/>
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ class FpDirectOrderWizard(models.TransientModel):
|
|||||||
string='Invoice Strategy',
|
string='Invoice Strategy',
|
||||||
)
|
)
|
||||||
deposit_percent = fields.Float(string='Deposit %')
|
deposit_percent = fields.Float(string='Deposit %')
|
||||||
|
progress_initial_percent = fields.Float(
|
||||||
|
string='Progress — Initial %', default=50.0,
|
||||||
|
)
|
||||||
|
|
||||||
notes = fields.Text(string='Internal Notes')
|
notes = fields.Text(string='Internal Notes')
|
||||||
|
|
||||||
@@ -184,6 +187,7 @@ class FpDirectOrderWizard(models.TransientModel):
|
|||||||
'x_fc_po_received': True,
|
'x_fc_po_received': True,
|
||||||
'x_fc_invoice_strategy': self.invoice_strategy,
|
'x_fc_invoice_strategy': self.invoice_strategy,
|
||||||
'x_fc_deposit_percent': self.deposit_percent,
|
'x_fc_deposit_percent': self.deposit_percent,
|
||||||
|
'x_fc_progress_initial_percent': self.progress_initial_percent,
|
||||||
'origin': 'Direct Order',
|
'origin': 'Direct Order',
|
||||||
'note': self.notes or False,
|
'note': self.notes or False,
|
||||||
'order_line': [(0, 0, {
|
'order_line': [(0, 0, {
|
||||||
|
|||||||
@@ -67,6 +67,8 @@
|
|||||||
<field name="invoice_strategy"/>
|
<field name="invoice_strategy"/>
|
||||||
<field name="deposit_percent"
|
<field name="deposit_percent"
|
||||||
invisible="invoice_strategy != 'deposit'"/>
|
invisible="invoice_strategy != 'deposit'"/>
|
||||||
|
<field name="progress_initial_percent"
|
||||||
|
invisible="invoice_strategy != 'progress'"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Invoicing',
|
'name': 'Fusion Plating — Invoicing',
|
||||||
'version': '19.0.1.0.0',
|
'version': '19.0.2.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.',
|
'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.',
|
||||||
'description': """
|
'description': """
|
||||||
@@ -29,6 +29,7 @@ Provides:
|
|||||||
'currency': 'CAD',
|
'currency': 'CAD',
|
||||||
'depends': [
|
'depends': [
|
||||||
'fusion_plating_configurator',
|
'fusion_plating_configurator',
|
||||||
|
'fusion_plating_logistics',
|
||||||
'sale_management',
|
'sale_management',
|
||||||
'account',
|
'account',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ from . import fp_invoice_strategy_default
|
|||||||
from . import res_partner
|
from . import res_partner
|
||||||
from . import sale_order
|
from . import sale_order
|
||||||
from . import account_move
|
from . import account_move
|
||||||
|
from . import fp_delivery
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -43,7 +43,6 @@ class SaleOrder(models.Model):
|
|||||||
) % (order.partner_id.name,
|
) % (order.partner_id.name,
|
||||||
order.partner_id.x_fc_account_hold_reason or 'No reason specified'))
|
order.partner_id.x_fc_account_hold_reason or 'No reason specified'))
|
||||||
else:
|
else:
|
||||||
# Manager gets a warning in chatter but can proceed
|
|
||||||
order.message_post(
|
order.message_post(
|
||||||
body=_(
|
body=_(
|
||||||
'Warning: Customer "%s" is on account hold (reason: %s). '
|
'Warning: Customer "%s" is on account hold (reason: %s). '
|
||||||
@@ -54,35 +53,45 @@ class SaleOrder(models.Model):
|
|||||||
|
|
||||||
res = super().action_confirm()
|
res = super().action_confirm()
|
||||||
|
|
||||||
# --- Invoice strategy automation ---
|
# --- Invoice strategy automation (on confirm) ---
|
||||||
for order in self:
|
for order in self:
|
||||||
strategy = order.x_fc_invoice_strategy
|
strategy = order.x_fc_invoice_strategy
|
||||||
if not strategy:
|
if not strategy:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if strategy == 'deposit' and order.x_fc_deposit_percent:
|
if strategy == 'deposit' and order.x_fc_deposit_percent:
|
||||||
order._create_deposit_invoice()
|
order._create_deposit_invoice()
|
||||||
elif strategy == 'cod_prepay':
|
elif strategy == 'cod_prepay':
|
||||||
order._create_full_invoice()
|
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
|
return res
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Strategy implementations
|
||||||
|
# ------------------------------------------------------------------
|
||||||
def _create_deposit_invoice(self):
|
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()
|
self.ensure_one()
|
||||||
percent = self.x_fc_deposit_percent
|
percent = self.x_fc_deposit_percent
|
||||||
if not percent or percent <= 0:
|
if not percent or percent <= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use Odoo's standard down payment mechanism
|
# The wizard's sale_order_ids default reads active_ids AT CREATE
|
||||||
wizard = self.env['sale.advance.payment.inv'].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',
|
'advance_payment_method': 'percentage',
|
||||||
'amount': percent,
|
'amount': percent,
|
||||||
})
|
})
|
||||||
wizard.with_context(active_ids=self.ids, active_model='sale.order').create_invoices()
|
wizard.create_invoices()
|
||||||
self.message_post(
|
self.message_post(
|
||||||
body=_('Deposit invoice (%.0f%%) created automatically — strategy: Deposit.') % percent,
|
body=_('Deposit invoice (%.0f%%) created — strategy: Deposit.') % percent,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.warning('Failed to create deposit invoice for SO %s: %s', self.name, 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):
|
def _create_full_invoice(self):
|
||||||
"""Create a full invoice immediately (COD/Prepay strategy)."""
|
"""COD / Prepay: invoice the entire order immediately."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
try:
|
try:
|
||||||
invoices = self._create_invoices()
|
invoices = self._create_invoices()
|
||||||
if invoices:
|
if invoices:
|
||||||
self.message_post(
|
self.message_post(
|
||||||
body=_('Full invoice created automatically — strategy: COD / Prepay.'),
|
body=_('Full invoice created — strategy: COD / Prepay.'),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.warning('Failed to create COD invoice for SO %s: %s', self.name, e)
|
_logger.warning('Failed to create COD invoice for SO %s: %s', self.name, e)
|
||||||
self.message_post(
|
self.message_post(
|
||||||
body=_('Failed to auto-create invoice: %s. Create manually.') % str(e),
|
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
|
||||||
|
|||||||
@@ -190,13 +190,9 @@ class FpNotificationTemplate(models.Model):
|
|||||||
_logger.warning('Failed to render %s: %s', xmlid, exc)
|
_logger.warning('Failed to render %s: %s', xmlid, exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self.attach_quotation and sale_order:
|
# Both attach_quotation and attach_sale_order point at the same
|
||||||
att = _render_report(
|
# report today — render once to avoid double attachment.
|
||||||
'fusion_plating_reports.action_report_fp_sale_portrait', sale_order,
|
if (self.attach_quotation or self.attach_sale_order) and sale_order:
|
||||||
)
|
|
||||||
if att:
|
|
||||||
ids.append(att)
|
|
||||||
if self.attach_sale_order and sale_order:
|
|
||||||
att = _render_report(
|
att = _render_report(
|
||||||
'fusion_plating_reports.action_report_fp_sale_portrait', sale_order,
|
'fusion_plating_reports.action_report_fp_sale_portrait', sale_order,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
|
<field name="paperformat_id" ref="paperformat_fp_a4_landscape"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Certificate of Conformance — Portrait -->
|
<!-- Certificate of Conformance — Portrait (legacy, Portal Job) -->
|
||||||
<record id="action_report_coc_portrait" model="ir.actions.report">
|
<record id="action_report_coc_portrait" model="ir.actions.report">
|
||||||
<field name="name">Certificate of Conformance (Portrait)</field>
|
<field name="name">Certificate of Conformance (Portrait)</field>
|
||||||
<field name="model">fusion.plating.portal.job</field>
|
<field name="model">fusion.plating.portal.job</field>
|
||||||
@@ -50,6 +50,34 @@
|
|||||||
<field name="binding_type">report</field>
|
<field name="binding_type">report</field>
|
||||||
</record>
|
</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 -->
|
<!-- 2. Non-Conformance Report -->
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
|
|||||||
@@ -3,18 +3,30 @@
|
|||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Part of the Fusion Plating product family.
|
Part of the Fusion Plating product family.
|
||||||
|
|
||||||
Shared CSS for all Fusion Plating reports (portrait + landscape).
|
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>
|
<odoo>
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
<!-- Portrait Styles -->
|
<!-- Portrait Styles -->
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
<template id="fp_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>
|
<style>
|
||||||
.fp-report { font-family: Arial, sans-serif; font-size: 10pt; color: #000; }
|
.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 { 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 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 td { padding: 6px 8px; vertical-align: top; font-size: 10pt; }
|
||||||
.fp-report .text-center { text-align: center; }
|
.fp-report .text-center { text-align: center; }
|
||||||
.fp-report .text-end { text-align: right; }
|
.fp-report .text-end { text-align: right; }
|
||||||
@@ -23,12 +35,13 @@
|
|||||||
.fp-report .client-bg { background-color: #fff3e0; }
|
.fp-report .client-bg { background-color: #fff3e0; }
|
||||||
.fp-report .section-row { background-color: #f0f0f0; font-weight: bold; }
|
.fp-report .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||||
.fp-report .note-row { font-style: italic; color: #555; font-size: 9pt; }
|
.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 { border: 1px solid #000; border-collapse: collapse; }
|
||||||
.fp-report .totals-table td { border: 1px solid #000; padding: 6px 8px; }
|
.fp-report .totals-table td { border: 1px solid #000; padding: 6px 8px; }
|
||||||
.fp-report .info-header { background-color: #f5f5f5; color: #333; }
|
.fp-report .info-header { background-color: #f5f5f5; color: #333; }
|
||||||
.fp-report .adp-header { background-color: #e3f2fd; 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 .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-ok { color: #2e7d32; font-weight: bold; }
|
||||||
.fp-report .status-warning { color: #f57f17; font-weight: bold; }
|
.fp-report .status-warning { color: #f57f17; font-weight: bold; }
|
||||||
@@ -43,11 +56,13 @@
|
|||||||
<!-- Landscape Styles -->
|
<!-- Landscape Styles -->
|
||||||
<!-- ============================================================= -->
|
<!-- ============================================================= -->
|
||||||
<template id="fp_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>
|
<style>
|
||||||
.fp-landscape { font-family: Arial, sans-serif; font-size: 11pt; color: #000; }
|
.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 { 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 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 td { padding: 6px 8px; vertical-align: top; font-size: 10pt; }
|
||||||
.fp-landscape .text-center { text-align: center; }
|
.fp-landscape .text-center { text-align: center; }
|
||||||
.fp-landscape .text-end { text-align: right; }
|
.fp-landscape .text-end { text-align: right; }
|
||||||
@@ -56,12 +71,13 @@
|
|||||||
.fp-landscape .client-bg { background-color: #fff3e0; }
|
.fp-landscape .client-bg { background-color: #fff3e0; }
|
||||||
.fp-landscape .section-row { background-color: #f0f0f0; font-weight: bold; }
|
.fp-landscape .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||||
.fp-landscape .note-row { font-style: italic; color: #555; }
|
.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 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 .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 { border: 1px solid #000; }
|
||||||
.fp-landscape .totals-table td { border: 1px solid #000; padding: 8px 12px; font-size: 11pt; }
|
.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 .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-ok { color: #2e7d32; font-weight: bold; }
|
||||||
.fp-landscape .status-warning { color: #f57f17; font-weight: bold; }
|
.fp-landscape .status-warning { color: #f57f17; font-weight: bold; }
|
||||||
|
|||||||
@@ -2,15 +2,355 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Certificate of Conformance — Portal Job (Portrait + Landscape).
|
Fusion Plating — Certificate of Conformance
|
||||||
The original `report_coc` id is kept as the landscape variant so existing
|
|
||||||
bindings keep working; a new `report_coc_portrait` variant is added.
|
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>
|
<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">
|
<template id="report_coc_portrait">
|
||||||
<t t-call="web.html_container">
|
<t t-call="web.html_container">
|
||||||
<t t-foreach="docs" t-as="doc">
|
<t t-foreach="docs" t-as="doc">
|
||||||
@@ -18,13 +358,10 @@
|
|||||||
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
|
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
|
||||||
<div class="fp-report">
|
<div class="fp-report">
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
||||||
<h4>
|
<h4>
|
||||||
Certificate of Conformance —
|
Certificate of Conformance —
|
||||||
<span t-field="doc.name"/>
|
<span t-field="doc.name"/>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<!-- Job info -->
|
|
||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -43,81 +380,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
@@ -125,9 +387,9 @@
|
|||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ============================================================= -->
|
<!-- ================================================================== -->
|
||||||
<!-- LANDSCAPE (legacy id `report_coc` kept for existing bindings) -->
|
<!-- Legacy landscape (bound to fusion.plating.portal.job) -->
|
||||||
<!-- ============================================================= -->
|
<!-- ================================================================== -->
|
||||||
<template id="report_coc">
|
<template id="report_coc">
|
||||||
<t t-call="web.html_container">
|
<t t-call="web.html_container">
|
||||||
<t t-foreach="docs" t-as="doc">
|
<t t-foreach="docs" t-as="doc">
|
||||||
@@ -139,8 +401,6 @@
|
|||||||
Certificate of Conformance
|
Certificate of Conformance
|
||||||
<span t-field="doc.name"/>
|
<span t-field="doc.name"/>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Job Info -->
|
|
||||||
<table class="bordered info-table">
|
<table class="bordered info-table">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>JOB REF</th>
|
<th>JOB REF</th>
|
||||||
@@ -149,7 +409,6 @@
|
|||||||
<th>RECEIVED</th>
|
<th>RECEIVED</th>
|
||||||
<th>SHIP DATE</th>
|
<th>SHIP DATE</th>
|
||||||
<th>TRACKING REF</th>
|
<th>TRACKING REF</th>
|
||||||
<th>STATUS</th>
|
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody><tr>
|
<tbody><tr>
|
||||||
<td class="text-center"><span t-field="doc.name"/></td>
|
<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.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.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.tracking_ref"/></td>
|
||||||
<td class="text-center"><span t-field="doc.state"/></td>
|
|
||||||
</tr></tbody>
|
</tr></tbody>
|
||||||
</table>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 12%;">PACKAGES</th>
|
<th style="width: 12%;">PACKAGES</th>
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 10%;">PACKAGES</th>
|
<th style="width: 10%;">PACKAGES</th>
|
||||||
|
|||||||
@@ -266,7 +266,9 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -275,7 +277,7 @@
|
|||||||
<th style="width: 8%;">QTY</th>
|
<th style="width: 8%;">QTY</th>
|
||||||
<th style="width: 8%;">UOM</th>
|
<th style="width: 8%;">UOM</th>
|
||||||
<th style="width: 12%;">UNIT PRICE</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%;">TAXES</th>
|
||||||
<th style="width: 10%;">AMOUNT</th>
|
<th style="width: 10%;">AMOUNT</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -283,10 +285,10 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<t t-foreach="doc.invoice_line_ids" t-as="line">
|
<t t-foreach="doc.invoice_line_ids" t-as="line">
|
||||||
<t t-if="line.display_type == 'line_section'">
|
<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 t-elif="line.display_type == 'line_note'">
|
<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 t-elif="not line.display_type">
|
<t t-elif="not line.display_type">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -305,7 +307,7 @@
|
|||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||||
</td>
|
</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-if="line.discount"><span t-esc="line.discount"/>%</t>
|
||||||
<t t-else="">-</t>
|
<t t-else="">-</t>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" style="background-color: #0066a1; color: white;">PAYMENT DETAILS</th>
|
<th colspan="2" class="fp-header-primary">PAYMENT DETAILS</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 34%;">INVOICE #</th>
|
<th style="width: 34%;">INVOICE #</th>
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 20%;">INVOICE #</th>
|
<th style="width: 20%;">INVOICE #</th>
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
<td class="text-center">
|
<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"/>
|
<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>
|
||||||
<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">
|
<td class="text-end">
|
||||||
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||||
</td>
|
</td>
|
||||||
@@ -327,7 +327,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</t>
|
</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">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -336,7 +338,7 @@
|
|||||||
<th style="width: 8%;">QTY</th>
|
<th style="width: 8%;">QTY</th>
|
||||||
<th style="width: 8%;">UOM</th>
|
<th style="width: 8%;">UOM</th>
|
||||||
<th style="width: 12%;">UNIT PRICE</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%;">TAXES</th>
|
||||||
<th style="width: 10%;">AMOUNT</th>
|
<th style="width: 10%;">AMOUNT</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -344,10 +346,10 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<t t-foreach="doc.order_line" t-as="line">
|
<t t-foreach="doc.order_line" t-as="line">
|
||||||
<t t-if="line.display_type == 'line_section'">
|
<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 t-elif="line.display_type == 'line_note'">
|
<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 t-elif="not line.display_type">
|
<t t-elif="not line.display_type">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -362,11 +364,11 @@
|
|||||||
<td class="text-center">
|
<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"/>
|
<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>
|
||||||
<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">
|
<td class="text-end">
|
||||||
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||||
</td>
|
</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-if="line.discount"><span t-esc="line.discount"/>%</t>
|
||||||
<t t-else="">-</t>
|
<t t-else="">-</t>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
<!-- Process parameters -->
|
<!-- Process parameters -->
|
||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
<t t-if="doc.operation_id and doc.operation_id.note">
|
<t t-if="doc.operation_id and doc.operation_id.note">
|
||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th style="background-color: #0066a1; color: white;">OPERATION INSTRUCTIONS</th></tr>
|
<tr><th class="fp-header-primary">OPERATION INSTRUCTIONS</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td><div t-field="doc.operation_id.note"/></td></tr>
|
<tr><td><div t-field="doc.operation_id.note"/></td></tr>
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40%;">OPERATOR</th>
|
<th style="width: 40%;">OPERATOR</th>
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th>PARAM</th>
|
<th>PARAM</th>
|
||||||
@@ -342,7 +342,7 @@
|
|||||||
<t t-if="doc.operation_id and doc.operation_id.note">
|
<t t-if="doc.operation_id and doc.operation_id.note">
|
||||||
<table class="bordered">
|
<table class="bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th style="background-color: #0066a1; color: white;">OPERATION INSTRUCTIONS</th></tr>
|
<tr><th class="fp-header-primary">OPERATION INSTRUCTIONS</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td><div t-field="doc.operation_id.note"/></td></tr>
|
<tr><td><div t-field="doc.operation_id.note"/></td></tr>
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
<table class="bordered" style="margin-top: 15px;">
|
<table class="bordered" style="margin-top: 15px;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 25%;">OPERATOR</th>
|
<th style="width: 25%;">OPERATOR</th>
|
||||||
|
|||||||
Reference in New Issue
Block a user