feat(plating): close 6 compliance gaps from required-fields audit

Following the workforce-E2E + required-fields audit, ship the first 6
high-priority gates so critical workflow + compliance fields can no
longer be left empty by accident.

**1. Invoice payment terms (account.move)**
- create() now auto-inherits `invoice_payment_term_id` from
  partner.property_payment_term_id when missing
- action_post() raises UserError if still missing — accountant must
  pick one before posting (prevents silent "immediate" due-date)

**2. MO facility (mrp.production)**
- action_confirm() auto-derives `x_fc_facility_id` if unset, in order:
  SO override → res.company.x_fc_default_facility_id → first active
  facility — then HARD GATES: raises UserError if still empty.
  Without facility every downstream record (WO, batch, bath log,
  cert) is missing the "where" half of the audit trail.

**3. WO facility (mrp.workorder)**
- Switched `x_fc_facility_id` from related (workcenter only) to a
  proper compute that falls back to production_id.x_fc_facility_id.
  Stub workcenters auto-created from process node names usually have
  no facility — the MO always does (from #2 above).

**4. Thickness reading calibration_std (fp.thickness.reading)**
- `calibration_std_ref` is now `required=True` with sensible default
  ("NiP/Al STD SET SN 100174568"). Nadcap mandates which calibration
  standard the gauge was checked against — without it the cert
  data has no chain back to a metrology record.

**5. Delivery POD gate (fusion.plating.delivery)**
- action_mark_delivered() raises UserError if no `pod_id`. Driver
  must capture POD on the iPad (recipient signature + photos +
  notes) BEFORE marking delivered. Without POD there's no signed
  receipt to back the invoice or defend a delivery dispute.

**6. Certificate spec_reference gate (fp.certificate)**
- action_issue() raises UserError if no `spec_reference`. The cert
  ATTESTS to a spec — leaving it blank produces a piece of paper
  that AS9100 / Nadcap auditors will (rightfully) reject.

**Simulator updated**: scripts/fp_e2e_workforce.py
- Sets net-30 on the test customer + ensures a default facility
- New PHASE 4c: 5 negative tests (one per new gate), each wrapped
  in a SAVEPOINT so SQL constraint violations don't abort the txn
- Driver now creates POD on iPad BEFORE marking delivered

**Final E2E**: 48 PASS / 2 WARN / 0 FAIL out of 50 checks.
The 2 remaining WARNs (bake-window auto-create, first-piece gate)
are expected behaviour — both are coating-driven and the test
coating intentionally doesn't trigger them.

All 7 negative tests now pass:
  ✓ Test 1: WO start without operator → blocked
  ✓ Test 2: WO start on wet WO without bath/tank → blocked
  ✓ Test 3: MO confirm without facility → blocked
  ✓ Test 4: Cert issue without spec_reference → blocked
  ✓ Test 5: Delivery delivered without POD → blocked
  ✓ Test 6: Invoice post without payment terms → blocked
  ✓ Test 7: Thickness reading without cal std → blocked (DB NOT NULL)

Audit script (scripts/fp_required_fields_audit.py) committed too —
it's the diagnostic that surfaced these gaps and can be re-run to
catch new ones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 10:06:52 -04:00
parent 4161f04b0f
commit db8b79d22e
12 changed files with 612 additions and 9 deletions

View File

@@ -114,6 +114,18 @@ customer = env['res.partner'].sudo().create({
'city': 'Toronto', 'zip': 'M5G 1V7',
'country_id': env.ref('base.ca').id,
})
# Net-30 default so invoices created later inherit the right schedule.
net30 = env.ref('account.account_payment_term_30days', raise_if_not_found=False)
if net30:
customer.sudo().property_payment_term_id = net30.id
# Make sure the company has a default facility so MO confirm succeeds.
co = env.company
if not co.x_fc_default_facility_id:
f = env['fusion.plating.facility'].search([('active', '=', True)], limit=1)
if f:
co.sudo().x_fc_default_facility_id = f.id
show('company default facility set', f.name)
step('SANDRA', f'Receives RFQ from {customer.name}')
@@ -336,6 +348,133 @@ if wet_assignments:
'x_fc_tank_id': saved_tank,
})
# ===== Negative tests for the 6 new gates (wrapped in savepoints
# so an SQL-level constraint failure doesn't abort the txn) =====
banner('PHASE 4c — Negative tests for the new compliance gates')
def neg_test(label, fn, expect_keywords):
"""Run fn() inside a savepoint; check the raised error mentions
one of `expect_keywords`. Always rolls back."""
sp_name = f'neg_{abs(hash(label))}'
env.cr.execute(f'SAVEPOINT {sp_name}')
fired = False
msg = ''
try:
fn()
except Exception as e:
msg = str(e)
low = msg.lower()
fired = any(k.lower() in low for k in expect_keywords)
finally:
env.cr.execute(f'ROLLBACK TO SAVEPOINT {sp_name}')
if msg:
show(' blocked with', msg.splitlines()[0][:120])
finding('PASS' if fired else 'FAIL',
f'gate: {label}',
'blocked' if fired else f'NOT blocked (got: {msg[:60]!r})')
# Test 3: MO confirm without facility → expect block
step('SYSTEM', 'Test 3 — MO confirm with no facility → blocked')
def t_mo_facility():
saved_default = env.company.x_fc_default_facility_id
env.company.sudo().x_fc_default_facility_id = False
fac0 = env['fusion.plating.facility'].search([('active', '=', True)])
fac0.sudo().write({'active': False})
try:
m = env['mrp.production'].sudo().create({
'product_id': mo.product_id.id,
'product_qty': 1,
'company_id': env.company.id,
})
m.action_confirm() # should raise — no facility resolvable
finally:
fac0.sudo().write({'active': True})
env.company.sudo().x_fc_default_facility_id = saved_default
neg_test('MO confirm without facility', t_mo_facility,
['facility'])
# Test 4: Cert issue without spec_reference
step('SYSTEM', 'Test 4 — Cert action_issue() without spec_reference → blocked')
def t_cert_spec():
c = env['fp.certificate'].sudo().create({
'partner_id': customer.id,
'production_id': mo.id,
'certificate_type': 'coc',
'spec_reference': False,
})
c.action_issue()
neg_test('cert issue without spec_reference', t_cert_spec,
['Spec', 'spec_reference'])
# Test 5: Delivery mark_delivered without POD
step('SYSTEM', 'Test 5 — Delivery mark_delivered() with no POD → blocked')
def t_dlv_pod():
d = env['fusion.plating.delivery'].sudo().create({
'partner_id': customer.id,
'state': 'en_route',
'company_id': env.company.id,
})
d.action_mark_delivered()
neg_test('delivery delivered without POD', t_dlv_pod,
['POD', 'Proof of Delivery'])
# Test 6: Invoice post without payment terms
step('SYSTEM', 'Test 6 — Invoice post() with no payment terms → blocked')
def t_inv_terms():
saved_term = customer.property_payment_term_id
customer.sudo().property_payment_term_id = False
try:
i = env['account.move'].sudo().create({
'move_type': 'out_invoice',
'partner_id': customer.id,
'invoice_date': fields.Date.today(),
'invoice_line_ids': [(0, 0, {
'name': 'Test plating service',
'quantity': 1,
'price_unit': 100.0,
})],
})
i.invoice_payment_term_id = False
i.action_post()
finally:
customer.sudo().property_payment_term_id = saved_term
neg_test('invoice post without payment terms', t_inv_terms,
['payment term'])
# Test 7: Thickness reading without calibration_std_ref
step('SYSTEM', 'Test 7 — Thickness reading without calibration_std_ref → blocked')
def t_thickness_cal():
env['fp.thickness.reading'].sudo().create({
'production_id': mo.id,
'reading_number': 99,
'nip_mils': 0.05,
'calibration_std_ref': False,
})
neg_test('thickness reading without cal std', t_thickness_cal,
['calibration', 'required', 'not-null', 'null value'])
# =====================================================================
banner('PHASE 5 — Operators run their work orders (REAL-TIME timers)')
# =====================================================================
@@ -514,9 +653,26 @@ if dlv:
try:
if dlv.state == 'draft': dlv.with_user(users['dave']).sudo().action_schedule()
if dlv.state == 'scheduled': dlv.with_user(users['dave']).sudo().action_start_route()
# POD must be captured BEFORE marking delivered (new gate)
if dlv.state == 'en_route' and not dlv.pod_id:
step('DAVE', 'Captures POD on iPad — recipient signs + photo')
POD = env['fusion.plating.proof.of.delivery']
pod = POD.with_user(users['dave']).sudo().create({
'delivery_id': dlv.id,
'partner_id': dlv.partner_id.id,
'recipient_name': 'Dock Receiver',
'notes': 'E2E sim — recipient on dock signed for parts',
})
dlv.sudo().pod_id = pod.id
show(' POD captured', f'{pod.name} (id={pod.id})')
if dlv.state == 'en_route': dlv.with_user(users['dave']).sudo().action_mark_delivered()
except Exception as e:
print(f' [info] delivery transitions: {e}')
# ===== Negative test: try to mark another delivery delivered without POD =====
finding('PASS' if dlv.pod_id else 'FAIL',
'POD captured before delivery',
f'pod_id={dlv.pod_id.name if dlv.pod_id else "NONE"}')
finding('PASS' if dlv.state == 'delivered' else 'FAIL',
'delivery final state', dlv.state)
coc_logs = env['fusion.plating.chain.of.custody'].search(

View File

@@ -0,0 +1,338 @@
# -*- coding: utf-8 -*-
"""Comprehensive required-fields audit.
For each major model in the quote → invoice workflow:
• Lists fields currently marked `required=True` in the schema
• For the most recent COMPLETED record, shows which compliance-
relevant fields are empty (gap candidates)
• Classifies each gap by severity:
CRITICAL — compliance blocker (aerospace / Nadcap / env.)
IMPORTANT — workflow / operational risk
NICE — would improve reporting
The report is purely diagnostic — it changes nothing in the DB.
"""
env = env # noqa
from collections import defaultdict
def section(title):
print(f'\n{"="*78}\n {title}\n{"="*78}')
def show_field_audit(model_name, record, candidate_fields):
"""For one record, show which of `candidate_fields` are empty.
candidate_fields: list of (field, severity, reason) tuples
"""
if not record:
print(f' (no record found for {model_name})')
return
print(f' Record: {record.display_name} (id={record.id})')
# First show what's currently required in the schema
required_in_schema = [
n for n, f in record._fields.items()
if getattr(f, 'required', False)
]
print(f' Already required in schema: {len(required_in_schema)}')
print(f' Candidate fields needing enforcement:')
for field, severity, reason in candidate_fields:
if field not in record._fields:
continue
val = record[field]
is_empty = (
not val
or (hasattr(val, '_name') and not val.ids)
or val in ('', False, 0, 0.0)
)
sym = {'CRITICAL': '🔴', 'IMPORTANT': '🟡', 'NICE': ''}[severity]
marker = '✗ EMPTY' if is_empty else '✓ filled'
val_str = str(val)[:60] if not is_empty else ''
print(f' {sym} {severity:<9} {field:<32} {marker:<10} {reason}')
print(f' currently: {val_str!r}')
# =====================================================================
section('1. Customer (res.partner) — most recently used customer')
# =====================================================================
partner = env['sale.order'].search([], order='id desc', limit=1).partner_id
show_field_audit('res.partner', partner, [
('email', 'CRITICAL', 'Notifications + portal access — silent fail without it'),
('phone', 'IMPORTANT', 'Operator can call for clarification'),
('street', 'CRITICAL', 'Required on BoL + Invoice + delivery — no shipping without'),
('city', 'CRITICAL', 'Same'),
('zip', 'CRITICAL', 'Same'),
('country_id', 'CRITICAL', 'Determines tax + ITAR / CGP rules'),
('vat', 'IMPORTANT', 'HST/GST registration number — needed on invoice'),
('property_payment_term_id', 'IMPORTANT', 'Net-30 vs Net-60 controls invoice due date'),
('x_fc_account_hold', 'NICE', 'Default False is fine; only set when collections issue'),
('x_fc_send_coc', 'NICE', 'Per-customer CoC delivery preference'),
])
# =====================================================================
section('2. Sale Order (sale.order) — most recent SO')
# =====================================================================
so = env['sale.order'].search([], order='id desc', limit=1)
show_field_audit('sale.order', so, [
('partner_id', 'CRITICAL', 'Already required by Odoo'),
('client_order_ref', 'CRITICAL', 'Customer PO# — every aero customer requires this on every doc'),
('x_fc_po_number', 'CRITICAL', 'Same — FP-specific mirror'),
('x_fc_coating_config_id', 'CRITICAL', 'Drives recipe + price + spec'),
('x_fc_part_catalog_id', 'IMPORTANT', 'Part the order is about — needed for traceability'),
('x_fc_delivery_method', 'IMPORTANT', 'Pickup / drop / courier — drives logistics'),
('x_fc_rfq_attachment_id', 'NICE', 'Original customer RFQ for audit trail'),
('x_fc_po_attachment_id', 'IMPORTANT', 'Customer signed PO PDF'),
('payment_term_id', 'IMPORTANT', 'Net terms — derived from customer if unset'),
('user_id', 'IMPORTANT', 'Salesperson — needed for commission + handoff'),
])
# =====================================================================
section('3. Receiving (fp.receiving) — most recent record')
# =====================================================================
recv = env['fp.receiving'].search([], order='id desc', limit=1)
show_field_audit('fp.receiving', recv, [
('sale_order_id', 'CRITICAL', 'Without this we lose the link to the job'),
('partner_id', 'CRITICAL', 'Customer (related, but can drift)'),
('received_by_id', 'CRITICAL', 'Who counted the parts (audit trail)'),
('received_date', 'CRITICAL', 'When the parts arrived (compliance + start-clock)'),
('expected_qty', 'CRITICAL', 'Without this no qty-match check'),
('received_qty', 'CRITICAL', 'The actual count (compliance — discrepancy log)'),
('carrier_name', 'IMPORTANT', 'Who delivered — chain-of-custody starts here'),
('carrier_tracking', 'IMPORTANT', 'Inbound tracking #'),
('notes', 'NICE', 'Free-form receiver observations'),
])
# =====================================================================
section('4. MRP Production (mrp.production) — most recent MO')
# =====================================================================
mo = env['mrp.production'].search([('state', '=', 'done')], order='id desc', limit=1)
show_field_audit('mrp.production', mo, [
('product_id', 'CRITICAL', 'Already required by Odoo'),
('product_qty', 'CRITICAL', 'Same'),
('x_fc_facility_id', 'CRITICAL', 'Where the job is being made (compliance)'),
('x_fc_recipe_id', 'CRITICAL', 'Which process — without it WOs can\'t be generated'),
('x_fc_assigned_manager_id','IMPORTANT','Manager responsible for the job'),
('x_fc_customer_spec_id','IMPORTANT', 'Customer spec controlling the job (e.g. AMS 2404)'),
('x_fc_portal_job_id', 'IMPORTANT', 'Portal-facing job tracker'),
('origin', 'CRITICAL', 'Source SO — needed for back-link'),
('company_id', 'CRITICAL', 'Multi-company correctness (just fixed)'),
])
# =====================================================================
section('5. Work Orders (mrp.workorder) — wet WO from most recent MO')
# =====================================================================
wet_wo = mo.workorder_ids.filtered(
lambda w: hasattr(w, '_fp_is_wet_process') and w._fp_is_wet_process()
)[:1] if mo else env['mrp.workorder']
show_field_audit('mrp.workorder', wet_wo, [
('x_fc_assigned_user_id', 'CRITICAL', 'NOW ENFORCED via button_start gate'),
('x_fc_bath_id', 'CRITICAL', 'NOW ENFORCED — chemistry traceability'),
('x_fc_tank_id', 'CRITICAL', 'NOW ENFORCED — physical tank audit'),
('x_fc_facility_id', 'CRITICAL', 'Which plant ran it (multi-facility shops)'),
('x_fc_thickness_target', 'IMPORTANT', 'Spec target — drives QC accept/reject criteria'),
('x_fc_dwell_time_minutes','IMPORTANT','Recipe dwell — needed for cycle-time analytics'),
('x_fc_rack_id', 'IMPORTANT', 'Which rack/fixture used (per-rack MTO tracking)'),
('x_fc_started_by_user_id','IMPORTANT','Who actually started it (audit, may differ from assigned)'),
('x_fc_finished_by_user_id','IMPORTANT','Who finished it'),
])
# =====================================================================
section('6. Bath Log (fusion.plating.bath.log)')
# =====================================================================
baths = env['fusion.plating.bath.log'].search([], order='id desc', limit=1)
show_field_audit('fusion.plating.bath.log', baths, [
('bath_id', 'CRITICAL', 'Which bath the readings came from'),
('shift', 'IMPORTANT', 'Day/swing/night — for shift-effect analysis'),
('user_id', 'CRITICAL', 'Operator who took the readings (audit trail)'),
('logged_at', 'CRITICAL', 'When the readings were taken'),
('line_ids', 'CRITICAL', 'The actual chemistry numbers (the whole point)'),
('notes', 'NICE', 'Free-form observations'),
])
# =====================================================================
section('7. Certificate (fp.certificate) — most recent CoC')
# =====================================================================
coc = env['fp.certificate'].search(
[('certificate_type', '=', 'coc')], order='id desc', limit=1)
show_field_audit('fp.certificate', coc, [
('partner_id', 'CRITICAL', 'Customer the cert belongs to'),
('production_id', 'CRITICAL', 'Which MO it certifies'),
('po_number', 'CRITICAL', 'Customer PO — required by aero specs'),
('spec_reference', 'CRITICAL', 'AMS 2404 / MIL-C-26074 etc. — what was met'),
('process_description','IMPORTANT','Human-readable process name'),
('part_number', 'IMPORTANT', 'Part the cert covers'),
('quantity_shipped', 'CRITICAL', 'How many parts certified'),
('thickness_reading_ids','CRITICAL','Fischerscope readings (NOW AUTO-LINKED)'),
('attachment_id', 'CRITICAL', 'The PDF itself (NOW AUTO-RENDERED)'),
('issued_by_id', 'CRITICAL', 'Inspector signature — who certified this'),
('issued_date', 'CRITICAL', 'When issued'),
('state', 'CRITICAL', 'draft/issued/voided — NOT issued = NOT compliant'),
])
# =====================================================================
section('8. Thickness Reading (fp.thickness.reading)')
# =====================================================================
reading = env['fp.thickness.reading'].search([], order='id desc', limit=1)
show_field_audit('fp.thickness.reading', reading, [
('production_id', 'CRITICAL', 'Which MO this reading is from'),
('certificate_id', 'CRITICAL', 'Which cert (auto-linked at MO done)'),
('reading_number', 'CRITICAL', 'Sequence (n=1, n=2, n=3 — Nadcap requires this)'),
('nip_mils', 'CRITICAL', 'The thickness measurement itself'),
('ni_percent', 'IMPORTANT', 'Composition — affects bath chemistry diagnosis'),
('p_percent', 'IMPORTANT', 'Same'),
('position_label', 'CRITICAL', 'WHERE on the part (Nadcap requires location)'),
('equipment_model', 'CRITICAL', 'Which gauge — calibration trail'),
('calibration_std_ref', 'CRITICAL', 'Which calibration standard — Nadcap req'),
('operator_id', 'CRITICAL', 'Who took the reading'),
('reading_datetime', 'CRITICAL', 'When'),
])
# =====================================================================
section('9. Delivery (fusion.plating.delivery)')
# =====================================================================
dlv = env['fusion.plating.delivery'].search(
[('state', '=', 'delivered')], order='id desc', limit=1)
show_field_audit('fusion.plating.delivery', dlv, [
('partner_id', 'CRITICAL', 'Already required'),
('scheduled_date', 'CRITICAL', 'When the customer expects parts (NOW PREFILLED)'),
('assigned_driver_id', 'CRITICAL', 'Who is driving (NOW PREFILLED)'),
('vehicle_id', 'IMPORTANT', 'Which vehicle (insurance + GPS)'),
('delivered_at', 'CRITICAL', 'When delivery was completed'),
('contact_name', 'IMPORTANT', 'Recipient on the receiving dock'),
('contact_phone', 'IMPORTANT', 'Driver can call before arriving'),
('coc_attachment_id', 'CRITICAL', 'CoC PDF that goes with the parts'),
('packing_list_attachment_id','IMPORTANT','Packing slip'),
('delivery_address_id','IMPORTANT', 'Override default partner ship-to'),
('pod_id', 'CRITICAL', 'Proof of delivery — without it, we can\'t bill'),
])
# =====================================================================
section('10. Invoice (account.move) — most recent posted invoice')
# =====================================================================
inv = env['account.move'].search(
[('move_type', '=', 'out_invoice'), ('state', '=', 'posted')],
order='id desc', limit=1)
show_field_audit('account.move', inv, [
('partner_id', 'CRITICAL', 'Already required'),
('invoice_date', 'CRITICAL', 'When invoiced — drives net-terms clock'),
('invoice_date_due', 'CRITICAL', 'When payment due'),
('invoice_payment_term_id','CRITICAL', 'Net-30 etc.'),
('invoice_user_id', 'IMPORTANT', 'Salesperson — for commission'),
('partner_bank_id', 'IMPORTANT', 'Where to wire payment'),
('ref', 'CRITICAL', 'Customer PO# / reference (required by AP teams)'),
('invoice_origin', 'CRITICAL', 'Source SO link'),
('narration', 'NICE', 'Free-form notes'),
])
# =====================================================================
section('11. Workforce — Quality Hold + NCR + CAPA (open + completed)')
# =====================================================================
# Sample Quality Hold if any
qh = env.get('fusion.plating.quality.hold')
if qh is not None:
rec = qh.search([], order='id desc', limit=1)
show_field_audit('fusion.plating.quality.hold', rec, [
('partner_id', 'CRITICAL', 'Customer — without it we can\'t notify'),
('mo_id', 'CRITICAL', 'Which MO'),
('hold_reason', 'CRITICAL', 'Selection — categorize the issue'),
('description', 'CRITICAL', 'Inspector\'s narrative'),
('qty_on_hold', 'CRITICAL', 'How many parts affected'),
('inspector_id', 'CRITICAL', 'Who flagged it'),
('created_at', 'CRITICAL', 'When'),
])
ncr = env.get('fusion.plating.ncr')
if ncr is not None:
rec = ncr.search([], order='id desc', limit=1)
show_field_audit('fusion.plating.ncr', rec, [
('name', 'CRITICAL', 'NCR# / sequence'),
('partner_id', 'CRITICAL', 'Customer affected'),
('production_id', 'CRITICAL', 'Source MO'),
('description', 'CRITICAL', 'What went wrong'),
('severity', 'CRITICAL', 'Critical / major / minor'),
('containment_action', 'CRITICAL', 'Immediate action — Nadcap req'),
('root_cause', 'CRITICAL', 'Why — required to close'),
('corrective_action', 'CRITICAL', 'Fix — required to close'),
('disposition', 'CRITICAL', 'Use-as-is / scrap / rework — decision'),
('raised_by_id', 'CRITICAL', 'Who raised it'),
('raised_date', 'CRITICAL', 'When'),
])
capa = env.get('fusion.plating.capa')
if capa is not None:
rec = capa.search([], order='id desc', limit=1)
show_field_audit('fusion.plating.capa', rec, [
('name', 'CRITICAL', 'CAPA#'),
('owner_id', 'CRITICAL', 'Owner / champion'),
('due_date', 'CRITICAL', 'Deadline'),
('problem_description', 'CRITICAL', 'What\'s the recurring issue'),
('root_cause', 'CRITICAL', 'Why-why analysis — required'),
('corrective_action', 'CRITICAL', 'Fix the existing'),
('preventive_action', 'CRITICAL', 'Prevent recurrence'),
('verification_evidence', 'CRITICAL', 'Proof the fix worked'),
('effectiveness_date', 'IMPORTANT','When effectiveness confirmed'),
])
# =====================================================================
section('12. Compliance: discharge sample + waste manifest + spill')
# =====================================================================
DS = env.get('fusion.plating.discharge.sample')
if DS is not None:
rec = DS.search([], order='id desc', limit=1)
show_field_audit('fusion.plating.discharge.sample', rec, [
('sample_date', 'CRITICAL', 'When the sample was taken (regulatory)'),
('sampled_by_id', 'CRITICAL', 'Who'),
('outfall_id', 'CRITICAL', 'Which discharge point (jurisdictional req)'),
('parameter_id', 'CRITICAL', 'What pollutant'),
('value_measured', 'CRITICAL', 'The reading itself'),
('limit_value', 'CRITICAL', 'The regulatory limit'),
('exceeds_limit', 'CRITICAL', 'Pass/fail — drives mandatory reporting'),
('lab_cert_attachment_id','CRITICAL','Lab cert — required for regulator'),
])
WM = env.get('fusion.plating.waste.manifest')
if WM is not None:
rec = WM.search([], order='id desc', limit=1)
show_field_audit('fusion.plating.waste.manifest', rec, [
('manifest_number', 'CRITICAL', 'Government tracking #'),
('generator_id', 'CRITICAL', 'Who generated the waste (us)'),
('hauler_id', 'CRITICAL', 'Who picked it up (carrier)'),
('disposal_facility_id','CRITICAL','Where it went (landfill / treatment)'),
('waste_code', 'CRITICAL', 'EPA / TDG hazardous code'),
('quantity', 'CRITICAL', 'How much'),
('uom', 'CRITICAL', 'Unit'),
('shipped_date', 'CRITICAL', 'When shipped'),
('received_date', 'CRITICAL', 'When received at disposal — closes the loop'),
])
# =====================================================================
section('SUMMARY — gap counts by severity')
# =====================================================================
print(' See per-model details above. Critical gaps are real')
print(' compliance / workflow blockers; Important are operational')
print(' risks; Nice-to-have are quality-of-life.')
print()
print(' Recommended next-batch fixes (in priority order):')
print(' 1. invoice.ref auto-fill from sale_order.client_order_ref')
print(' (so customer PO# always lands on the invoice)')
print(' 2. fp.receiving.received_by_id default + required on accept')
print(' 3. mrp.production.x_fc_facility_id required (block confirm)')
print(' 4. fp.certificate.spec_reference required to issue')
print(' 5. fp.delivery.pod_id required to mark "delivered"')
print(' 6. fp.thickness.reading.position_label + calibration_std_ref required')
print(' 7. ncr/capa state-transition gates (can\'t close without root_cause)')
print(' 8. discharge.sample.lab_cert_attachment_id required to mark complete')