chore(plating): de-dash shipped code + intake-neutral customer emails

Replace em-dashes and en-dashes with hyphens across 789 shipped source
files (py/xml/js/scss) so the delivered module reads as human-written;
em-dashes had become a recognizable AI-generated tell. Internal .md dev
notes are excluded. The WO-sticker mojibake strippers keep their dash
search targets (now written — / –). No logic changes: comments
and display strings only; validated with py_compile + lxml parse.

Rewrite the 7 customer notification emails to be intake-neutral
(ship-in / drop-off / pickup) and repair-aware, and fix the Shipped
email documents line (packing slip vs bill of lading; certificate only
when issued). Subjects use a hyphen separator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-05 00:16:19 -04:00
parent c9eb61ee0c
commit 8c76a16366
789 changed files with 4692 additions and 4692 deletions

View File

@@ -72,7 +72,7 @@ for wo in all_wos:
print(f'5. plating WOs thickness/dwell backfilled: {n_th}')
# 6. Clean up OLD inspection WOs that have bath/tank wrongly set
# (legacy bug earlier simulator pinned bath to "Post-plate Inspection"
# (legacy bug - earlier simulator pinned bath to "Post-plate Inspection"
# because the old classifier matched 'plat' keyword. Fixed now.)
n_cl = 0
for wo in all_wos:
@@ -82,7 +82,7 @@ for wo in all_wos:
n_cl += 1
print(f'6. legacy bath/tank cleared from inspection WOs: {n_cl}')
# Verify classifier fix re-classify all WOs and report
# Verify classifier fix - re-classify all WOs and report
kinds = Counter()
mis_pi = []
for wo in all_wos:

View File

@@ -28,4 +28,4 @@ try:
print(f'PAGE {p}: cert={cert_present} sigs={sig_labels} '
f'(all-3-sigs-together={all(sig_labels)})')
except FileNotFoundError:
print('pdftotext not installed skipping per-page text check')
print('pdftotext not installed - skipping per-page text check')

View File

