diff --git a/fusion_plating/fusion_plating_compliance/__manifest__.py b/fusion_plating/fusion_plating_compliance/__manifest__.py index 3281beb4..ae00319c 100644 --- a/fusion_plating/fusion_plating_compliance/__manifest__.py +++ b/fusion_plating/fusion_plating_compliance/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating - Compliance (Framework)', - 'version': '19.0.1.0.0', + 'version': '19.0.1.1.0', 'category': 'Manufacturing/Plating', 'summary': 'Jurisdiction-agnostic compliance framework: permits, discharge monitoring, waste manifests, pollutant inventory, compliance calendar, spill register.', 'description': 'Generic compliance framework. Region packs load jurisdiction-specific data.', diff --git a/fusion_plating/fusion_plating_compliance/models/fp_discharge_sample.py b/fusion_plating/fusion_plating_compliance/models/fp_discharge_sample.py index 54ff426c..720b7617 100644 --- a/fusion_plating/fusion_plating_compliance/models/fp_discharge_sample.py +++ b/fusion_plating/fusion_plating_compliance/models/fp_discharge_sample.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError class FpDischargeSample(models.Model): @@ -63,4 +64,32 @@ class FpDischargeSample(models.Model): self.write({'state': 'escalated'}) def action_close(self): + """Block close until lab evidence is on file. + + A closed discharge sample without a lab report ref + at least + one parameter reading + (when results are in) a lab cert + attachment fails any environmental audit. The whole point + of the record is to document the test was performed and what + the lab said. + """ + for rec in self: + missing = [] + if not rec.lab_report_ref: + missing.append(_('Lab Report #')) + if not rec.received_date: + missing.append(_('Results Received Date')) + if not rec.line_ids: + missing.append(_('At least one parameter reading')) + if not rec.attachment_ids: + missing.append(_('Lab certificate / report attachment')) + if missing: + raise UserError(_( + 'Cannot close discharge sample "%(name)s" — these ' + 'fields must be filled in first:\n • %(fields)s\n\n' + 'Without lab evidence on file the record fails any ' + 'environmental compliance audit.' + ) % { + 'name': rec.name or rec.display_name, + 'fields': '\n • '.join(missing), + }) self.write({'state': 'closed'}) diff --git a/fusion_plating/fusion_plating_invoicing/__manifest__.py b/fusion_plating/fusion_plating_invoicing/__manifest__.py index 86c91529..1de47e22 100644 --- a/fusion_plating/fusion_plating_invoicing/__manifest__.py +++ b/fusion_plating/fusion_plating_invoicing/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Invoicing', - 'version': '19.0.2.1.0', + 'version': '19.0.2.2.0', 'category': 'Manufacturing/Plating', 'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.', 'description': """ diff --git a/fusion_plating/fusion_plating_invoicing/models/account_move.py b/fusion_plating/fusion_plating_invoicing/models/account_move.py index 9c6ac8de..8f645cf0 100644 --- a/fusion_plating/fusion_plating_invoicing/models/account_move.py +++ b/fusion_plating/fusion_plating_invoicing/models/account_move.py @@ -12,21 +12,39 @@ class AccountMove(models.Model): @api.model_create_multi def create(self, vals_list): - """Auto-inherit payment terms from the customer when missing. + """Auto-inherit payment terms + customer PO# at creation time. - Customers usually have a default `property_payment_term_id` - (Net-30, Net-60, COD…). When an invoice is created without - terms, the due date silently defaults to "immediate" — wrong - for almost every B2B customer. Pull the partner's terms in - before super so the invoice is born with the right schedule. + Two defensive defaults so newly-created invoices come out + compliant out of the box: + + 1. **invoice_payment_term_id** — pulled from the customer's + property_payment_term_id (Net-30, COD, etc.). Without this + the due date silently becomes "immediate", wrong for B2B. + + 2. **ref** (customer reference / PO#) — pulled from the source + sale order's client_order_ref or x_fc_po_number. Customer + AP teams reject invoices that don't quote their PO# back. + We already populate this on the SO confirm path, but a + manually-created invoice would miss it without this default. """ Partner = self.env['res.partner'] + SO = self.env['sale.order'] for vals in vals_list: if vals.get('move_type') in ('out_invoice', 'out_refund'): if not vals.get('invoice_payment_term_id') and vals.get('partner_id'): partner = Partner.browse(vals['partner_id']) if partner.property_payment_term_id: vals['invoice_payment_term_id'] = partner.property_payment_term_id.id + # Defensive PO#: invoice_origin links to the SO; pull the + # customer ref from there if the caller didn't pass one. + if not vals.get('ref') and vals.get('invoice_origin'): + so = SO.search([('name', '=', vals['invoice_origin'])], limit=1) + if so: + vals['ref'] = ( + so.client_order_ref + or (so.x_fc_po_number if 'x_fc_po_number' in so._fields else False) + or False + ) return super().create(vals_list) def action_post(self): diff --git a/fusion_plating/fusion_plating_quality/__manifest__.py b/fusion_plating/fusion_plating_quality/__manifest__.py index 630d728b..89f5e0c0 100644 --- a/fusion_plating/fusion_plating_quality/__manifest__.py +++ b/fusion_plating/fusion_plating_quality/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Quality (QMS)', - 'version': '19.0.1.1.0', + 'version': '19.0.1.2.0', 'category': 'Manufacturing/Plating', 'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, ' 'internal audits, customer specs, document control. CE + EE compatible.', diff --git a/fusion_plating/fusion_plating_quality/models/fp_capa.py b/fusion_plating/fusion_plating_quality/models/fp_capa.py index c407ca19..e3fc74fb 100644 --- a/fusion_plating/fusion_plating_quality/models/fp_capa.py +++ b/fusion_plating/fusion_plating_quality/models/fp_capa.py @@ -3,7 +3,8 @@ # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError class FpCapa(models.Model): @@ -160,6 +161,43 @@ class FpCapa(models.Model): }) def action_close(self): + """Block close unless root_cause + action_plan + verification are set. + + A CAPA without these is just an open ticket — the AS9100 §10.2 + / Nadcap loop requires evidence of the root cause analysis, + the corrective/preventive action plan, AND that effectiveness + was verified before the loop is closed. + """ + for rec in self: + missing = [] + + def is_empty_html(val): + if not val: + return True + s = str(val).replace('
', '').replace('
', '') + s = s.replace('', '').replace('
', '') + s = s.replace('