From 307afbf3c0b5a056ebeba679f988c950c7e1573c Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Thu, 28 May 2026 21:17:09 -0400 Subject: [PATCH] feat(plating): CoC spec-optional + SO-style header + thickness for any cert - Drop the hard spec_reference gate on fp.certificate.action_issue. The customer-facing description (_fp_resolve_customer_facing_description, walks job -> SO line, reuses fp_customer_description) now drives the CoC Process column; spec_reference prints only when an estimator fills it. - CoC EN/FR reports swap web.external_layout for fp_external_layout_clean + paperformat_fp_a4_portrait. New shared coc_header (company logo + address left, Nadcap logo centre, title + Code128 barcode right) mirrors the Sale Order header. Removed the 3-logo Nadcap/AS9100/CGP accreditation strip and the body H1s; padding-top 0 on both body wrappers. - Un-gate the Issue Certs wizard thickness upload (was invisible unless the customer was thickness-flagged) so a Fischerscope report can be attached to ANY cert; merge (page 2) + inline readings already render unconditionally. - Update issue-gate tests, bump versions (certificates 19.0.9.1.0, reports 19.0.11.27.0, jobs 19.0.11.2.0), record CLAUDE.md rule 14c. Deployed + render-verified on entech (CoC-30065, 223KB PDF, no QWeb errors). Co-Authored-By: Claude Opus 4.7 --- fusion_plating/CLAUDE.md | 1 + .../__manifest__.py | 2 +- .../models/fp_certificate.py | 64 ++++++- .../tests/test_action_issue_gates.py | 26 +-- .../fusion_plating_jobs/__manifest__.py | 2 +- .../wizards/fp_cert_issue_wizard_views.xml | 33 ++-- .../fusion_plating_reports/__manifest__.py | 2 +- .../report/report_actions.xml | 8 +- .../report/report_coc.xml | 175 +++++++++++------- .../report/report_coc_chronological.xml | 6 +- 10 files changed, 209 insertions(+), 110 deletions(-) diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index 6b269f75..312d5015 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -384,6 +384,7 @@ Use only: `name`, `model_id`, `state`, `code` (or `function`/`model`), `interval Applied to `.fp-report table.bordered`, `.fp-landscape table.bordered`, `.fp-report .totals-table`, and `.fp-report .sig-table`. If you need to add a new bordered table, follow the same longhand-border + background-clip template. 14b. **FP report signature source**: every FP report that prints a signer signature (WO Detail, CoC, CoC Chronological, future cert templates) reads from **`res.users.x_fc_signature_image`** — the "Plating Signature" the user uploads under Preferences → My Profile. Retired alternatives (2026-05-17): the HR Employee signature lookup (`user.employee_ids[:1].signature`) and the company-level Signature Override Image (`res.company.x_fc_coc_signature_override`). Both have been removed from all report templates and the company-level setting UI; the override column on `res.company` is kept for now (no migration) but is no longer read. **Don't re-introduce the HR-Employee or override patterns** — pick the signer user via whatever resolution chain the report needs (cert's `certified_by_id`, company's `x_fc_qa_manager_user_ids[:1]`, `job.manager_id`, `company.x_fc_owner_user_id`, etc.) then read `signer_user.x_fc_signature_image`. +14c. **CoC header + spec policy (client request 2026-05-28)**: the formal CoC reports (`report_coc_en` / `report_coc_fr`) render their OWN header (`coc_header` template in `report_coc.xml`) via `fusion_plating_reports.fp_external_layout_clean` — **NOT `web.external_layout`** — bound to `paperformat_fp_a4_portrait`. `coc_header` mirrors the Sale Order header (`report_fp_sale.xml`): company logo+address LEFT, **Nadcap accreditation logo CENTRE** (`company.x_fc_nadcap_logo`), document title + Code128 barcode of `doc.name` RIGHT. The old 3-logo accreditation strip (Nadcap/AS9100/CGP columns) was **removed** from `coc_body`; don't re-add it — Nadcap lives in the header now. The body wrappers (`.fp-coc`, `.fp-coc-chrono`) use `padding-top: 0` (no external_layout band to clear) and no longer carry an `

