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:
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 ===')
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user