This commit is contained in:
gsinghpal
2026-04-27 00:11:18 -04:00
parent d9f58b9851
commit f08f328688
116 changed files with 9891 additions and 359 deletions

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Reports',
'version': '19.0.8.0.0',
'version': '19.0.9.1.0',
'category': 'Manufacturing/Plating',
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
'depends': [
@@ -11,14 +11,19 @@
'sale_pdf_quote_builder',
'account',
'stock',
'mrp',
# 'mrp' dep dropped post-Sub 11 (MRP cutout). Plating uses fp.job
# exclusively now. Re-introducing this dep silently pulls mrp +
# cascade back to `installed` on any -u base rescan.
'fusion_plating',
'fusion_plating_quality',
'fusion_plating_compliance',
'fusion_plating_safety',
'fusion_plating_portal',
'fusion_plating_configurator',
'fusion_plating_jobs',
# NB: fusion_plating_jobs intentionally NOT depended on. Jobs depends
# on us (uses report_fp_wo_sticker_inner). Adding the reverse dep
# creates a cycle. Our only fp.job touchpoint is wo_scan.py which
# uses runtime env.get('fp.job') — safe without the manifest dep.
'fusion_plating_logistics',
],
'data': [
@@ -48,6 +53,10 @@
'report/report_fp_bol.xml',
'report/report_fp_invoice.xml',
'report/report_fp_receipt.xml',
# Sub 12 Phase E — quality/RMA reports.
'report/report_fp_rma_authorisation.xml',
'report/report_fp_8d.xml',
'report/report_fp_quality_monthly.xml',
# Hide Odoo's default reports from the Print menu wherever FP
# ships an equivalent (loaded last so it overrides any earlier
# binding declarations from base modules).

View File

@@ -64,14 +64,8 @@
<field name="binding_type">action</field>
</record>
<!-- ================================================================
mrp.production — hide Odoo's Production Order PDF
FP ships fp_job_traveller as the shop-floor router / traveller
================================================================ -->
<record id="mrp.action_report_production_order" model="ir.actions.report">
<field name="binding_model_id" eval="False"/>
<field name="binding_type">action</field>
</record>
<!-- mrp.production hide-block removed post-Sub 11 (MRP module
uninstalled; xmlid is no longer resolvable). -->
<!-- ================================================================
account.payment — hide Odoo's Payment Receipt
@@ -121,16 +115,13 @@
<field name="sequence" eval="15"/>
</record>
<!-- mrp.production: Job Traveller is the primary -->
<record id="fusion_plating_reports.action_report_fp_job_traveller_mo_portrait" model="ir.actions.report">
<field name="sequence" eval="10"/>
</record>
<record id="fusion_plating_reports.action_report_fp_job_traveller_mo_landscape" model="ir.actions.report">
<field name="sequence" eval="15"/>
</record>
<record id="fusion_plating_reports.action_report_wo_margin" model="ir.actions.report">
<field name="sequence" eval="20"/>
</record>
<!-- mrp.production traveller sequencing removed post-Sub 11 — those
ir.actions.report records were bound to mrp.production and got
removed when Sub 11 cascade-uninstalled mrp. The fp.job-bound
versions are sequenced in their own report files now.
action_report_wo_margin reference also removed — it was an
ir.actions.report bound to mrp.workorder that went with Sub 11. -->
<!-- account.payment: Receipt — primary -->
<record id="fusion_plating_reports.action_report_fp_receipt_portrait" model="ir.actions.report">

View File

@@ -5,3 +5,4 @@
from . import ir_actions_report
from . import report_wo_margin
from . import report_fp_quality_monthly

View File

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

View File

@@ -96,6 +96,12 @@ class ReportWoMargin(models.AbstractModel):
# ------------------------------------------------------------------
@api.model
def _get_report_values(self, docids, data=None):
# Sub 11 — MRP gone. The report is bound to fusion_plating_reports.action_report_wo_margin
# which itself was uninstalled. Returning empty docs keeps the
# AbstractModel safe to import (its sister fp.job report
# report_fp_job_margin owns the live margin path now).
if 'mrp.production' not in self.env:
return {'doc_ids': [], 'doc_model': 'mrp.production', 'docs': []}
productions = self.env['mrp.production'].browse(docids)
docs = []
for mo in productions:

View File

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

View File

@@ -116,10 +116,12 @@
</tbody>
</table>
<!-- Cargo description — iterate MO finished moves so each part
renders with its customer part number via the shared macro. -->
<t t-set="_mo" t-value="env['mrp.production'].sudo().search([('name', '=', doc.job_ref)], limit=1) if doc.job_ref else False"/>
<t t-set="_finished_moves" t-value="_mo.move_finished_ids.filtered(lambda m: m.state != 'cancel') if _mo else False"/>
<!-- Cargo description — iterate the linked fp.job's SO lines
so each part renders with its customer part number via
the shared macro. Sub 11 — replaced mrp.production lookup. -->
<t t-set="_job" t-value="env['fp.job'].sudo().search([('name', '=', doc.job_ref)], limit=1) if doc.job_ref else env['fp.job']"/>
<t t-set="_so" t-value="_job.sale_order_id if _job else False"/>
<t t-set="_lines" t-value="_so.order_line.filtered(lambda l: l.product_id and l.product_uom_qty &gt; 0) if _so else False"/>
<table class="bordered">
<thead>
<tr>
@@ -134,22 +136,21 @@
</tr>
</thead>
<tbody>
<t t-if="_finished_moves">
<t t-foreach="_finished_moves" t-as="move">
<t t-if="_lines">
<t t-foreach="_lines" t-as="line">
<tr>
<td class="text-center fp-cell-mid">
<t t-if="move_first">1</t>
<t t-if="line_first">1</t>
<t t-else=""/>
</td>
<td class="fp-cell-mid">
<t t-set="line" t-value="move.sale_line_id or move"/>
<t t-call="fusion_plating_reports.customer_line_header"/>
<t t-if="move_last and doc.notes">
<t t-if="line_last and doc.notes">
<br/><span t-field="doc.notes"/>
</t>
</td>
<td class="text-center fp-cell-mid">
<span t-esc="int(move.product_uom_qty) if move.product_uom_qty == int(move.product_uom_qty) else move.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 class="text-center fp-cell-mid"></td>
<td class="text-center fp-cell-mid">
@@ -327,10 +328,11 @@
</tbody>
</table>
<!-- Cargo description — iterate MO finished moves so each part
renders with its customer part number via the shared macro. -->
<t t-set="_mo" t-value="env['mrp.production'].sudo().search([('name', '=', doc.job_ref)], limit=1) if doc.job_ref else False"/>
<t t-set="_finished_moves" t-value="_mo.move_finished_ids.filtered(lambda m: m.state != 'cancel') if _mo else False"/>
<!-- Cargo description — iterate the linked fp.job's SO lines.
Sub 11 — replaced mrp.production lookup. -->
<t t-set="_job" t-value="env['fp.job'].sudo().search([('name', '=', doc.job_ref)], limit=1) if doc.job_ref else env['fp.job']"/>
<t t-set="_so" t-value="_job.sale_order_id if _job else False"/>
<t t-set="_lines" t-value="_so.order_line.filtered(lambda l: l.product_id and l.product_uom_qty &gt; 0) if _so else False"/>
<table class="bordered">
<thead>
<tr>
@@ -346,22 +348,21 @@
</tr>
</thead>
<tbody>
<t t-if="_finished_moves">
<t t-foreach="_finished_moves" t-as="move">
<t t-if="_lines">
<t t-foreach="_lines" t-as="line">
<tr>
<td class="text-center">
<t t-if="move_first">1</t>
<t t-if="line_first">1</t>
<t t-else=""/>
</td>
<td>
<t t-set="line" t-value="move.sale_line_id or move"/>
<t t-call="fusion_plating_reports.customer_line_header"/>
<t t-if="move_last and doc.notes">
<t t-if="line_last and doc.notes">
<br/><span t-field="doc.notes"/>
</t>
</td>
<td class="text-center">
<span t-esc="int(move.product_uom_qty) if move.product_uom_qty == int(move.product_uom_qty) else move.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 class="text-center"></td>
<td class="text-center">

View File

@@ -508,8 +508,10 @@
<template id="report_fp_job_traveller_so_landscape">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="so">
<t t-set="mos" t-value="so.env['mrp.production'].search([('origin', '=', so.name)])"/>
<t t-if="not mos">
<!-- Sub 11 — MRP gone; find fp.jobs for this SO and delegate
to the native fp.job traveller template (jobs module). -->
<t t-set="jobs" t-value="so.env['fp.job'].search([('sale_order_id', '=', so.id)])"/>
<t t-if="not jobs">
<t t-call="web.external_layout">
<t t-set="doc" t-value="so"/>
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
@@ -519,21 +521,40 @@
<div class="highlight-box">
<strong class="status-warning">
<i class="fa fa-info-circle"/>
No Manufacturing Order has been generated for this Sale Order yet.
No plating job has been generated for this Sale Order yet.
</strong>
</div>
</div>
</div>
</t>
</t>
<t t-foreach="mos" t-as="mo">
<t t-foreach="jobs" t-as="job">
<t t-call="web.external_layout">
<t t-set="doc" t-value="mo"/>
<t t-call="fusion_plating_reports.fp_landscape_styles"/>
<div class="fp-landscape">
<div class="page">
<t t-call="fusion_plating_reports.report_fp_job_traveller_body"/>
</div>
<div class="page">
<h1>Job Traveller — <span t-esc="job.name"/></h1>
<table class="table table-sm" style="margin-top: 1em;">
<tr><th>Customer</th><td><span t-esc="job.partner_id.name"/></td></tr>
<tr><th>SO</th><td><span t-esc="job.sale_order_id.name or '-'"/></td></tr>
<tr><th>Qty</th><td><span t-esc="job.qty"/></td></tr>
<tr><th>Recipe</th><td><span t-esc="job.recipe_id.name or '-'"/></td></tr>
<tr><th>Deadline</th><td><span t-esc="job.date_deadline and job.date_deadline.strftime('%b %d, %Y') or '-'"/></td></tr>
<tr><th>Status</th><td><span t-esc="job.state"/></td></tr>
</table>
<h3 style="margin-top: 1.5em;">Steps</h3>
<table class="table table-sm">
<thead>
<tr><th>#</th><th>Step</th><th>Work Centre</th><th>Kind</th><th>State</th></tr>
</thead>
<tbody>
<tr t-foreach="job.step_ids.sorted('sequence')" t-as="step">
<td><span t-esc="step.sequence"/></td>
<td><span t-esc="step.name"/></td>
<td><span t-esc="step.work_centre_id.name or '-'"/></td>
<td><span t-esc="step.kind or '-'"/></td>
<td><span t-esc="step.state"/></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
@@ -547,8 +568,8 @@
<template id="report_fp_job_traveller_so_portrait">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="so">
<t t-set="mos" t-value="so.env['mrp.production'].search([('origin', '=', so.name)])"/>
<t t-if="not mos">
<t t-set="jobs" t-value="so.env['fp.job'].search([('sale_order_id', '=', so.id)])"/>
<t t-if="not jobs">
<t t-call="web.external_layout">
<t t-set="doc" t-value="so"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
@@ -558,21 +579,40 @@
<div class="highlight-box">
<strong class="status-warning">
<i class="fa fa-info-circle"/>
No Manufacturing Order has been generated for this Sale Order yet.
No plating job has been generated for this Sale Order yet.
</strong>
</div>
</div>
</div>
</t>
</t>
<t t-foreach="mos" t-as="mo">
<t t-foreach="jobs" t-as="job">
<t t-call="web.external_layout">
<t t-set="doc" t-value="mo"/>
<t t-call="fusion_plating_reports.fp_portrait_styles"/>
<div class="fp-report">
<div class="page">
<t t-call="fusion_plating_reports.report_fp_job_traveller_body"/>
</div>
<div class="page">
<h2>Job Traveller — <span t-esc="job.name"/></h2>
<table class="table table-sm" style="margin-top: 1em;">
<tr><th>Customer</th><td><span t-esc="job.partner_id.name"/></td></tr>
<tr><th>SO</th><td><span t-esc="job.sale_order_id.name or '-'"/></td></tr>
<tr><th>Qty</th><td><span t-esc="job.qty"/></td></tr>
<tr><th>Recipe</th><td><span t-esc="job.recipe_id.name or '-'"/></td></tr>
<tr><th>Deadline</th><td><span t-esc="job.date_deadline and job.date_deadline.strftime('%b %d, %Y') or '-'"/></td></tr>
<tr><th>Status</th><td><span t-esc="job.state"/></td></tr>
</table>
<h3 style="margin-top: 1.5em;">Steps</h3>
<table class="table table-sm">
<thead>
<tr><th>#</th><th>Step</th><th>Work Centre</th><th>Kind</th><th>State</th></tr>
</thead>
<tbody>
<tr t-foreach="job.step_ids.sorted('sequence')" t-as="step">
<td><span t-esc="step.sequence"/></td>
<td><span t-esc="step.name"/></td>
<td><span t-esc="step.work_centre_id.name or '-'"/></td>
<td><span t-esc="step.kind or '-'"/></td>
<td><span t-esc="step.state"/></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>

View File

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

View File

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