` (title is in the header, rendered once by the EN/FR wrapper for BOTH body styles). **`spec_reference` is NO LONGER a hard gate** on `fp.certificate.action_issue` — the customer-facing description (`fp.certificate._fp_resolve_customer_facing_description()`, walks `x_fc_job_id → sale_order_id.order_line` matching the part, reuses `sale.order.line.fp_customer_description()`) is the cert's Process/spec text, with `process_description` (recipe name) as fallback; `spec_reference` still prints below when an estimator fills it. The Issue Certs wizard's thickness upload fields are **un-gated** (no longer `invisible="not needs_thickness"`) so a Fischerscope report can be attached to ANY cert and pulled onto the CoC (merge as page 2 for PDFs / inline readings table for .docx/RTF — both already render unconditionally on data presence). NB: the `process_description` / `certified_by_id` / `contact_partner_id`(+email) gates on `action_issue` are SEPARATE and still enforced. 14. **Sticker template — leave the CSS units alone**: `report_fp_wo_sticker_inner` is calibrated for **px units at paperformat dpi=300** on entech's wkhtmltopdf. Do NOT "modernise" it by converting px→mm or by bumping paperformat dpi — both have been tried (2026-05-16) and both collapsed the layout (tiny logo, tiny QR, body grid shorter than the body band, font sizes visually smaller despite using pt). The math suggests the conversions should be equivalent, but wkhtmltopdf's px↔mm↔dpi mapping doesn't follow the obvious model on this image. Trust the working geometry, change only what you came to change. **Barcode size cap**: Odoo core raises `ValueError("Barcode too large")` when `width * height > 1_200_000` OR `max(width, height) > 10000` (see `base/models/ir_actions_report.py::barcode`). Largest safe square is ~1095×1095 — we use 1000×1000 to stay clear of the ceiling. **Em-dash mojibake**: wkhtmltopdf's default font on entech mojibakes em-dash (—), en-dash (–), smart quotes, and ellipsis into `â€"` etc. — strip them defensively for any free-text field that bleeds into the sticker (thickness, notes, line.name). The strip pattern is `.replace(u'—', '-').replace(u'–', '-')...` in `report_fp_wo_sticker_inner`. 15. **Recipe editor parity**: Step-level UX features (image attachments, prompt editing, settings toggles, preview affordances, etc.) MUST be implemented in BOTH the **Simple Editor** (`fusion_plating/static/src/{js,xml,scss}/simple_recipe_editor.*` + `controllers/simple_recipe_controller.py`) AND the **Tree Editor** (`fusion_plating/static/src/{js,xml,scss}/recipe_tree_editor.*` + `controllers/recipe_controller.py`). Authors choose between editors per-recipe via `preferred_editor`; if a feature only lands in one, half the userbase silently misses it. Default assumption: most clients use the Simple Editor — when in doubt, ship Simple first, then port to Tree in the same change. Backend model + view changes (e.g. new fields on `fusion.plating.process.node`, new tabs on the node form) automatically reach both editors via the related model — only the editor-specific JS/XML/SCSS needs duplicating. 16. **HTTP controller route override = method name must match parent**: To override a route on an inherited controller (e.g. `portal.CustomerPortal.home()` at `/my/home`), the override method MUST share the parent's method name. Declaring a new method name with the same `@http.route()` URL does NOT override — Odoo registers BOTH handlers as siblings and the parent typically wins, silently. Pattern: `class FpCustomerPortal(CustomerPortal): @http.route() def home(self, **kw): ...`. Bit us 2026-05-17 in `fusion_plating_portal/controllers/portal.py` — `portal_my_home_dashboard()` failed to override stock `home()`; symptom was the rich FP dashboard never rendering at `/my/home` even though the template was active in DB. diff --git a/fusion_plating/fusion_plating_certificates/__manifest__.py b/fusion_plating/fusion_plating_certificates/__manifest__.py index cc6c6c4a..d9f2ca1a 100644 --- a/fusion_plating/fusion_plating_certificates/__manifest__.py +++ b/fusion_plating/fusion_plating_certificates/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Certificates', - 'version': '19.0.9.0.0', + 'version': '19.0.9.1.0', 'category': 'Manufacturing/Plating', 'summary': 'Certificate registry for CoC, thickness reports, and quality documents.', 'description': """ diff --git a/fusion_plating/fusion_plating_certificates/models/fp_certificate.py b/fusion_plating/fusion_plating_certificates/models/fp_certificate.py index 359bb441..2fa39b86 100644 --- a/fusion_plating/fusion_plating_certificates/models/fp_certificate.py +++ b/fusion_plating/fusion_plating_certificates/models/fp_certificate.py @@ -496,16 +496,13 @@ class FpCertificate(models.Model): and 'x_fc_owner_user_id' in rec.company_id._fields and rec.company_id.x_fc_owner_user_id): rec.certified_by_id = rec.company_id.x_fc_owner_user_id - # Spec reference is what the cert ATTESTS — without it the - # cert is just a piece of paper. AS9100 / Nadcap require - # naming the spec the work was performed to. - if not rec.spec_reference: - raise UserError(_( - 'Cannot issue certificate "%(name)s" — no Spec ' - 'Reference set.\n\nFill the Spec Reference field ' - '(e.g. "AMS 2404", "MIL-C-26074") so the cert ' - 'states which standard the work meets.' - ) % {'name': rec.name or rec.display_name}) + # Spec Reference is OPTIONAL (client request 2026-05-28). + # The customer-facing description now serves as the cert's + # spec / certificate information (see + # _fp_resolve_customer_facing_description + the CoC Process + # column). spec_reference still prints below the description + # 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 # has no idea what they paid for. Auto-filled from the @@ -733,6 +730,53 @@ class FpCertificate(models.Model): return delivery.coc_attachment_id = self.attachment_id.id + def _fp_resolve_customer_facing_description(self): + """Resolve the customer-facing description used as the cert's + spec / certificate information on the printed CoC. + + Client request 2026-05-28: Spec Reference is no longer + 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 + sale.order.line.fp_customer_description() (strips the + "[code] product" prefix Odoo re-prepends). + 2. Any product line on the linked sale order. + Returns '' when no order context exists; the report template + then falls back to process_description. All cross-module field + access is guarded so the method stays safe even if the jobs / + configurator layers aren't installed. + """ + self.ensure_one() + job = self.x_fc_job_id if 'x_fc_job_id' in self._fields else False + so = self.sale_order_id or ( + job.sale_order_id + if job and 'sale_order_id' in job._fields else False + ) + if not so: + return '' + lines = so.order_line.filtered(lambda l: not l.display_type) + if not lines: + return '' + # Prefer the line whose part matches this cert's job. + part = (job.part_catalog_id + if job and 'part_catalog_id' in job._fields else False) + line = self.env['sale.order.line'] + if part and 'x_fc_part_catalog_id' in lines._fields: + line = lines.filtered( + lambda l: l.x_fc_part_catalog_id == part + )[:1] + if not line: + line = lines[:1] + if not line: + return '' + if hasattr(line, 'fp_customer_description'): + desc = line.fp_customer_description() + else: + desc = line.name + return (desc or '').strip() + def _fp_render_and_attach_pdf(self): """Render the CoC PDF via the bound report action, OPTIONALLY merge the Fischerscope thickness report PDF (uploaded by the diff --git a/fusion_plating/fusion_plating_certificates/tests/test_action_issue_gates.py b/fusion_plating/fusion_plating_certificates/tests/test_action_issue_gates.py index 70cdd301..016947bb 100644 --- a/fusion_plating/fusion_plating_certificates/tests/test_action_issue_gates.py +++ b/fusion_plating/fusion_plating_certificates/tests/test_action_issue_gates.py @@ -55,13 +55,16 @@ class TestActionIssueGates(TransactionCase): vals.update(kw) return self.env['fp.certificate'].create(vals) - # ---- the existing gate still works (spec_reference) ---- + # ---- spec_reference is OPTIONAL (client request 2026-05-28) ---- - def test_blocks_on_missing_spec_reference(self): + def test_no_block_on_missing_spec_reference(self): + """Spec Reference no longer gates issuance — the customer-facing + description now serves as the cert's spec/certificate info. A + cert with everything else present must issue even with a blank + spec_reference.""" cert = self._make_cert(spec_reference=False) - with self.assertRaises(UserError) as exc: - cert.action_issue() - self.assertIn('Spec Reference', str(exc.exception)) + cert.action_issue() + self.assertEqual(cert.state, 'issued') # ---- new gate: process_description ---- @@ -100,10 +103,12 @@ class TestActionIssueGates(TransactionCase): cert.action_issue() self.assertEqual(cert.state, 'issued') - # ---- order: spec_reference still wins (cheapest first) ---- + # ---- order: process_description is now the first gate ---- - def test_gate_order_spec_reference_first(self): - # Multiple missing → spec_reference message surfaces first. + def test_gate_order_process_description_first(self): + # spec_reference no longer gates, so with everything missing the + # process_description message surfaces first; the (removed) Spec + # Reference message must NOT appear. cert = self._make_cert( spec_reference=False, process_description=False, @@ -112,9 +117,8 @@ class TestActionIssueGates(TransactionCase): ) with self.assertRaises(UserError) as exc: cert.action_issue() - self.assertIn('Spec Reference', str(exc.exception)) - # And NOT the process_description message (gate hit first). - self.assertNotIn('Process Description', str(exc.exception)) + self.assertIn('Process Description', str(exc.exception)) + self.assertNotIn('Spec Reference', str(exc.exception)) # ---- new gate: thickness_report cert needs thickness data ---- diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index 20aae879..1097367a 100644 --- a/fusion_plating/fusion_plating_jobs/__manifest__.py +++ b/fusion_plating/fusion_plating_jobs/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating — Native Jobs', - 'version': '19.0.11.1.0', + 'version': '19.0.11.2.0', 'category': 'Manufacturing/Plating', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'author': 'Nexa Systems Inc.', diff --git a/fusion_plating/fusion_plating_jobs/wizards/fp_cert_issue_wizard_views.xml b/fusion_plating/fusion_plating_jobs/wizards/fp_cert_issue_wizard_views.xml index 5fa19a03..693278e3 100644 --- a/fusion_plating/fusion_plating_jobs/wizards/fp_cert_issue_wizard_views.xml +++ b/fusion_plating/fusion_plating_jobs/wizards/fp_cert_issue_wizard_views.xml @@ -48,20 +48,22 @@ - + + string="Fischerscope File (PDF or .docx)"/> + invisible="not parsed_summary"/> - + - + - -

