# -*- 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')