@@ -13,7 +13,7 @@ if m:
print('=== fp-keep-together block ===')
print(m.group(1)[:3000])
else:
# Fallback just find the sig-table
# Fallback - just find the sig-table
m2 = re.search(r'(<table class="sig-table".*?</table>)', out, re.S)
if m2:
print('=== sig-table block ===')

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Fusion Plating Demo Seeder
# Fusion Plating - Demo Seeder
# ================================
# Story-driven demo data: six customers, every workflow stage populated,
# historical depth for dashboards/trends, exception cases for enforcement.
@@ -67,7 +67,7 @@ def _make_badge(lines, width=420, height=220, bg='#0066A1', fg='white',
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
LOG(" PIL not available skipping badge generation")
LOG(" PIL not available - skipping badge generation")
return None
import io
img = Image.new('RGB', (width, height), bg)
@@ -198,7 +198,7 @@ if not _kris_user:
_company.x_fc_owner_user_id = _kris_user.id
# Always refresh signature (cheap, looks clean)
_company.x_fc_coc_signature_override = _make_signature('Kris Pathinather')
LOG(f" Accreditation badges + signature generated owner: {_kris_user.name}")
LOG(f" Accreditation badges + signature generated - owner: {_kris_user.name}")
# ============================================================
@@ -277,7 +277,7 @@ westin = ensure_partner('FPD-WESTIN', {
})
delinquent = ensure_partner('FPD-DELINQUENT', {
'name': 'Delinquent Industries (DEMO On Hold)',
'name': 'Delinquent Industries (DEMO - On Hold)',
'email': 'ap@delinquent-demo.com',
'phone': '+1 (416) 555-0199',
'street': '1 Overdue Lane',
@@ -324,8 +324,8 @@ def ensure_process_type(code, name, family='plating'):
'process_family': family,
})
en_midphos = ensure_process_type('EN_MID', 'Electroless Nickel Mid Phos', 'plating')
en_lowphos = ensure_process_type('EN_LOW', 'Electroless Nickel Low Phos', 'plating')
en_midphos = ensure_process_type('EN_MID', 'Electroless Nickel - Mid Phos', 'plating')
en_lowphos = ensure_process_type('EN_LOW', 'Electroless Nickel - Low Phos', 'plating')
passivation = ensure_process_type('PASS_AMS2700', 'Passivation AMS 2700', 'passivation')
hard_chrome = ensure_process_type('HARD_CR', 'Hard Chrome', 'plating')
alk_clean = ensure_process_type('ALK_CLEAN', 'Alkaline Clean', 'pre_treatment')
@@ -392,8 +392,8 @@ PARTS = [
(magellan, 'MG-BRK-112', 'Bell Crank Lever', 'steel', 'moderate', 9.7, 1),
(cyclone, 'CY-STR-101', 'Structural Strut Fitting', 'stainless', 'moderate', 11.5, 1),
(cyclone, 'CY-VLV-44', 'Valve Body Assembly', 'stainless', 'complex', 14.8, 2),
(cyclone, 'CY-PIN-880', 'Clevis Pin Port Side', 'steel', 'simple', 2.4, 1),
(cyclone, 'CY-PIN-881', 'Clevis Pin Starboard', 'steel', 'simple', 2.4, 1),
(cyclone, 'CY-PIN-880', 'Clevis Pin - Port Side', 'steel', 'simple', 2.4, 1),
(cyclone, 'CY-PIN-881', 'Clevis Pin - Starboard', 'steel', 'simple', 2.4, 1),
(honeywell, 'HW-TBO-7001', 'Turbine Output Shaft', 'stainless', 'complex', 28.6, 1),
(honeywell, 'HW-GRB-22', 'Grommet Retainer', 'aluminium', 'simple', 1.8, 1),
(honeywell, 'HW-VNG-114', 'Vane Ring Segment', 'stainless', 'complex', 16.9, 1),
@@ -444,7 +444,7 @@ for partner, pn, name, sub, cx, sa, revs in PARTS:
'parent_part_id': part.id,
'is_latest_revision': (r == revs),
'geometry_source': 'pdf_drawing',
'revision_note': f'Revision {r} minor geometry update',
'revision_note': f'Revision {r} - minor geometry update',
})
created_parts[pn] = part
@@ -518,7 +518,7 @@ LOG("Phase 6: Baths + chemistry logs")
facility = env['fusion.plating.facility'].search([], limit=1)
if not facility:
facility = env['fusion.plating.facility'].create({
'name': 'Main Shop 36 Taber Rd',
'name': 'Main Shop - 36 Taber Rd',
'code': 'MAIN',
'company_id': env.company.id,
})
@@ -574,7 +574,7 @@ for param in (p_nickel, p_ph, p_temp):
if param not in en_midphos.parameter_ids:
en_midphos.parameter_ids = [Command.link(param.id)]
# Per-bath target ranges (override) use bath.target_line_ids
# Per-bath target ranges (override) - use bath.target_line_ids
for bath, params in [(bath_en, [p_nickel, p_ph, p_temp])]:
for p in params:
existing = env['fusion.plating.bath.target'].search([
@@ -586,13 +586,13 @@ for bath, params in [(bath_en, [p_nickel, p_ph, p_temp])]:
'target_min': p.target_min, 'target_max': p.target_max,
})
# Replenishment rule Ni drop → add NiP replenisher
# Replenishment rule - Ni drop → add NiP replenisher
existing_rule = env['fusion.plating.bath.replenishment.rule'].search([
('bath_id', '=', bath_en.id), ('parameter_id', '=', p_nickel.id),
], limit=1)
if not existing_rule:
env['fusion.plating.bath.replenishment.rule'].create({
'name': 'EN Bath Nickel Low Replenishment',
'name': 'EN Bath - Nickel Low Replenishment',
'bath_id': bath_en.id,
'parameter_id': p_nickel.id,
'trigger': 'below_min',
@@ -602,7 +602,7 @@ if not existing_rule:
'min_dose': 50.0,
})
# Historical bath logs 15 readings over 30 days, last 2 out-of-spec (to trigger suggestions)
# Historical bath logs - 15 readings over 30 days, last 2 out-of-spec (to trigger suggestions)
existing_log_count = env['fusion.plating.bath.log'].search_count([('bath_id', '=', bath_en.id)])
if existing_log_count < 12:
for days_ago in range(30, 0, -2): # 15 readings
@@ -720,7 +720,7 @@ def register_payment(invoice):
return False
# Historical job factory creates full SO→MO→Delivery→Invoice→Payment chain
# Historical job factory - creates full SO→MO→Delivery→Invoice→Payment chain
# then backdates everything to simulate months of history
def make_closed_job(partner, coating, part_cat, qty, unit_price, days_ago,
strategy='net_terms', deposit_pct=0.0):
@@ -735,7 +735,7 @@ def make_closed_job(partner, coating, part_cat, qty, unit_price, days_ago,
'x_fc_po_received': True,
'order_line': [Command.create({
'product_id': product.id,
'name': f'{coating.name} {part_cat.name if part_cat else "custom"} (x{qty})',
'name': f'{coating.name} - {part_cat.name if part_cat else "custom"} (x{qty})',
'product_uom_qty': qty, 'price_unit': unit_price,
})],
}
@@ -843,7 +843,7 @@ if closed_count < 10:
print(f" ! history job failed: {e}")
LOG(f" Created 8 historical closed jobs")
else:
LOG(f" Already has {closed_count} closed jobs skipping history phase")
LOG(f" Already has {closed_count} closed jobs - skipping history phase")
# ============================================================
@@ -863,7 +863,7 @@ def quick_so(partner, coating, part_cat, qty, price, strategy, deposit_pct=0.0,
'x_fc_po_received': True,
'order_line': [Command.create({
'product_id': product.id,
'name': f'{coating.name} {part_cat.name if part_cat else "part"} (x{qty})',
'name': f'{coating.name} - {part_cat.name if part_cat else "part"} (x{qty})',
'product_uom_qty': qty, 'price_unit': price,
})],
})
@@ -880,14 +880,14 @@ active_marker = env['sale.order'].search_count([
])
if active_marker == 0:
# Magellan progress billing SO confirmed, 40% invoice already posted
# Magellan progress billing - SO confirmed, 40% invoice already posted
so_mag = quick_so(magellan, coating_chrome, created_parts.get('MG-CYL-550'),
30, 120.00, 'progress', 40.0, 'FPDEMO-ACTIVE-MAGELLAN')
for inv in so_mag.invoice_ids:
try: inv.action_post()
except: pass
# Cyclone deposit SO confirmed, 50% deposit invoice posted & paid,
# Cyclone deposit - SO confirmed, 50% deposit invoice posted & paid,
# MO in progress at WO stage
so_cyc = quick_so(cyclone, coating_en_mid, created_parts.get('CY-VLV-44'),
40, 52.00, 'deposit', 50.0, 'FPDEMO-ACTIVE-CYCLONE')
@@ -902,7 +902,7 @@ if active_marker == 0:
try: mo_cyc.action_confirm()
except: pass
# Honeywell net_terms SO just confirmed, receiving pending
# Honeywell net_terms - SO just confirmed, receiving pending
so_hw = quick_so(honeywell, coating_en_low, created_parts.get('HW-TBO-7001'),
25, 72.00, 'net_terms', 0, 'FPDEMO-ACTIVE-HONEYWELL')
@@ -910,7 +910,7 @@ if active_marker == 0:
so_wn = quick_so(westin, coating_en_low, created_parts.get('WM-HSG-201'),
20, 48.00, 'cod_prepay', 0, 'FPDEMO-WESTIN-DIRECT')
# Amphenol MO done, ready to ship (for live delivery demo)
# Amphenol - MO done, ready to ship (for live delivery demo)
so_am = quick_so(amphenol, coating_en_mid, created_parts.get('VS-ESMC6H00801P01'),
500, 38.00, 'net_terms', 0, 'FPDEMO-AMPHENOL-READY')
mo_am = env['mrp.production'].create({
@@ -925,19 +925,19 @@ if active_marker == 0:
LOG(" 5 active orders across workflow stages")
else:
LOG(" Active demo orders already present skipping")
LOG(" Active demo orders already present - skipping")
# ============================================================
# Phase 11: Exception case Delinquent Industries
# Phase 11: Exception case - Delinquent Industries
# ============================================================
LOG("Phase 11: Exception account hold blocked SO")
LOG("Phase 11: Exception - account hold blocked SO")
block_marker = env['sale.order'].search_count([
('partner_id', '=', delinquent.id), ('x_fc_po_number', '=', 'FPDEMO-HOLD-TEST'),
])
if block_marker == 0:
# Create but leave in DRAFT it will raise UserError when they click confirm
# Create but leave in DRAFT - it will raise UserError when they click confirm
env['sale.order'].create({
'partner_id': delinquent.id,
'x_fc_invoice_strategy': 'net_terms',
@@ -948,7 +948,7 @@ if block_marker == 0:
'product_uom_qty': 10, 'price_unit': 100.0,
})],
})
LOG(" Draft SO for Delinquent Industries click Confirm to demo the hold")
LOG(" Draft SO for Delinquent Industries - click Confirm to demo the hold")
# ============================================================
@@ -958,7 +958,7 @@ LOG("Phase 12: Bake window variety")
bw_count = env['fusion.plating.bake.window'].search_count([])
if bw_count < 3:
# Awaiting recent plate exit, still within window
# Awaiting - recent plate exit, still within window
env['fusion.plating.bake.window'].create({
'bath_id': bath_en.id,
'part_ref': 'MG-WG-8801', 'lot_ref': 'LOT-BW-001',
@@ -990,7 +990,7 @@ if bw_count < 3:
backdate(bw_done, 14)
LOG(" 3 bake windows: 1 awaiting, 1 missed, 1 baked")
else:
LOG(f" Already has {bw_count} bake windows skipping")
LOG(f" Already has {bw_count} bake windows - skipping")
# ============================================================
@@ -1025,9 +1025,9 @@ if stn_count < 5:
})
LOG(f" 5 shop-floor stations created")
else:
LOG(f" Already has {stn_count} stations skipping")
LOG(f" Already has {stn_count} stations - skipping")
# More bake windows add a couple active ones for realism
# More bake windows - add a couple active ones for realism
if env['fusion.plating.bake.window'].search_count([]) < 6:
env['fusion.plating.bake.window'].create({
'bath_id': bath_en.id,
@@ -1056,7 +1056,7 @@ if env['fusion.plating.bake.window'].search_count([]) < 6:
# First-piece-gate seeding retired with the model (19.0.33.2.0).
# Quality holds on active MOs gives the Shop Floor quality-holds panel content
# Quality holds on active MOs - gives the Shop Floor quality-holds panel content
Hold = env['fusion.plating.quality.hold']
if Hold.search_count([]) < 2:
active_mos = env['mrp.production'].search(
@@ -1070,7 +1070,7 @@ if Hold.search_count([]) < 2:
'qty_on_hold': random.randint(3, 8),
'qty_original': int(mo.product_qty or 10),
'hold_reason': hold_reasons[i % len(hold_reasons)],
'description': f'Demo hold flagged during in-process inspection on {mo.name}.',
'description': f'Demo hold - flagged during in-process inspection on {mo.name}.',
'production_id': mo.id,
'workorder_id': wo.id if wo else False,
'portal_job_id': mo.x_fc_portal_job_id.id if mo.x_fc_portal_job_id else False,
@@ -1080,7 +1080,7 @@ if Hold.search_count([]) < 2:
})
LOG(f" {Hold.search_count([])} quality holds seeded")
else:
LOG(f" Already has {Hold.search_count([])} quality holds skipping")
LOG(f" Already has {Hold.search_count([])} quality holds - skipping")
# ============================================================

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
"""Demo stage-filler fills the gaps left after fp_demo_seed.py.
"""Demo stage-filler - fills the gaps left after fp_demo_seed.py.
The base seeder gives us customers, SOs, MOs, WOs, deliveries, invoices
and payments. After the team-skills + timer-audit + presence-aware
@@ -11,7 +11,7 @@ Manager Desk work landed (commit 0d12902) the demo needs:
on done WOs so the new "Timer Audit" group has values
2. WO role tags filled in on any leftover WOs (auto-promotion needs
a role on the WO to credit the operator)
3. A diverse team Marie / James / Priya / Diego / Aisha / Carlos
3. A diverse team - Marie / James / Priya / Diego / Aisha / Carlos
covering different role combinations including lead hands
4. Proficiency records seeded from completed WOs so the "Task
Proficiency" list on the employee form has rich data and a few
@@ -27,7 +27,7 @@ Run via odoo-shell:
su - odoo -s /bin/bash -c "odoo shell -c /etc/odoo/odoo.conf -d admin \\
--no-http --stop-after-init < fp_demo_stage_filler.py"
Idempotent re-runs are safe.
Idempotent - re-runs are safe.
"""
import logging
from datetime import timedelta
@@ -90,7 +90,7 @@ def _backfill_timer_audit(env):
('x_fc_started_at', '=', False),
])
for wo in wos:
# Prefer real time_ids data that's what the live override would
# Prefer real time_ids data - that's what the live override would
# have captured if the WO had been finished after the upgrade.
if wo.time_ids:
first = wo.time_ids.sorted('date_start')[:1]
@@ -112,7 +112,7 @@ def _backfill_timer_audit(env):
)
start_uid = end_uid = uid
# Always falls back to the assigned worker if we couldn't pull a
# user from time_ids that's the operator who SHOULD get credit.
# user from time_ids - that's the operator who SHOULD get credit.
if not start_uid:
start_uid = (
wo.x_fc_assigned_user_id.id
@@ -156,7 +156,7 @@ TEAM = [
'lead_codes': ['oven', 'inspection'],
},
{
# Diego is the "still in training" employee racks and plates
# Diego is the "still in training" employee - racks and plates
# but isn't qualified for masking yet. The proficiency tracker
# will promote him once he's finished N masking WOs.
'name': 'Diego Ramirez',
@@ -173,7 +173,7 @@ TEAM = [
'lead_codes': [],
},
{
# Carlos is the senior lead hand for everything, can cover
# Carlos is the senior - lead hand for everything, can cover
# any shift. Manager Desk surfaces him on every dropdown.
'name': 'Carlos Silva',
'work_email': 'carlos.silva@entech.demo',
@@ -250,7 +250,7 @@ def _redistribute_completed_wos(env):
two original users (Administrator + Andrew) and the new team
members (Marie/James/Priya/etc.) look like blank slates with no
completion history. Spread the credit so the demo shows realistic
proficiency variance Marie has 8 masking jobs done, James has
proficiency variance - Marie has 8 masking jobs done, James has
12 plating_op, Carlos has touched everything, Diego is mid-training.
"""
Emp = env['hr.employee']
@@ -341,7 +341,7 @@ def _seed_proficiency(env):
promoted += 1
# Give Diego a partial-mastery row on masking so we can SEE the
# progress label "2 / 3" and the not-yet-promoted state that's
# progress label "2 / 3" and the not-yet-promoted state - that's
# the most interesting demo case.
diego = env['hr.employee'].search([('name', '=', 'Diego Ramirez')], limit=1)
masking = env['fp.work.role'].search([('code', '=', 'masking')], limit=1)
@@ -373,7 +373,7 @@ def _clock_in_team(env):
targets = ['Marie Dubois', 'James O\'Connor', 'Carlos Silva']
started = []
# Always 4 hours ago guaranteed before "now", regardless of when
# Always 4 hours ago - guaranteed before "now", regardless of when
# the close-stale-records step ran. The earlier .replace() approach
# could land in the future if the script ran after noon UTC, which
# the validator rejected.
@@ -391,7 +391,7 @@ def _clock_in_team(env):
# ---------------------------------------------------------------------------
# 6. Top up extra demo records quality holds + paused WO + RFQs
# 6. Top up extra demo records - quality holds + paused WO + RFQs
# ---------------------------------------------------------------------------
def _add_quality_holds(env):
Hold = env['fusion.plating.quality.hold']
@@ -405,7 +405,7 @@ def _add_quality_holds(env):
# customer_complaint / process_deviation / other.
reasons = ['damaged', 'out_of_spec']
descriptions = [
'Light scratch on the masked face flagging for re-inspection.',
'Light scratch on the masked face - flagging for re-inspection.',
'Thickness reading 0.4 mils, target 0.5 ± 0.05. Out of spec.',
]
created = 0
@@ -427,7 +427,7 @@ def _add_quality_holds(env):
def _add_paused_wo(env):
"""Show one WO mid-flight with pause/resume history.
The audit fields show started_by but no finished_at exactly the
The audit fields show started_by but no finished_at - exactly the
"in progress, paused for lunch" state a manager would see live.
"""
progress = env['mrp.workorder'].search([('state', '=', 'progress')], limit=1)
@@ -477,17 +477,17 @@ def _seed_fresh_mos(env):
active_mo_count = Production.search_count([('state','not in',('done','cancel'))])
if active_mo_count >= 5:
print(f"[6g] Already have {active_mo_count} active MOs skipping")
print(f"[6g] Already have {active_mo_count} active MOs - skipping")
return
product = Product.search([('default_code', '=', 'FP-WIDGET')], limit=1)
# Recipe code stores underscores (ENP_ALUM_BASIC) while the
# display name uses dashes historical drift between the two
# display name uses dashes - historical drift between the two
# data files. Just take the first available recipe so the seed
# works regardless of which spelling is canonical today.
recipe = Recipe.search([('node_type', '=', 'recipe')], limit=1)
if not product or not recipe:
print("[6g] Missing product/recipe skipping (need FP-WIDGET + a recipe)")
print("[6g] Missing product/recipe - skipping (need FP-WIDGET + a recipe)")
return
# Customer + qty + priority + post-create tweaks for each MO.
@@ -512,7 +512,7 @@ def _seed_fresh_mos(env):
partner = env['res.partner'].search(
[('name', '=', customer_names[cust_idx])], limit=1,
)
# Sale order origin lookup fall back to a synthetic ref if no
# Sale order origin lookup - fall back to a synthetic ref if no
# SO exists for this customer (just so the demo card has text).
so = SO.search([('partner_id', '=', partner.id)], limit=1) if partner else None
origin = so.name if so else f'DEMO-{customer_names[cust_idx][:6].upper()}'
@@ -549,7 +549,7 @@ def _seed_fresh_mos(env):
try:
first.button_start()
except Exception:
# Cert gate or similar not fatal for the demo seed.
# Cert gate or similar - not fatal for the demo seed.
pass
created += 1
@@ -565,7 +565,7 @@ def _populate_active_wos(env):
deliberately left unassigned so the "Needs a Worker" column has
cards to pick from). Earlier keyword-based fuzzy matching missed
a few WOs whose names didn't contain the expected substring, so
this rewrite uses a positional plan instead less clever, more
this rewrite uses a positional plan instead - less clever, more
predictable.
Manager Desk's WO domain was widened to include 'blocked' state in
@@ -575,7 +575,7 @@ def _populate_active_wos(env):
Emp = env['hr.employee']
mo = env['mrp.production'].search([('state', '=', 'progress')], limit=1)
if not mo:
print("[6e] No in-progress MO available skipping")
print("[6e] No in-progress MO available - skipping")
return
# Sequence-aligned plan: the Nth downstream WO (skipping done +
@@ -585,8 +585,8 @@ def _populate_active_wos(env):
'Carlos Silva', # senior owns the critical plating step
"James O'Connor", # lead hand for plating_op also covers demask
'Priya Sharma', # lead hand for oven
None, # de-rack left empty for "Needs a Worker"
None, # post-bake left empty for "Needs a Worker"
None, # de-rack - left empty for "Needs a Worker"
None, # post-bake - left empty for "Needs a Worker"
'Aisha Khan', # final inspection
]
@@ -648,7 +648,7 @@ def _mark_quote_sent(env):
"""Bump one draft SO into the 'sent' state so the funnel has data
in every workflow column.
Without this, the dashboard "Sent" stage is always empty the
Without this, the dashboard "Sent" stage is always empty - the
seeder jumps straight from draft to confirmed.
"""
sent = env['sale.order'].search([('state', '=', 'sent')], limit=1)
@@ -661,7 +661,7 @@ def _mark_quote_sent(env):
if not draft:
print("[6d] No draft SO with lines available")
return
# action_quotation_send opens a wizard skip the wizard and just
# action_quotation_send opens a wizard - skip the wizard and just
# flip the state directly with a chatter line, which is what the
# wizard would do anyway after the email is sent.
draft.write({'state': 'sent'})
@@ -672,7 +672,7 @@ def _mark_quote_sent(env):
def _add_quote_requests(env):
QR = env.get('fusion.plating.quote.request')
if QR is None:
print("[6c] Portal module not installed skipping RFQ seed")
print("[6c] Portal module not installed - skipping RFQ seed")
return
if QR.search_count([]) >= 3:
print("[6c] Quote requests already populated, skipping")
@@ -689,7 +689,7 @@ def _add_quote_requests(env):
][:3]
notes = [
'<p>Need a quote for 200 brass fittings Type II passivation, urgent.</p>',
'<p>Need a quote for 200 brass fittings - Type II passivation, urgent.</p>',
'<p>Recurring customer, standard EN on 50 housings, 0.5 mil target.</p>',
'<p>Aerospace job, AS9100 + Nadcap CoC required, 12 turbine vanes.</p>',
]
@@ -722,7 +722,7 @@ def _safe(label, fn):
print("\n=========================================================")
print("FUSION PLATING DEMO STAGE FILLER")
print("FUSION PLATING - DEMO STAGE FILLER")
print("=========================================================")
_safe('1. fill WO roles', _fill_wo_roles)
_safe('3. seed team', _seed_team)
@@ -738,5 +738,5 @@ _safe('6e. populate active WOs', _populate_active_wos)
_safe('6f. SO awaiting-manager', _mark_so_awaiting_manager)
_safe('6g. seed fresh MOs', _seed_fresh_mos)
print("=========================================================")
print("Done. Re-run anytime script is idempotent.")
print("Done. Re-run anytime - script is idempotent.")
print("=========================================================\n")

