Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-18-cert-creation-and-data-gates-design.md
gsinghpal 091f98e1f9 changes
2026-05-18 22:33:23 -04:00

11 KiB

Certificate Creation Timing + Data Completeness Gates

Date: 2026-05-18 Status: Approved for implementation Author: Brainstorming session (gsinghpal) Triggering incident: WO-30040 marked done with no CoC produced — chatter showed Cert auto-create (coc) failed: name 'coating' is not defined (regression in fusion_plating_jobs/models/fp_job.py:1706 where coating was referenced but never bound).

Goal

Two things, decided as one unit of work:

  1. Fix the broken cert-creation path so jobs marked done always produce the expected draft certs.
  2. Harden the data-completeness gates so a CoC cannot be issued with missing critical information.

Out of scope

  • Redesigning the cert lifecycle timing (kept at button_mark_done()).
  • Wizard-based "Issue CoC" flow (Approach C, rejected).
  • SO-confirm cert-stub flow (Approach B, rejected).
  • Email delivery refactor — issuance still triggers existing fp.notification.template dispatch.

Decisions reached

# Decision Rationale
D1 Cert creation stays at fp.job.button_mark_done() All upstream data should be settled by then; existing architecture is sound — only the bug masks that.
D2 Receiving must close before job-done qty_received blank or unreconciled blocks button_mark_done. Guarantees the cert always points to a closed receiving.
D3 Strict qty accounting qty_received ≡ qty_done + qty_scrapped + qty_visual_inspection_rejects. NC qty on cert = qty_scrapped + qty_visual_inspection_rejects.
D4 Per-company default signer New res.company.x_fc_default_coc_signer_id. Customer-spec signer_user_id wins if set.
D5 Per-partner default CoC contact New res.partner.x_fc_default_coc_contact_id. Sales sets it once per customer.
D6 Mandatory fields at action_issue() spec_reference (existing), process_description, certified_by_id, contact_partner_id with valid email, qty reconciliation.
D7 Backfill action for closed jobs missing certs One-shot server action — walks state='done' jobs whose _resolve_required_cert_types() is non-empty and have no matching cert; calls _fp_create_certificates().

Architecture

┌─ JOB EXECUTION ─────────────────────────────────────────────────┐
│  Steps run → Bake → QC → Receiving closed                       │
│                              │                                   │
│                              ▼                                   │
│      button_mark_done()  [HARDENED GATE]                         │
│      existing checks PLUS:                                       │
│        qty_received present AND                                  │
│        qty_received ≡ qty_done + qty_scrapped + qty_rejects      │
│                              │                                   │
│                              ▼                                   │
│      _fp_create_certificates() (bug fixed + richer prefill)      │
│      Resolved sources:                                           │
│        process_description ← job.recipe_id.name                  │
│        certified_by_id     ← customer_spec.signer_user_id        │
│                             OR company.x_fc_default_coc_signer_id│
│        contact_partner_id  ← partner.x_fc_default_coc_contact_id │
│        nc_quantity         ← qty_scrapped + qty_visual_rejects   │
│                              │                                   │
│                              ▼                                   │
│      Draft cert(s) — milestone advances to "Issue Certs"         │
└─────────────────────────────────────────────────────────────────┘

┌─ ISSUANCE ──────────────────────────────────────────────────────┐
│  Manager opens cert → action_issue()  [HARDENED GATE]            │
│  existing checks PLUS:                                           │
│    process_description present                                   │
│    certified_by_id present                                       │
│    contact_partner_id present, with email                        │
│    qty reconciliation (belt-and-suspenders vs Gate 1)            │
│                              │                                   │
│                              ▼                                   │
│  state → issued, PDF generated, attached                         │
└─────────────────────────────────────────────────────────────────┘

Schema changes (additive)

Model New field Type Notes
res.company x_fc_default_coc_signer_id M2O res.users Default signing authority. Set once per facility.
res.partner x_fc_default_coc_contact_id M2O res.partner (children of self) Sales sets per customer.

Both are additive — no data migration needed.

Module changes

Module Version bump Files
fusion_plating 19.0.20.1.0 → 19.0.20.2.0 models/res_company.py, views/res_company_views.xml (or settings view)
fusion_plating_certificates 19.0.6.1.0 → 19.0.6.2.0 models/res_partner.py, models/fp_certificate.py, views/res_partner_views.xml
fusion_plating_jobs 19.0.10.8.0 → 19.0.10.9.0 models/fp_job.py (mark_done gate + cert prefill bug fix + backfill action)

Gate logic — button_mark_done()

Inside the existing if not skip_qty_gate and job.qty: block, add:

if not job.qty_received:
    raise UserError(_(
        "Job %s cannot be marked Done — Quantity Received is blank. "
        "Close the receiving record for SO %s before completing this job."
    ) % (job.name, job.sale_order_id.name if job.sale_order_id else '?'))

