|
|
|
|
@@ -19,7 +19,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
formats. Auto-created when reports are generated.
|
|
|
|
|
"""
|
|
|
|
|
_name = 'fp.certificate'
|
|
|
|
|
_description = 'Fusion Plating — Certificate'
|
|
|
|
|
_description = 'Fusion Plating - Certificate'
|
|
|
|
|
_inherit = ['mail.thread', 'mail.activity.mixin', 'fp.parent.numbered.mixin']
|
|
|
|
|
_order = 'issue_date desc, id desc'
|
|
|
|
|
|
|
|
|
|
@@ -39,7 +39,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
domain="[('customer_rank', '>', 0)]",
|
|
|
|
|
)
|
|
|
|
|
sale_order_id = fields.Many2one('sale.order', string='Sale Order')
|
|
|
|
|
# Phase 6 (Sub 11) — production_id retired (MRP module gone).
|
|
|
|
|
# Phase 6 (Sub 11) - production_id retired (MRP module gone).
|
|
|
|
|
# Certificates link via sale_order_id + portal_job_id natively.
|
|
|
|
|
portal_job_id = fields.Many2one('fusion.plating.portal.job', string='Portal Job')
|
|
|
|
|
part_number = fields.Char(string='Part Number', help='Denormalized for fast search.')
|
|
|
|
|
@@ -52,7 +52,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
quantity_shipped = fields.Integer(string='Qty Shipped')
|
|
|
|
|
nc_quantity = fields.Integer(
|
|
|
|
|
string='NC Qty',
|
|
|
|
|
help='Non-conforming quantity — parts that failed inspection / rework.',
|
|
|
|
|
help='Non-conforming quantity - parts that failed inspection / rework.',
|
|
|
|
|
)
|
|
|
|
|
customer_job_no = fields.Char(
|
|
|
|
|
string='Customer Job No.',
|
|
|
|
|
@@ -69,7 +69,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
'job ships. The first is the primary (its name, email and '
|
|
|
|
|
'phone print on the CoC); ALL of them are emailed when the '
|
|
|
|
|
'cert is sent to the customer. (Renamed from the single '
|
|
|
|
|
'contact_partner_id — see migration 19.0.10.3.0.)',
|
|
|
|
|
'contact_partner_id - see migration 19.0.10.3.0.)',
|
|
|
|
|
)
|
|
|
|
|
issued_by_id = fields.Many2one(
|
|
|
|
|
'res.users', string='Issued By', default=lambda self: self.env.user,
|
|
|
|
|
@@ -78,7 +78,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
'res.users', string='Certified By', help='Signing authority (e.g. Quality Manager).',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ===== Sub 12c — chronological CoC opt-in ===============================
|
|
|
|
|
# ===== Sub 12c - chronological CoC opt-in ===============================
|
|
|
|
|
body_style = fields.Selection(
|
|
|
|
|
[
|
|
|
|
|
('classic', 'Classic (recipe-order)'),
|
|
|
|
|
@@ -113,7 +113,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
string='Fischerscope PDF filename',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Non-PDF Fischerscope uploads (.doc / .docx / .xlsx / images) — the
|
|
|
|
|
# Non-PDF Fischerscope uploads (.doc / .docx / .xlsx / images) - the
|
|
|
|
|
# Issue Certs wizard stashes them here so the thickness-required gate
|
|
|
|
|
# can still pass. Unlike `x_fc_local_thickness_pdf`, this attachment
|
|
|
|
|
# is NOT merged into the CoC PDF as page 2 (we can't rasterize .doc
|
|
|
|
|
@@ -129,7 +129,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
'the CoC PDF.',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Report-level Fischerscope metadata — populated by the Issue Certs
|
|
|
|
|
# Report-level Fischerscope metadata - populated by the Issue Certs
|
|
|
|
|
# wizard when parsing an RTF/.docx upload. Rendered on the CoC so
|
|
|
|
|
# the printed cert shows the same context an auditor would see on
|
|
|
|
|
# the original XDAL 600 export (equipment, operator, calibration,
|
|
|
|
|
@@ -171,11 +171,11 @@ class FpCertificate(models.Model):
|
|
|
|
|
'parsed from.',
|
|
|
|
|
)
|
|
|
|
|
# Two paths populate this field, with operator upload winning:
|
|
|
|
|
# 1. RTF auto-extraction — Issue Certs wizard runs libwmf
|
|
|
|
|
# 1. RTF auto-extraction - Issue Certs wizard runs libwmf
|
|
|
|
|
# (wmf2svg) on the embedded WMF blocks and picks the
|
|
|
|
|
# largest raster (header banners filtered by area threshold).
|
|
|
|
|
# 2. Manual PNG/JPEG upload via the wizard's "Measurement
|
|
|
|
|
# Image" field — operator override path when the
|
|
|
|
|
# Image" field - operator override path when the
|
|
|
|
|
# auto-extracted image is wrong, missing, or low-quality.
|
|
|
|
|
# See _apply_to_cert and _apply_image_to_cert in the wizard.
|
|
|
|
|
x_fc_thickness_image_id = fields.Many2one(
|
|
|
|
|
@@ -203,7 +203,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
|
|
|
|
|
@api.depends('sale_order_id')
|
|
|
|
|
def _compute_batch_ids(self):
|
|
|
|
|
# Phase 6 (Sub 11) — walks fp.job via SO instead of mrp.production.
|
|
|
|
|
# Phase 6 (Sub 11) - walks fp.job via SO instead of mrp.production.
|
|
|
|
|
Batch = self.env.get('fusion.plating.batch')
|
|
|
|
|
Bath = self.env['fusion.plating.bath']
|
|
|
|
|
Job = self.env.get('fp.job')
|
|
|
|
|
@@ -235,7 +235,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
string='Age (hours)',
|
|
|
|
|
compute='_compute_x_fc_age_hours',
|
|
|
|
|
help='Hours since the cert was created. Drives the Quality '
|
|
|
|
|
'Dashboard age chip and overdue filter. Non-stored — '
|
|
|
|
|
'Dashboard age chip and overdue filter. Non-stored - '
|
|
|
|
|
'always fresh on read. Spec 2026-05-25.',
|
|
|
|
|
)
|
|
|
|
|
void_reason = fields.Text(string='Void Reason')
|
|
|
|
|
@@ -340,7 +340,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
sigma = 0.0
|
|
|
|
|
rec.std_dev_mils = round(sigma, 4)
|
|
|
|
|
|
|
|
|
|
# Cpk — requires spec limits + non-zero sigma
|
|
|
|
|
# Cpk - requires spec limits + non-zero sigma
|
|
|
|
|
usl = rec.spec_max_mils or 0.0
|
|
|
|
|
lsl = rec.spec_min_mils or 0.0
|
|
|
|
|
if n < 5 or sigma == 0 or (usl == 0 and lsl == 0):
|
|
|
|
|
@@ -404,7 +404,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
def create(self, vals_list):
|
|
|
|
|
SaleOrder = self.env['sale.order']
|
|
|
|
|
for vals in vals_list:
|
|
|
|
|
# Spec-limit auto-fill — sources thickness range from the
|
|
|
|
|
# Spec-limit auto-fill - sources thickness range from the
|
|
|
|
|
# recipe (Phase A moved the thickness fields onto the
|
|
|
|
|
# recipe root). Falls back gracefully when the SO has no
|
|
|
|
|
# recipe-bearing line.
|
|
|
|
|
@@ -449,12 +449,12 @@ class FpCertificate(models.Model):
|
|
|
|
|
Single source of truth shared by action_issue (hard gate) and the
|
|
|
|
|
Issue-Certs wizard (readiness hint) so the two can never drift.
|
|
|
|
|
|
|
|
|
|
Partner side — the ceiling: a CoC needs thickness when the customer
|
|
|
|
|
Partner side - the ceiling: a CoC needs thickness when the customer
|
|
|
|
|
is strict-thickness (aerospace / Nadcap) or opts into the
|
|
|
|
|
thickness-on-CoC bundle; a thickness_report cert always needs it.
|
|
|
|
|
|
|
|
|
|
Recipe side — suppress-only: a recipe whose requires_thickness_report
|
|
|
|
|
is False (passivation, chemical conversion, anodize seal-only — no
|
|
|
|
|
Recipe side - suppress-only: a recipe whose requires_thickness_report
|
|
|
|
|
is False (passivation, chemical conversion, anodize seal-only - no
|
|
|
|
|
plating thickness physically exists) REMOVES the requirement even
|
|
|
|
|
when the customer asked. This mirrors Step 2 of
|
|
|
|
|
fp.job._resolve_required_cert_types so the cert-type resolver and
|
|
|
|
|
@@ -543,7 +543,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
# the common case and this fill is what locks the signer at
|
|
|
|
|
# issue. fp.certificate doesn't declare company_id directly,
|
|
|
|
|
# so resolve the company from the SO (then env.company) the
|
|
|
|
|
# same way the CoC report does — otherwise this fill is
|
|
|
|
|
# same way the CoC report does - otherwise this fill is
|
|
|
|
|
# skipped and the "Certified By is not set" gate below would
|
|
|
|
|
# block issuance.
|
|
|
|
|
if not rec.certified_by_id:
|
|
|
|
|
@@ -566,33 +566,33 @@ class FpCertificate(models.Model):
|
|
|
|
|
# when an estimator chooses to fill it, but it no longer
|
|
|
|
|
# blocks issuance.
|
|
|
|
|
# Process description (what was done to the parts). Without
|
|
|
|
|
# it the cert PDF just shows blank process text — customer
|
|
|
|
|
# it the cert PDF just shows blank process text - customer
|
|
|
|
|
# has no idea what they paid for. Auto-filled from the
|
|
|
|
|
# recipe at create time; manager can override before issuing.
|
|
|
|
|
if not rec.process_description:
|
|
|
|
|
raise UserError(_(
|
|
|
|
|
'Cannot issue certificate "%(name)s" — Process '
|
|
|
|
|
'Cannot issue certificate "%(name)s" - Process '
|
|
|
|
|
'Description is blank.\n\nFill it manually (e.g. '
|
|
|
|
|
'"ELECTROLESS NICKEL PLATING PER AMS 2404") or '
|
|
|
|
|
'assign a recipe to the job so it auto-fills.'
|
|
|
|
|
) % {'name': rec.name or rec.display_name})
|
|
|
|
|
# Signing authority — the human who attests the work. Auto-
|
|
|
|
|
# Signing authority - the human who attests the work. Auto-
|
|
|
|
|
# filled from per-spec signer_user_id, falling back to
|
|
|
|
|
# company.x_fc_owner_user_id. If neither is configured, the
|
|
|
|
|
# manager must pick before issuing.
|
|
|
|
|
if not rec.certified_by_id:
|
|
|
|
|
raise UserError(_(
|
|
|
|
|
'Cannot issue certificate "%(name)s" — Certified By '
|
|
|
|
|
'Cannot issue certificate "%(name)s" - Certified By '
|
|
|
|
|
'is not set.\n\nPick the signing authority, or have '
|
|
|
|
|
'an admin configure the company\'s Certificate Owner '
|
|
|
|
|
'(Settings > Fusion Plating).'
|
|
|
|
|
) % {'name': rec.name or rec.display_name})
|
|
|
|
|
# Customer contact — the named recipient printed on the
|
|
|
|
|
# Customer contact - the named recipient printed on the
|
|
|
|
|
# cert and emailed when it ships. Auto-filled from the FIRST
|
|
|
|
|
# of partner.x_fc_default_coc_contact_ids when set.
|
|
|
|
|
if not rec.contact_partner_ids:
|
|
|
|
|
raise UserError(_(
|
|
|
|
|
'Cannot issue certificate "%(name)s" — Customer '
|
|
|
|
|
'Cannot issue certificate "%(name)s" - Customer '
|
|
|
|
|
'Contact is not set.\n\nPick the recipient contact(s), '
|
|
|
|
|
'or configure Default CoC Contacts on customer '
|
|
|
|
|
'"%(cust)s".'
|
|
|
|
|
@@ -602,7 +602,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
})
|
|
|
|
|
if not (rec.contact_partner_ids[:1].email or '').strip():
|
|
|
|
|
raise UserError(_(
|
|
|
|
|
'Cannot issue certificate "%(name)s" — primary contact '
|
|
|
|
|
'Cannot issue certificate "%(name)s" - primary contact '
|
|
|
|
|
'"%(c)s" has no email address.\n\nAdd an email to the '
|
|
|
|
|
'contact before issuing (the cert is sent by email '
|
|
|
|
|
'post-issue).'
|
|
|
|
|
@@ -611,7 +611,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
'c': rec.contact_partner_ids[:1].name,
|
|
|
|
|
})
|
|
|
|
|
# Orphan cert types (Nadcap / Mill Test / Customer-Specific)
|
|
|
|
|
# are manual-attach only — operator uploads supplier doc /
|
|
|
|
|
# are manual-attach only - operator uploads supplier doc /
|
|
|
|
|
# regulator-issued cert / filled customer template. Block
|
|
|
|
|
# issuance until an attachment is present so the customer
|
|
|
|
|
# doesn't receive an empty PDF. See spec 2026-05-27.
|
|
|
|
|
@@ -622,7 +622,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
rec._fields['certificate_type'].selection
|
|
|
|
|
).get(rec.certificate_type, rec.certificate_type)
|
|
|
|
|
raise UserError(_(
|
|
|
|
|
'Cannot issue %(type)s "%(name)s" — no PDF attached.'
|
|
|
|
|
'Cannot issue %(type)s "%(name)s" - no PDF attached.'
|
|
|
|
|
'\n\nThis certificate type expects a PDF you upload '
|
|
|
|
|
'from disk (supplier doc / regulator-issued cert / '
|
|
|
|
|
'filled customer template). Upload the PDF to the '
|
|
|
|
|
@@ -631,14 +631,14 @@ class FpCertificate(models.Model):
|
|
|
|
|
'type': type_label,
|
|
|
|
|
'name': rec.name or rec.display_name,
|
|
|
|
|
})
|
|
|
|
|
# Thickness data requirement — _fp_needs_thickness_data is the
|
|
|
|
|
# Thickness data requirement - _fp_needs_thickness_data is the
|
|
|
|
|
# single source of truth (shared with the Issue-Certs wizard).
|
|
|
|
|
# A customer needs thickness data when the cert is a
|
|
|
|
|
# thickness_report, or it's a CoC and the partner is
|
|
|
|
|
# strict-thickness / opts into the thickness-on-CoC bundle —
|
|
|
|
|
# strict-thickness / opts into the thickness-on-CoC bundle -
|
|
|
|
|
# UNLESS the job's recipe suppresses thickness
|
|
|
|
|
# (requires_thickness_report=False: passivation, chemical
|
|
|
|
|
# conversion, anodize seal-only — no plating thickness exists).
|
|
|
|
|
# conversion, anodize seal-only - no plating thickness exists).
|
|
|
|
|
# Acceptable data: logged readings on the cert OR a Fischerscope
|
|
|
|
|
# PDF on the linked QC OR a cert-local Fischerscope upload.
|
|
|
|
|
partner = rec.partner_id
|
|
|
|
|
@@ -660,7 +660,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
else _('CoC')
|
|
|
|
|
)
|
|
|
|
|
raise UserError(_(
|
|
|
|
|
'Cannot issue %(type)s "%(name)s" — customer '
|
|
|
|
|
'Cannot issue %(type)s "%(name)s" - customer '
|
|
|
|
|
'"%(cust)s" requires thickness data on every '
|
|
|
|
|
'%(type)s. No readings, no Fischerscope PDF on '
|
|
|
|
|
'the linked QC, and no local Fischerscope upload '
|
|
|
|
|
@@ -674,10 +674,10 @@ class FpCertificate(models.Model):
|
|
|
|
|
'cust': partner.name if partner else '?',
|
|
|
|
|
'so': rec.sale_order_id.name if rec.sale_order_id else '?',
|
|
|
|
|
})
|
|
|
|
|
# Defensive qty reconciliation — should already be guaranteed
|
|
|
|
|
# Defensive qty reconciliation - should already be guaranteed
|
|
|
|
|
# by fp.job.button_mark_done's gate, but re-checked here so
|
|
|
|
|
# certs created outside the job flow (manual, scripts) still
|
|
|
|
|
# can't issue with a mismatched job. No bypass — qty integrity
|
|
|
|
|
# can't issue with a mismatched job. No bypass - qty integrity
|
|
|
|
|
# is non-negotiable at issue.
|
|
|
|
|
job = (rec.x_fc_job_id
|
|
|
|
|
if 'x_fc_job_id' in rec._fields else False)
|
|
|
|
|
@@ -690,7 +690,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
)
|
|
|
|
|
if abs(job.qty_received - accounted) > 0.0001:
|
|
|
|
|
raise UserError(_(
|
|
|
|
|
'Cannot issue certificate "%(name)s" — job '
|
|
|
|
|
'Cannot issue certificate "%(name)s" - job '
|
|
|
|
|
'%(job)s qty mismatch (received %(r)g vs '
|
|
|
|
|
'accounted-out %(a)g). Reconcile job '
|
|
|
|
|
'quantities before issuing.'
|
|
|
|
|
@@ -752,7 +752,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
if was_issued:
|
|
|
|
|
for rec in self:
|
|
|
|
|
if not was_issued.get(rec.id):
|
|
|
|
|
continue # wasn't issued — no regress
|
|
|
|
|
continue # wasn't issued - no regress
|
|
|
|
|
if ('x_fc_job_id' in rec._fields and rec.x_fc_job_id
|
|
|
|
|
and hasattr(rec.x_fc_job_id,
|
|
|
|
|
'_fp_check_regress_after_cert_void')):
|
|
|
|
|
@@ -788,8 +788,8 @@ class FpCertificate(models.Model):
|
|
|
|
|
mandatory; the customer-facing description (what the estimator
|
|
|
|
|
actually typed on the order) now carries the descriptive text.
|
|
|
|
|
|
|
|
|
|
Resolution — first non-empty wins:
|
|
|
|
|
1. The order line matching this cert's job/part — cleaned via
|
|
|
|
|
Resolution - first non-empty wins:
|
|
|
|
|
1. The order line matching this cert's job/part - cleaned via
|
|
|
|
|
sale.order.line.fp_customer_description() (strips the
|
|
|
|
|
"[code] product" prefix Odoo re-prepends).
|
|
|
|
|
2. Any product line on the linked sale order.
|
|
|
|
|
@@ -880,7 +880,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
the supporting evidence inline with the cert.
|
|
|
|
|
|
|
|
|
|
Tries the EN-language CoC report first, falls back to the
|
|
|
|
|
generic action_report_coc. Idempotent — skips if attachment_id
|
|
|
|
|
generic action_report_coc. Idempotent - skips if attachment_id
|
|
|
|
|
is already set. PDF merge is best-effort: corrupt Fischerscope
|
|
|
|
|
upload or missing pypdf falls back to CoC-only with a warning.
|
|
|
|
|
"""
|
|
|
|
|
@@ -888,7 +888,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
import io
|
|
|
|
|
self.ensure_one()
|
|
|
|
|
# Orphan cert types (Nadcap / Mill Test / Customer-Specific) are
|
|
|
|
|
# manual-attach only — never render a CoC QWeb template for them.
|
|
|
|
|
# manual-attach only - never render a CoC QWeb template for them.
|
|
|
|
|
# action_issue's precondition gate already enforces the
|
|
|
|
|
# attachment requirement, so by the time we get here the operator
|
|
|
|
|
# has uploaded the right PDF. See spec 2026-05-27 §4.
|
|
|
|
|
@@ -950,9 +950,9 @@ class FpCertificate(models.Model):
|
|
|
|
|
if self.certificate_type != 'coc':
|
|
|
|
|
return None
|
|
|
|
|
# Resolution order for the source of the Fischerscope bytes:
|
|
|
|
|
# 1. Cert-local upload (x_fc_local_thickness_pdf) — manager
|
|
|
|
|
# 1. Cert-local upload (x_fc_local_thickness_pdf) - manager
|
|
|
|
|
# dropped it directly on the cert form
|
|
|
|
|
# 2. Linked QC's thickness_report_pdf_id — operator uploaded
|
|
|
|
|
# 2. Linked QC's thickness_report_pdf_id - operator uploaded
|
|
|
|
|
# via the tablet during inspection
|
|
|
|
|
# Either path yields the same merged-PDF outcome.
|
|
|
|
|
fischer_bytes = b''
|
|
|
|
|
@@ -993,7 +993,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
)
|
|
|
|
|
if not fischer_bytes:
|
|
|
|
|
return None
|
|
|
|
|
# Merge — pypdf is the modern name; PyPDF2 still works on older
|
|
|
|
|
# Merge - pypdf is the modern name; PyPDF2 still works on older
|
|
|
|
|
# Odoo bundles. Either is fine.
|
|
|
|
|
try:
|
|
|
|
|
from pypdf import PdfWriter
|
|
|
|
|
@@ -1013,7 +1013,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
if use_append:
|
|
|
|
|
# pypdf 3.x — PdfWriter.append() handles bytes/streams
|
|
|
|
|
# pypdf 3.x - PdfWriter.append() handles bytes/streams
|
|
|
|
|
writer = writer_cls()
|
|
|
|
|
writer.append(io.BytesIO(coc_pdf_bytes))
|
|
|
|
|
writer.append(io.BytesIO(fischer_bytes))
|
|
|
|
|
@@ -1021,7 +1021,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
writer.write(out)
|
|
|
|
|
merged = out.getvalue()
|
|
|
|
|
else:
|
|
|
|
|
# PyPDF2 — PdfMerger.append + write
|
|
|
|
|
# PyPDF2 - PdfMerger.append + write
|
|
|
|
|
merger = writer_cls()
|
|
|
|
|
merger.append(io.BytesIO(coc_pdf_bytes))
|
|
|
|
|
merger.append(io.BytesIO(fischer_bytes))
|
|
|
|
|
@@ -1031,7 +1031,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
merged = out.getvalue()
|
|
|
|
|
except Exception:
|
|
|
|
|
_logger.exception(
|
|
|
|
|
'PDF merge failed for cert %s — Fischerscope PDF may '
|
|
|
|
|
'PDF merge failed for cert %s - Fischerscope PDF may '
|
|
|
|
|
'be corrupt / encrypted / malformed. Falling back to '
|
|
|
|
|
'CoC-only.', self.name,
|
|
|
|
|
)
|
|
|
|
|
@@ -1048,7 +1048,7 @@ class FpCertificate(models.Model):
|
|
|
|
|
def action_reset_to_draft(self):
|
|
|
|
|
"""Move an issued/voided cert back to draft so the manager can
|
|
|
|
|
correct typos in the thickness metadata, swap the microscope
|
|
|
|
|
image, re-pick the void reason, etc. — then re-Issue.
|
|
|
|
|
image, re-pick the void reason, etc. - then re-Issue.
|
|
|
|
|
|
|
|
|
|
Wipes the existing `attachment_id` so the next render picks up
|
|
|
|
|
whatever was changed. The original PDF stays around as a
|
|
|
|
|
@@ -1109,11 +1109,11 @@ class FpCertificate(models.Model):
|
|
|
|
|
|
|
|
|
|
def action_view_traceability(self):
|
|
|
|
|
"""Show the batches (and their chemistry logs) that produced
|
|
|
|
|
these parts — auditor's dream, customer's RMA friend."""
|
|
|
|
|
these parts - auditor's dream, customer's RMA friend."""
|
|
|
|
|
self.ensure_one()
|
|
|
|
|
return {
|
|
|
|
|
'type': 'ir.actions.act_window',
|
|
|
|
|
'name': _('Traceability — %s') % self.name,
|
|
|
|
|
'name': _('Traceability - %s') % self.name,
|
|
|
|
|
'res_model': 'fusion.plating.batch',
|
|
|
|
|
'view_mode': 'list,form',
|
|
|
|
|
'domain': [('id', 'in', self.batch_ids.ids)],
|
|
|
|
|
|