View File

@@ -17,7 +17,7 @@ def banner(txt):
def check(label, ok, detail=''):
flag = 'PASS' if ok else 'FAIL'
print(f" [{flag}] {label}{(' ' + detail) if detail else ''}")
print(f" [{flag}] {label}{(' - ' + detail) if detail else ''}")
return ok
@@ -30,7 +30,7 @@ def expect(label, ok, detail=''):
# =====================================================================
banner('PHASE 1 Quote configurator + PO → client_order_ref')
banner('PHASE 1 - Quote configurator + PO → client_order_ref')
# =====================================================================
# Fresh customer per run so we can verify automations on a clean slate.
@@ -74,7 +74,7 @@ expect('client_order_ref carries PO', so.client_order_ref == po_number,
f'got "{so.client_order_ref}"')
# =====================================================================
banner('PHASE 2 SO confirm → MO + portal job + WOs')
banner('PHASE 2 - SO confirm → MO + portal job + WOs')
# =====================================================================
so.action_confirm()
@@ -106,7 +106,7 @@ expect('work orders generated', len(mo.workorder_ids) > 0,
f'{len(mo.workorder_ids)} WOs')
# =====================================================================
banner('PHASE 3 Receiving with auto-prefilled qty')
banner('PHASE 3 - Receiving with auto-prefilled qty')
# =====================================================================
Receiving = env['fp.receiving']
@@ -131,11 +131,11 @@ if recv.state == 'draft':
print(f" [info] receiving confirm: {e}")
# =====================================================================
banner('PHASE 4 Execute work orders + audit timing')
banner('PHASE 4 - Execute work orders + audit timing')
# =====================================================================
# Use the first operator we can find. Auto-promotion test only needs
# one completion for verification N is configurable per role.
# one completion for verification - N is configurable per role.
operator = env['hr.employee'].search([('active', '=', True)], limit=1)
expect('operator available', bool(operator), operator.name or '')
@@ -175,7 +175,7 @@ except Exception as e:
expect('MO state = done', mo.state == 'done', mo.state)
# =====================================================================
banner('PHASE 5 Certificates (CoC issued + rich PDF, no thickness dup)')
banner('PHASE 5 - Certificates (CoC issued + rich PDF, no thickness dup)')
# =====================================================================
certs = env['fp.certificate'].search([('production_id', '=', mo.id)])
@@ -200,7 +200,7 @@ if coc:
coc.attachment_id.name)
# =====================================================================
banner('PHASE 6 Delivery auto-prefilled + chain of custody')
banner('PHASE 6 - Delivery auto-prefilled + chain of custody')
# =====================================================================
dlv = env['fusion.plating.delivery'].search(
@@ -235,7 +235,7 @@ expect('portal job state advanced to shipped/complete',
job.state in ('shipped', 'complete'), job.state)
# =====================================================================
banner('PHASE 7 Invoice creation + post + body has lines')
banner('PHASE 7 - Invoice creation + post + body has lines')
# =====================================================================
# Let Odoo's standard invoice flow handle it.
@@ -277,7 +277,7 @@ if inv:
job.state == 'complete', job.state)
# =====================================================================
banner('PHASE 8 Notification log + email attachments')
banner('PHASE 8 - Notification log + email attachments')
# =====================================================================
logs = env['fp.notification.log'].search(

View File

@@ -12,7 +12,7 @@ from odoo import fields # noqa
def step(num, who, action):
bar = '' * 70
print(f'\n{bar}\n STEP {num} {who}\n{action}\n{bar}')
print(f'\n{bar}\n STEP {num} - {who}\n{action}\n{bar}')
def show(label, value):
@@ -24,7 +24,7 @@ def hr():
# ─────────────────────────────────────────────────────────────────────
# Setup: pick a generic existing customer? No fresh customer so we
# Setup: pick a generic existing customer? No - fresh customer so we
# can prove the full chain of automations on a clean slate.
# ─────────────────────────────────────────────────────────────────────
stamp = datetime.now().strftime('%y%m%d-%H%M%S')
@@ -83,7 +83,7 @@ show('estimator override', f'${quote.estimator_override_price:,.2f}')
show('calculated price', f'${quote.calculated_price:,.2f} ({quote.currency_id.name})')
show('PO# entered', po_number)
# Sales clicks "Create Quotation" this is the SO.
# Sales clicks "Create Quotation" - this is the SO.
result = quote.action_create_quotation()
so = env['sale.order'].browse(result.get('res_id'))
show('SO drafted', f'{so.name} (state={so.state})')
@@ -122,7 +122,7 @@ show('portal job auto-created',
# =====================================================================
step(4, 'RECEIVING (warehouse)',
'Customer parts arrive count + log + auto-prefill')
'Customer parts arrive - count + log + auto-prefill')
# =====================================================================
recv = env['fp.receiving'].create({
@@ -199,7 +199,7 @@ step(7, 'MANAGER',
try:
mo.button_mark_done()
except Exception as e:
print(f' [info] mark_done: {e} falling back to _action_done')
print(f' [info] mark_done: {e} - falling back to _action_done')
try:
mo.qty_producing = mo.product_qty
mo._action_done()
@@ -213,7 +213,7 @@ certs = env['fp.certificate'].search([('production_id', '=', mo.id)])
coc = certs.filtered(lambda c: c.certificate_type == 'coc')[:1]
thickness = certs.filtered(lambda c: c.certificate_type == 'thickness_report')
show('CoC cert', f'{coc.name} (state={coc.state})' if coc else '(MISSING)')
show('thickness cert', f'count={len(thickness)} (expected 0 CoC includes it)')
show('thickness cert', f'count={len(thickness)} (expected 0 - CoC includes it)')
if coc and coc.attachment_id:
kb = len(base64.b64decode(coc.attachment_id.datas)) / 1024
show('CoC PDF', f'{coc.attachment_id.name}{kb:.1f} KB')
@@ -253,7 +253,7 @@ show('portal job state', job.state)
# =====================================================================
step(9, 'ACCOUNTING',
'Creates + posts invoice (does NOT register payment '
'Creates + posts invoice (does NOT register payment - '
'real customer pays through bank/Stripe)')
# =====================================================================
@@ -276,7 +276,7 @@ if inv:
show('invoice posted', f'state={inv.state}, payment_state={inv.payment_state}')
show('total', f'${inv.amount_total:,.2f}')
show('payment_state explanation',
'"not_paid" is correct accountant has not yet registered any payment')
'"not_paid" is correct - accountant has not yet registered any payment')
inv_report = env.ref(
'fusion_plating_reports.action_report_fp_invoice_portrait',

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Comprehensive E2E simulator workforce edition.
"""Comprehensive E2E simulator - workforce edition.
Role-plays each employee touching a job from quote → invoice. For
each work order:
@@ -17,7 +17,7 @@ Then audits:
• Notification log with attachment names
• Portal job final state + SO workflow_stage
Findings printed at the end as PASS/FAIL/WARN each FAIL/WARN is a
Findings printed at the end as PASS/FAIL/WARN - each FAIL/WARN is a
gap that needs fixing before this can ship to a real shop floor.
"""
from datetime import datetime
@@ -53,7 +53,7 @@ def finding(level, area, msg):
stamp = datetime.now().strftime('%y%m%d-%H%M%S')
# =====================================================================
banner(f'PHASE 0 Set up cast of employees ({stamp})')
banner(f'PHASE 0 - Set up cast of employees ({stamp})')
# =====================================================================
# Reuse existing users when present so we don't bloat the DB on reruns.
@@ -99,10 +99,10 @@ for key, (name, desc) in PERSONAS.items():
'name': name,
'user_id': u.id,
})
show(f'{key:<8}', f'{u.name} ({desc}) uid={u.id}, emp={emp.id}')
show(f'{key:<8}', f'{u.name} ({desc}) - uid={u.id}, emp={emp.id}')
# =====================================================================
banner('PHASE 1 Sandra builds a quote (estimator)')
banner('PHASE 1 - Sandra builds a quote (estimator)')
# =====================================================================
customer = env['res.partner'].sudo().create({
@@ -160,10 +160,10 @@ finding('PASS' if so.client_order_ref == po_number else 'FAIL',
'quote→SO PO#', f'client_order_ref="{so.client_order_ref}"')
# =====================================================================
banner('PHASE 2 Customer accepts → SO confirm → auto-MO + portal job')
banner('PHASE 2 - Customer accepts → SO confirm → auto-MO + portal job')
# =====================================================================
step('CUSTOMER', 'Accepts quote Sandra confirms SO')
step('CUSTOMER', 'Accepts quote - Sandra confirms SO')
so.with_user(users['sandra']).sudo().action_confirm()
finding('PASS' if so.state == 'sale' else 'FAIL', 'SO confirm', f'state={so.state}')
@@ -178,10 +178,10 @@ job = mo.x_fc_portal_job_id if mo else False
finding('PASS' if job else 'FAIL', 'portal job', job.name if job else 'MISSING')
# =====================================================================
banner('PHASE 3 Carlos receives parts')
banner('PHASE 3 - Carlos receives parts')
# =====================================================================
step('CARLOS', 'Logs receiving 40 housings in 2 boxes from FedEx')
step('CARLOS', 'Logs receiving - 40 housings in 2 boxes from FedEx')
recv = env['fp.receiving'].with_user(users['carlos']).sudo().create({
'partner_id': customer.id,
'sale_order_id': so.id,
@@ -205,7 +205,7 @@ finding('PASS' if recv.state == 'accepted' else 'FAIL',
'receiving accept', f'state={recv.state}')
# =====================================================================
banner('PHASE 4 Hannah plans the job')
banner('PHASE 4 - Hannah plans the job')
# =====================================================================
step('HANNAH', 'Assigns recipe + generates work orders')
@@ -275,8 +275,8 @@ if Cert is not None and test_bath and test_bath.process_type_id:
'notes': 'Auto-issued for E2E workforce simulation',
})
show(' certifications', f'pre-issued for {pt.name} → 5 operators')
show(' test bath', f'{test_bath.name}' if test_bath else '(none wet-WO assignment will fail)')
show(' test tank', f'{test_tank.name}' if test_tank else '(none wet-WO assignment will fail)')
show(' test bath', f'{test_bath.name}' if test_bath else '(none - wet-WO assignment will fail)')
show(' test tank', f'{test_tank.name}' if test_tank else '(none - wet-WO assignment will fail)')
assignments = []
wet_assignments = []
@@ -302,18 +302,18 @@ for wo in mo.workorder_ids:
'x_fc_tank_id': test_tank.id,
})
wet_assignments.append(wo)
extras = f' [WET bath={test_bath.name}, tank={test_tank.name}]'
extras = f' [WET - bath={test_bath.name}, tank={test_tank.name}]'
elif kind == 'bake' and test_oven:
wo.sudo().x_fc_oven_id = test_oven.id
extras = f' [BAKE oven={test_oven.name}]'
extras = f' [BAKE - oven={test_oven.name}]'
elif kind == 'rack':
rack = env['fusion.plating.rack'].search([], limit=1)
if rack:
wo.sudo().x_fc_rack_id = rack.id
extras = f' [RACK fixture={rack.name}]'
extras = f' [RACK - fixture={rack.name}]'
elif kind == 'mask':
wo.sudo().x_fc_masking_material = 'tape'
extras = ' [MASK material=tape]'
extras = ' [MASK - material=tape]'
assignments.append((wo, op_user, operator_key))
show(f' WO {wo.id}', f'"{wo.name}"{op_user.name}{extras}')
@@ -328,10 +328,10 @@ finding('PASS' if (not wet_assignments) or (wet_with_bath == len(wet_assignments
f'{wet_with_bath}/{len(wet_assignments)} wet WOs have both bath + tank')
# ===== Negative tests: validation MUST block bad starts =====
banner('PHASE 4b Negative tests: validation gates fire correctly')
banner('PHASE 4b - Negative tests: validation gates fire correctly')
# Test 1: try to start a WO with operator stripped → expect UserError
step('SYSTEM', 'Test 1 un-assigning operator and trying to start')
step('SYSTEM', 'Test 1 - un-assigning operator and trying to start')
test_wo = mo.workorder_ids[0]
saved_op = test_wo.x_fc_assigned_user_id.id
test_wo.sudo().x_fc_assigned_user_id = False
@@ -343,12 +343,12 @@ except Exception as e:
show(' blocked with', str(e).splitlines()[0][:120])
finding('PASS' if gate_fired else 'FAIL',
'gate: missing operator',
'blocked' if gate_fired else 'NOT blocked validation broken')
'blocked' if gate_fired else 'NOT blocked - validation broken')
test_wo.sudo().x_fc_assigned_user_id = saved_op
# Test 2: try to start a WET WO without bath/tank → expect UserError
if wet_assignments:
step('SYSTEM', 'Test 2 wet WO with bath/tank stripped')
step('SYSTEM', 'Test 2 - wet WO with bath/tank stripped')
wet_wo = wet_assignments[0]
saved_bath = wet_wo.x_fc_bath_id.id
saved_tank = wet_wo.x_fc_tank_id.id
@@ -362,7 +362,7 @@ if wet_assignments:
show(' blocked with', msg.splitlines()[0][:120])
finding('PASS' if gate_fired else 'FAIL',
'gate: missing bath/tank on wet WO',
'blocked' if gate_fired else 'NOT blocked validation broken')
'blocked' if gate_fired else 'NOT blocked - validation broken')
wet_wo.sudo().write({
'x_fc_bath_id': saved_bath,
'x_fc_tank_id': saved_tank,
@@ -370,7 +370,7 @@ if wet_assignments:
# ===== 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')
banner('PHASE 4c - Negative tests for the new compliance gates')
def neg_test(label, fn, expect_keywords):
@@ -396,7 +396,7 @@ def neg_test(label, fn, expect_keywords):
# Test 3: MO confirm without facility → expect block
step('SYSTEM', 'Test 3 MO confirm with no facility → blocked')
step('SYSTEM', 'Test 3 - MO confirm with no facility → blocked')
def t_mo_facility():
@@ -410,7 +410,7 @@ def t_mo_facility():
'product_qty': 1,
'company_id': env.company.id,
})
m.action_confirm() # should raise no facility resolvable
m.action_confirm() # should raise - no facility resolvable
finally:
fac0.sudo().write({'active': True})
env.company.sudo().x_fc_default_facility_id = saved_default
@@ -420,7 +420,7 @@ 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')
step('SYSTEM', 'Test 4 - Cert action_issue() without spec_reference → blocked')
def t_cert_spec():
@@ -437,7 +437,7 @@ 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')
step('SYSTEM', 'Test 5 - Delivery mark_delivered() with no POD → blocked')
def t_dlv_pod():
@@ -453,7 +453,7 @@ 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')
step('SYSTEM', 'Test 6 - Invoice post() with no payment terms → blocked')
def t_inv_terms():
@@ -480,7 +480,7 @@ 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')
step('SYSTEM', 'Test 7 - Thickness reading without calibration_std_ref → blocked')
def t_thickness_cal():
@@ -496,7 +496,7 @@ neg_test('thickness reading without cal std', t_thickness_cal,
['calibration', 'required', 'not-null', 'null value'])
# Test 8: NCR close without root cause / containment / disposition
step('SYSTEM', 'Test 8 NCR close() with missing fields → blocked')
step('SYSTEM', 'Test 8 - NCR close() with missing fields → blocked')
def t_ncr_close():
@@ -515,7 +515,7 @@ neg_test('NCR close without RC/containment/disposition', t_ncr_close,
['Root Cause', 'Containment', 'Disposition'])
# Test 9: CAPA close without root cause analysis / action plan / verification
step('SYSTEM', 'Test 9 CAPA close() with missing fields → blocked')
step('SYSTEM', 'Test 9 - CAPA close() with missing fields → blocked')
def t_capa_close():
@@ -531,7 +531,7 @@ neg_test('CAPA close without analysis/plan/verification', t_capa_close,
['Root Cause Analysis', 'Action Plan', 'Verification'])
# Test 10: Discharge sample close without lab evidence
step('SYSTEM', 'Test 10 Discharge sample close() with no lab evidence → blocked')
step('SYSTEM', 'Test 10 - Discharge sample close() with no lab evidence → blocked')
def t_discharge_close():
@@ -546,7 +546,7 @@ neg_test('discharge sample close without lab evidence', t_discharge_close,
['Lab Report', 'Results Received', 'parameter', 'Lab certificate'])
# Test 11: Invoice ref auto-fill from SO at create time
step('SYSTEM', 'Test 11 Invoice ref auto-fills from SO.client_order_ref')
step('SYSTEM', 'Test 11 - Invoice ref auto-fills from SO.client_order_ref')
test_inv2 = env['account.move'].sudo().create({
'move_type': 'out_invoice',
'partner_id': customer.id,
@@ -562,7 +562,7 @@ finding('PASS' if test_inv2.ref == so.client_order_ref else 'FAIL',
test_inv2.sudo().unlink()
# =====================================================================
banner('PHASE 5 Operators run their work orders (REAL-TIME timers)')
banner('PHASE 5 - Operators run their work orders (REAL-TIME timers)')
# =====================================================================
# Pick a bath for the plating step so chemistry logging has somewhere
@@ -592,7 +592,7 @@ WO_DURATIONS_BEFORE = {wo.id: wo.duration for wo in mo.workorder_ids}
for wo, op_user, op_key in assignments:
actor = PERSONAS[op_key][0].split()[0].upper()
step(actor, f'Picks up "{wo.name}" on iPad taps START')
step(actor, f'Picks up "{wo.name}" on iPad - taps START')
wo_op = wo.with_user(op_user).sudo()
started_state = wo_op.state
try:
@@ -603,7 +603,7 @@ for wo, op_user, op_key in assignments:
continue
show(f' state', f'{started_state}{wo_op.state}')
# Real-time work sleep 2s for non-plating, 4s for plating
# Real-time work - sleep 2s for non-plating, 4s for plating
work_seconds = 4 if 'plating' in (wo.name or '').lower() else 2
show(f' working...', f'{work_seconds}s elapsed')
time.sleep(work_seconds)
@@ -624,7 +624,7 @@ for wo, op_user, op_key in assignments:
})
show(' chemistry log', f'{log.id} ({len(log.line_ids)} readings)')
else:
finding('WARN', 'chemistry', 'no fusion.plating.bath.parameter records log skipped')
finding('WARN', 'chemistry', 'no fusion.plating.bath.parameter records - log skipped')
# Frank logs Fischerscope thickness readings during inspection
if 'inspect' in (wo.name or '').lower() and op_key == 'frank':
@@ -687,14 +687,14 @@ finding('PASS' if distinct_operators_logged > 1 else 'WARN',
f'{distinct_operators_logged} distinct operators recorded')
# =====================================================================
banner('PHASE 6 Hannah closes the MO')
banner('PHASE 6 - Hannah closes the MO')
# =====================================================================
step('HANNAH', 'Marks MO done')
try:
mo_h.button_mark_done()
except Exception as e:
print(f' [info] mark_done: {e} falling back')
print(f' [info] mark_done: {e} - falling back')
try:
mo_h.qty_producing = mo.product_qty
mo_h._action_done()
@@ -703,7 +703,7 @@ except Exception as e:
finding('PASS' if mo.state == 'done' else 'FAIL', 'MO done', f'state={mo.state}')
# =====================================================================
banner('PHASE 7 Frank inspects + CoC')
banner('PHASE 7 - Frank inspects + CoC')
# =====================================================================
certs = env['fp.certificate'].search([('production_id', '=', mo.id)])
@@ -727,7 +727,7 @@ if coc:
step('FRANK', 'Reviews + signs CoC (already auto-issued)')
# =====================================================================
banner('PHASE 8 Dave drives the delivery')
banner('PHASE 8 - Dave drives the delivery')
# =====================================================================
dlv = env['fusion.plating.delivery'].search(
@@ -749,13 +749,13 @@ if dlv:
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')
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',
'notes': 'E2E sim - recipient on dock signed for parts',
})
dlv.sudo().pod_id = pod.id
show(' POD captured', f'{pod.name} (id={pod.id})')
@@ -775,7 +775,7 @@ if dlv:
'chain of custody', f'{len(coc_logs)} entries')
# =====================================================================
banner('PHASE 9 Linda creates + posts invoice')
banner('PHASE 9 - Linda creates + posts invoice')
# =====================================================================
step('LINDA', 'Creates invoice from SO')
@@ -797,7 +797,7 @@ if inv:
'invoice posted', f'state={inv.state}, payment_state={inv.payment_state}')
# =====================================================================
banner('PHASE 10 Compliance + notification audit')
banner('PHASE 10 - Compliance + notification audit')
# =====================================================================
# Notification log
@@ -858,7 +858,7 @@ if BakeWin is not None and job:
# Each operator can see their OWN assigned WOs via the tablet
# (queue is a TransientModel; tablet calls build_for_user on load)
# Reset MO to make some WOs ready/progress for queue test BEFORE this is run
# would be needed but the queue should still work for any in-progress WOs
# would be needed - but the queue should still work for any in-progress WOs
# elsewhere in the system that match the user.
OpQueue = env.get('fusion.plating.operator.queue')
if OpQueue is not None:

