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',
|
||||
)
|
||||
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),
|
||||
]))
|
||||
|
||||
|
||||
|
||||
@@ -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.',
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
|
||||
from . import fp_thickness_reading
|
||||
from . import fp_certificate
|
||||
from . import res_config_settings
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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="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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %',
|
||||
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'),
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 -->
|
||||
<!-- ============================================================= -->
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user