197 lines
11 KiB
Markdown
197 lines
11 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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_plating` → `fusion_plating_certificates` → `fusion_plating_jobs`.
|