View File

@@ -57,7 +57,7 @@ mo_done = env['mrp.production'].search([('state', '=', 'done')], order='id desc'
mo_progress = env['mrp.production'].search([('state', 'in', ('confirmed', 'progress'))], limit=1)
# =====================================================================
section('1. Sale Order quote → confirmation')
section('1. Sale Order - quote → confirmation')
# =====================================================================
def t_so_no_partner():
@@ -82,7 +82,7 @@ gate('SO confirm blocked when customer on Account Hold', t_so_confirm_account_ho
# =====================================================================
section('2. Manufacturing Order confirm + done')
section('2. Manufacturing Order - confirm + done')
# =====================================================================
def t_mo_confirm_no_facility():
@@ -107,7 +107,7 @@ gate('MO confirm blocked when no facility resolvable', t_mo_confirm_no_facility,
# =====================================================================
section('3. Work Order start (per kind) + finish')
section('3. Work Order - start (per kind) + finish')
# =====================================================================
if mo_progress:
@@ -201,7 +201,7 @@ if mask_wo:
# =====================================================================
section('4. Receiving accept/discrepancy with damage')
section('4. Receiving - accept/discrepancy with damage')
# =====================================================================
if customer:
@@ -218,14 +218,14 @@ if customer:
'resolved': False,
})
r.action_start_inspection()
r.action_accept() # should fail unresolved damage
r.action_accept() # should fail - unresolved damage
gate('Receiving accept blocked when unresolved damage exists',
t_recv_accept_with_unresolved_damage,
['unresolved damage'])
# =====================================================================
section('5. Certificate action_issue')
section('5. Certificate - action_issue')
# =====================================================================
if mo_done:
@@ -242,7 +242,7 @@ if mo_done:
# =====================================================================
section('6. Delivery schedule → en_route → delivered')
section('6. Delivery - schedule → en_route → delivered')
# =====================================================================
if customer:
@@ -257,7 +257,7 @@ if customer:
# =====================================================================
section('7. Invoice post')
section('7. Invoice - post')
# =====================================================================
if customer:
@@ -294,7 +294,7 @@ if customer:
# =====================================================================
section('8. QMS NCR / CAPA / Discharge sample close')
section('8. QMS - NCR / CAPA / Discharge sample close')
# =====================================================================
def t_ncr_close_missing():
@@ -327,7 +327,7 @@ gate('Discharge sample close blocked without lab evidence', t_discharge_close_mi
# =====================================================================
section('9. SUSPECTED GAPS these probably AREN\'T enforced today')
section('9. SUSPECTED GAPS - these probably AREN\'T enforced today')
# =====================================================================
# Each of these MIGHT slip through silently. If they do, we'll see GAP.
@@ -364,8 +364,8 @@ if customer:
['mismatch', 'short', 'discrepancy', 'qty', 'quantity'])
# 9c. MO done without all WOs done Odoo enforces this natively, so should PASS
# Skip Odoo handles it.
# 9c. MO done without all WOs done - Odoo enforces this natively, so should PASS
# Skip - Odoo handles it.
# 9d. Cert issue without thickness readings (only blocks when partner
# is flagged aerospace via x_fc_strict_thickness_required)
@@ -424,7 +424,7 @@ if bath:
def t_bath_log_no_lines():
env['fusion.plating.bath.log'].sudo().create({
'bath_id': bath.id,
}) # no line_ids should this be allowed?
}) # no line_ids - should this be allowed?
gate('Bath log create blocked without any parameter readings',
t_bath_log_no_lines,
['line', 'reading', 'parameter'])

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
"""One-off rate-quote sweep across every FedEx service code.
Loops the full carrier selection (~38 services) against two routes
CA domestic (matching SO-30045) and CA → US to figure out which
Loops the full carrier selection (~38 services) against two routes -
CA domestic (matching SO-30045) and CA → US - to figure out which
services are valid for the shipping lanes EN Technologies actually
uses. Prints a CSV-ish matrix to stdout so the report can be pasted
straight into chat.
@@ -40,13 +40,13 @@ sender = SimpleNamespace(
is_company=True,
)
# Route A CA domestic (Niagara Falls, ON)
# Route A - CA domestic (Niagara Falls, ON)
ca_recipient = env['res.partner'].search([
('city', 'ilike', 'Niagara Falls'), ('country_id.code', '=', 'CA'),
], limit=1)
assert ca_recipient.exists(), 'No CA partner found for the domestic route.'
# Route B CA → US (a real US partner with a complete address)
# Route B - CA → US (a real US partner with a complete address)
us_recipient = env['res.partner'].search([
('country_id.code', '=', 'US'), ('city', '!=', False),
('zip', '!=', False), ('state_id', '!=', False),
@@ -68,7 +68,7 @@ if not us_recipient:
'email': 'us@test.com',
})
# Sender partner use the company partner for proper address resolution.
# Sender partner - use the company partner for proper address resolution.
sender_partner = env.company.partner_id

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Per-step compliance audit walks every WO of the most recent MO
"""Per-step compliance audit - walks every WO of the most recent MO
and reports which compliance data points are captured vs missing,
broken down by WO kind.
@@ -32,7 +32,7 @@ KIND_RULES = {
('x_fc_oven_id', 'CRITICAL', 'Which oven'),
('x_fc_bake_temp', 'CRITICAL', 'Setpoint temp (Nadcap req)'),
('x_fc_bake_duration_hours','CRITICAL','Actual bake duration'),
('chart_recorder_ref', 'CRITICAL', 'Chart-recorder ref on the OVEN auditor demands the chart for the run'),
('chart_recorder_ref', 'CRITICAL', 'Chart-recorder ref on the OVEN - auditor demands the chart for the run'),
('duration', 'CRITICAL', 'WO timer duration'),
('x_fc_started_by_user_id','IMPORTANT','Who started'),
('x_fc_finished_by_user_id','IMPORTANT','Who finished'),
@@ -40,7 +40,7 @@ KIND_RULES = {
'mask': [
('x_fc_assigned_user_id', 'CRITICAL', 'Operator'),
('duration', 'CRITICAL', 'Run time'),
('masking_material', 'IMPORTANT','Which material needed for stripping later'),
('masking_material', 'IMPORTANT','Which material - needed for stripping later'),
('x_fc_started_by_user_id','IMPORTANT','Who started'),
('x_fc_finished_by_user_id','IMPORTANT','Who finished'),
],
@@ -72,7 +72,7 @@ def check_field(wo, field):
if field == 'bath_log_during_window':
# Look for any bath log on this WO's bath, between start+finish
if not wo.x_fc_bath_id or not wo.x_fc_started_at or not wo.x_fc_finished_at:
return ('', False, 'no log searchable')
return ('-', False, 'no log searchable')
Log = env['fusion.plating.bath.log']
n = Log.search_count([
('bath_id', '=', wo.x_fc_bath_id.id),
@@ -82,11 +82,11 @@ def check_field(wo, field):
return (f'{n} log(s)', n > 0, '')
if field == 'chart_recorder_ref':
ref = wo.x_fc_oven_id.chart_recorder_ref if wo.x_fc_oven_id else False
return (ref or '', bool(ref), 'on oven')
return (ref or '-', bool(ref), 'on oven')
if field == 'masking_material':
val = wo.x_fc_masking_material if 'x_fc_masking_material' in wo._fields else False
if not val:
return ('', False, '')
return ('-', False, '')
label = dict(wo._fields['x_fc_masking_material'].selection).get(val, val)
return (label, True, '')
if field == 'thickness_readings':
@@ -99,7 +99,7 @@ def check_field(wo, field):
('production_id', '=', wo.production_id.id),
])
if not rs:
return ('', False, 'no readings')
return ('-', False, 'no readings')
n_with = sum(1 for r in rs if r.calibration_std_ref)
return (f'{n_with}/{len(rs)} have cal std', n_with == len(rs), '')
if field == 'gauge_serial':
@@ -107,14 +107,14 @@ def check_field(wo, field):
r = env['fp.thickness.reading'].search(
[('production_id', '=', wo.production_id.id)], limit=1)
if not r:
return ('', False, 'no readings')
return (r.equipment_model or '', bool(r.equipment_model), 'from reading.equipment_model')
return ('-', False, 'no readings')
return (r.equipment_model or '-', bool(r.equipment_model), 'from reading.equipment_model')
# Direct field on WO
val = getattr(wo, field, False) if field in wo._fields else None
if val is None:
return ('(field n/a)', False, '')
if hasattr(val, '_name'):
label = val.display_name if val else ''
label = val.display_name if val else '-'
return (label, bool(val.ids), '')
if isinstance(val, (int, float)):
return (str(val), val > 0, '')
@@ -158,7 +158,7 @@ for wo in mo.workorder_ids.sorted('sequence'):
print(ln)
# =====================================================================
banner('SUMMARY gaps per WO kind across this MO')
banner('SUMMARY - gaps per WO kind across this MO')
# =====================================================================
for kind, gaps in PER_KIND.items():
@@ -169,7 +169,7 @@ for kind, gaps in PER_KIND.items():
print(f' × {field:<30} missing in {n} WO(s)')
print(f'\n Totals: {GAP_TOTALS["CRITICAL"]} CRITICAL gaps, {GAP_TOTALS["IMPORTANT"]} IMPORTANT gaps')
print('\n Note: "missing" doesn\'t always mean "broken" some fields')
print('\n Note: "missing" doesn\'t always mean "broken" - some fields')
print(' are optional today but should be required for stricter')
print(' AS9100 / Nadcap compliance. See the per-kind list to')
print(' decide which are real bugs vs roadmap items.')

View File

@@ -1,5 +1,5 @@
env = env # noqa
# Use the SAME path the web client uses (the cog menu) _get_bindings.
# Use the SAME path the web client uses (the cog menu) - _get_bindings.
# This honours the new sequence-based sort we just added.
MODELS = ['sale.order', 'account.move', 'stock.picking', 'mrp.production',
'fusion.plating.delivery', 'account.payment', 'fusion.plating.portal.job',

View File

@@ -6,11 +6,11 @@ For each major model in the quote → invoice workflow:
• 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
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.
The report is purely diagnostic - it changes nothing in the DB.
"""
env = env # noqa
from collections import defaultdict
@@ -48,49 +48,49 @@ def show_field_audit(model_name, record, candidate_fields):
)
sym = {'CRITICAL': '🔴', 'IMPORTANT': '🟡', 'NICE': ''}[severity]
marker = '✗ EMPTY' if is_empty else '✓ filled'
val_str = str(val)[:60] if not is_empty else ''
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')
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'),
('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'),
('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'),
('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')
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'),
('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_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'),
('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')
section('3. Receiving (fp.receiving) - most recent record')
# =====================================================================
recv = env['fp.receiving'].search([], order='id desc', limit=1)
@@ -100,14 +100,14 @@ show_field_audit('fp.receiving', recv, [
('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'),
('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')
section('4. MRP Production (mrp.production) - most recent MO')
# =====================================================================
mo = env['mrp.production'].search([('state', '=', 'done')], order='id desc', limit=1)
@@ -115,16 +115,16 @@ 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_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'),
('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')
section('5. Work Orders (mrp.workorder) - wet WO from most recent MO')
# =====================================================================
wet_wo = mo.workorder_ids.filtered(
@@ -132,11 +132,11 @@ wet_wo = mo.workorder_ids.filtered(
)[: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_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_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'),
@@ -149,7 +149,7 @@ 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'),
('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)'),
@@ -157,7 +157,7 @@ show_field_audit('fusion.plating.bath.log', baths, [
])
# =====================================================================
section('7. Certificate (fp.certificate) most recent CoC')
section('7. Certificate (fp.certificate) - most recent CoC')
# =====================================================================
coc = env['fp.certificate'].search(
@@ -165,16 +165,16 @@ coc = env['fp.certificate'].search(
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'),
('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_by_id', 'CRITICAL', 'Inspector signature - who certified this'),
('issued_date', 'CRITICAL', 'When issued'),
('state', 'CRITICAL', 'draft/issued/voided NOT issued = NOT compliant'),
('state', 'CRITICAL', 'draft/issued/voided - NOT issued = NOT compliant'),
])
# =====================================================================
@@ -185,13 +185,13 @@ 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)'),
('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'),
('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'),
('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'),
])
@@ -213,11 +213,11 @@ show_field_audit('fusion.plating.delivery', dlv, [
('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'),
('pod_id', 'CRITICAL', 'Proof of delivery - without it, we can\'t bill'),
])
# =====================================================================
section('10. Invoice (account.move) most recent posted invoice')
section('10. Invoice (account.move) - most recent posted invoice')
# =====================================================================
inv = env['account.move'].search(
@@ -225,10 +225,10 @@ inv = env['account.move'].search(
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', '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'),
('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'),
@@ -236,7 +236,7 @@ show_field_audit('account.move', inv, [
])
# =====================================================================
section('11. Workforce Quality Hold + NCR + CAPA (open + completed)')
section('11. Workforce - Quality Hold + NCR + CAPA (open + completed)')
# =====================================================================
# Sample Quality Hold if any
@@ -244,9 +244,9 @@ 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'),
('partner_id', 'CRITICAL', 'Customer - without it we can\'t notify'),
('mo_id', 'CRITICAL', 'Which MO'),
('hold_reason', 'CRITICAL', 'Selection categorize the issue'),
('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'),
@@ -262,10 +262,10 @@ if ncr is not None:
('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'),
('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'),
])
@@ -278,7 +278,7 @@ if capa is not None:
('owner_id', 'CRITICAL', 'Owner / champion'),
('due_date', 'CRITICAL', 'Deadline'),
('problem_description', 'CRITICAL', 'What\'s the recurring issue'),
('root_cause', 'CRITICAL', 'Why-why analysis required'),
('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'),
@@ -299,8 +299,8 @@ if DS is not None:
('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'),
('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')
@@ -315,11 +315,11 @@ if WM is not None:
('quantity', 'CRITICAL', 'How much'),
('uom', 'CRITICAL', 'Unit'),
('shipped_date', 'CRITICAL', 'When shipped'),
('received_date', 'CRITICAL', 'When received at disposal closes the loop'),
('received_date', 'CRITICAL', 'When received at disposal - closes the loop'),
])
# =====================================================================
section('SUMMARY gap counts by severity')
section('SUMMARY - gap counts by severity')
# =====================================================================
print(' See per-model details above. Critical gaps are real')