+ +

Auto-filled from the .docx upload above. Edit/add rows manually as needed.

- + diff --git a/fusion_plating/fusion_plating_reports/__manifest__.py b/fusion_plating/fusion_plating_reports/__manifest__.py index 5dccdc61..2fb1c471 100644 --- a/fusion_plating/fusion_plating_reports/__manifest__.py +++ b/fusion_plating/fusion_plating_reports/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating — Reports', - 'version': '19.0.11.26.31', + 'version': '19.0.11.27.0', 'category': 'Manufacturing/Plating', 'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.', 'depends': [ diff --git a/fusion_plating/fusion_plating_reports/report/report_actions.xml b/fusion_plating/fusion_plating_reports/report/report_actions.xml index a6429a19..c764ef70 100644 --- a/fusion_plating/fusion_plating_reports/report/report_actions.xml +++ b/fusion_plating/fusion_plating_reports/report/report_actions.xml @@ -122,8 +122,10 @@ - - + + + + Certificate of Conformance (English) @@ -134,6 +136,7 @@ 'CoC EN - %s' % object.name report + @@ -148,6 +151,7 @@ 'CoC FR - %s' % object.name report + diff --git a/fusion_plating/fusion_plating_reports/report/report_coc.xml b/fusion_plating/fusion_plating_reports/report/report_coc.xml index 11fff39d..4be8214e 100644 --- a/fusion_plating/fusion_plating_reports/report/report_coc.xml +++ b/fusion_plating/fusion_plating_reports/report/report_coc.xml @@ -22,7 +22,91 @@ - + + + + + + + + + +