accounted_out = (job.qty_done or 0) + (job.qty_scrapped or 0) \
              + (job.qty_visual_inspection_rejects or 0)
if abs(job.qty_received - accounted_out) > 0.0001:
    raise UserError(_(
        "Job %s qty mismatch — received %g, but qty_done (%g) + "
        "qty_scrapped (%g) + visual rejects (%g) = %g. "
        "Reconcile before closing."
    ) % (job.name, job.qty_received, job.qty_done or 0,
         job.qty_scrapped or 0, job.qty_visual_inspection_rejects or 0,
         accounted_out))

Manager bypass: existing fp_skip_qty_reconcile=True context covers both.

Cert prefill table (_fp_create_certificates)

Cert field Source
partner_id job.partner_id (existing)
sale_order_id job.sale_order_id (existing)
x_fc_job_id job.id (existing)
certificate_type _resolve_required_cert_types() (existing)
part_number job.part_catalog_id.part_number (existing)
entech_wo_number job.name (existing)
po_number job.sale_order_id.x_fc_po_number (existing)
customer_job_no job.sale_order_id.x_fc_customer_job_number (existing)
spec_reference from customer_spec.code [+ " Rev " + revision] (existing)
customer_spec_id job.customer_spec_id (existing)
quantity_shipped qty_done - qty_scrapped (existing)
nc_quantity qty_scrapped + qty_visual_inspection_rejects (NEW)
process_description job.recipe_id.name (NEW; was broken — coating was undefined)
certified_by_id customer_spec.signer_user_id OR company.x_fc_default_coc_signer_id (NEW)
contact_partner_id partner.x_fc_default_coc_contact_id (NEW)

Gate logic — action_issue() (added in sequence before state = 'issued')

  1. process_description present — raise with hint to set coating-config / fill manually.
  2. certified_by_id present — raise with hint to set company default.
  3. contact_partner_id present AND email non-empty — raise with specific hint.
  4. qty reconciliation — defensive; reads x_fc_job_id if linked.

Order: cheapest checks first; first failure wins.

Edge cases

Case Behavior
Job has no recipe_id process_description = False → action_issue blocks → manager fills manually.
Company has no default signer certified_by_id blank → action_issue blocks.
Partner has no default contact contact_partner_id blank → action_issue blocks.
Contact has no email Action_issue blocks specifically on email.
Customer-spec overrides company signer customer_spec.signer_user_id wins (already used by signature unification).
Multi-line SO with different recipes First line with a recipe wins for process_description; manager can override.
Re-running _fp_create_certificates Idempotent by (job_id, certificate_type); NEW fields only set on initial create.
Older jobs with NULL qty_visual_inspection_rejects Coerce to 0; no migration needed.
Receiving never existed (internal rework) Mark_done blocks; manager bypass via fp_skip_qty_reconcile=True.

Backwards compatibility

  • WO-30040 itself (already done, no cert) is not auto-fixed by this change.
  • New server action "Generate missing certs for closed jobs" walks fp.job records where state='done' AND _resolve_required_cert_types() is non-empty AND no matching cert exists. Surfaced in the Jobs menu so the user can run once after deploy.

Test plan

Unit tests (in fusion_plating_jobs/tests/test_fp_job_milestone_cascade.py and new fusion_plating_certificates/tests/test_action_issue_gates.py):

  • test_mark_done_blocks_on_blank_qty_received
  • test_mark_done_blocks_on_qty_received_mismatch
  • test_mark_done_passes_with_clean_qty_reconcile
  • test_mark_done_bypass_skips_qty_received_check
  • test_create_cert_resolves_recipe_name (replaces "coating" wording)
  • test_create_cert_handles_job_with_no_recipe
  • test_create_cert_prefills_signer_from_company
  • test_create_cert_prefills_signer_from_customer_spec
  • test_create_cert_prefills_contact_from_partner
  • test_create_cert_computes_nc_quantity
  • test_create_cert_handles_null_visual_rejects
  • test_action_issue_blocks_on_missing_process_description
  • test_action_issue_blocks_on_missing_certified_by
  • test_action_issue_blocks_on_missing_contact
  • test_action_issue_blocks_on_contact_without_email
  • test_action_issue_blocks_on_qty_mismatch
  • test_action_issue_passes_when_all_data_present
  • test_create_cert_idempotency

Manual verification on entech (post-deploy):

  1. Run "Generate missing certs for closed jobs" → confirm WO-30040 gets 2 draft certs.
  2. Try action_issue → expect blockers for unset defaults.
  3. Configure defaults; retry → cert issues, PDF renders, attaches.

Deployment

  • Push to K:/Github/Odoo-Modules/fusion_plating/ (git path).
  • Mirror to docker mount as needed.
  • Update on entech LXC 111 via the deploy commands in project_entech_session_handoff.md.
  • Module install order: fusion_platingfusion_plating_certificatesfusion_plating_jobs.