# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # # Fusion Plating — Demo Seeder # ================================ # Story-driven demo data: six customers, every workflow stage populated, # historical depth for dashboards/trends, exception cases for enforcement. # # Run: odoo shell -c /etc/odoo/odoo.conf -d admin < fp_demo_seed.py # Safe to run multiple times (idempotent on the records it creates). import base64 import logging import random from datetime import datetime, timedelta from odoo import Command, fields _log = logging.getLogger('fp_demo') def LOG(msg): print(f"[FP-DEMO] {msg}") # ============================================================ # Helpers # ============================================================ def soc(model, key_field, key_value, vals): """Search-or-create a singleton by a unique key field.""" rec = env[model].search([(key_field, '=', key_value)], limit=1) if rec: rec.write({k: v for k, v in vals.items() if k != key_field}) return rec return env[model].create(vals) def backdate(rec, days_ago): """Force create_date back in time, so history tabs look real.""" if not rec or not rec.ids: return d = datetime.now() - timedelta(days=days_ago) env.cr.execute( f"UPDATE {rec._table} SET create_date = %s, write_date = %s WHERE id IN %s", (d, d, tuple(rec.ids)), ) def set_date_field(rec, field, days_ago): d = datetime.now() - timedelta(days=days_ago) rec.write({field: d}) # ============================================================ # Phase 0.5: Company CoC settings (accreditation badges + signature) # ============================================================ # We generate clean PIL-based badge PNGs for Nadcap / AS9100 / CGP # so the CoC PDF renders complete without the client having to upload # anything. They can still replace them with the real trademarked logos # via Settings → Fusion Plating → Accreditation Logos whenever they want. def _make_badge(lines, width=420, height=220, bg='#0066A1', fg='white', border_color='#003d66', border_px=6, font_size=42, subtitle=None, subtitle_color='white', subtitle_size=18): """Render a rectangular badge with centred stacked text.""" try: from PIL import Image, ImageDraw, ImageFont except ImportError: LOG(" PIL not available — skipping badge generation") return None import io img = Image.new('RGB', (width, height), bg) draw = ImageDraw.Draw(img) # Border draw.rectangle([(border_px // 2, border_px // 2), (width - border_px, height - border_px)], outline=border_color, width=border_px) # Pick a sans-serif bold font font = None for candidate in ( '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf', '/System/Library/Fonts/Helvetica.ttc', 'DejaVuSans-Bold.ttf', ): try: font = ImageFont.truetype(candidate, font_size) break except Exception: continue if font is None: font = ImageFont.load_default() sub_font = None if subtitle: for candidate in ( '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', ): try: sub_font = ImageFont.truetype(candidate, subtitle_size) break except Exception: continue if sub_font is None: sub_font = font # Stack lines vertically, centred line_gap = int(font_size * 1.2) total_h = line_gap * len(lines) + (subtitle_size + 10 if subtitle else 0) y = (height - total_h) // 2 for line in lines: bbox = draw.textbbox((0, 0), line, font=font) tw = bbox[2] - bbox[0] draw.text(((width - tw) / 2, y), line, fill=fg, font=font) y += line_gap if subtitle: y += 4 bbox = draw.textbbox((0, 0), subtitle, font=sub_font) tw = bbox[2] - bbox[0] draw.text(((width - tw) / 2, y), subtitle, fill=subtitle_color, font=sub_font) buf = io.BytesIO() img.save(buf, format='PNG', optimize=True) return base64.b64encode(buf.getvalue()) def _make_signature(name, width=700, height=180, color='#00338D'): """Render a plausible handwritten-looking signature from a name.""" try: from PIL import Image, ImageDraw, ImageFont except ImportError: return None import io img = Image.new('RGBA', (width, height), (255, 255, 255, 0)) draw = ImageDraw.Draw(img) # Prefer an italic / oblique font for the script look font = None for candidate in ( '/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Italic.ttf', '/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-BoldOblique.ttf', ): try: font = ImageFont.truetype(candidate, 88) break except Exception: continue if font is None: font = ImageFont.load_default() draw.text((30, 30), name, fill=color, font=font) # Underline flourish bbox = draw.textbbox((30, 30), name, font=font) draw.line( [(30, bbox[3] + 10), (bbox[2] + 80, bbox[3] + 10)], fill=color, width=3, ) buf = io.BytesIO() img.save(buf, format='PNG', optimize=True) return base64.b64encode(buf.getvalue()) LOG("Phase 0.5: Company CoC settings (badges + signature)") _company = env.company # Build accreditation badges (only if not already set to avoid clobbering # real logos the client uploaded via Settings) if not _company.x_fc_nadcap_logo: _company.x_fc_nadcap_logo = _make_badge( ['NADCAP', 'ACCREDITED'], bg='#0066A1', border_color='#003d66', subtitle='Administered by PRI', ) if not _company.x_fc_as9100_logo: _company.x_fc_as9100_logo = _make_badge( ['AS9100D', 'CERTIFIED'], bg='#2B6CB0', border_color='#1a4d80', subtitle='ISO 9001', ) if not _company.x_fc_cgp_logo: _company.x_fc_cgp_logo = _make_badge( ['CGP', 'REGISTERED'], bg='#C8102E', border_color='#8B0A1F', subtitle="Canada's Controlled Goods Program", subtitle_size=15, ) _company.x_fc_nadcap_active = True _company.x_fc_as9100_active = True _company.x_fc_cgp_active = True # Designate a demo owner: a user named "Kris Pathinather" so the # Certified By / Name line on the CoC matches the signature image. _kris_user = env['res.users'].search([('login', '=', 'kris.pathinather')], limit=1) if not _kris_user: _kris_user = env['res.users'].with_context(no_reset_password=True).create({ 'name': 'Kris Pathinather', 'login': 'kris.pathinather', 'email': 'kris@enplating.ca', }) _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}") # ============================================================ # Phase 1: Customers (6 stories) # ============================================================ LOG("Phase 1: Customers") def ensure_partner(ref, vals): p = env['res.partner'].search([('ref', '=', ref)], limit=1) if p: p.write(vals) return p vals = dict(vals) vals['ref'] = ref vals.setdefault('customer_rank', 1) return env['res.partner'].create(vals) amphenol = ensure_partner('FPD-AMPHENOL', { 'name': 'Amphenol Canada Corp.', 'email': 'jimip@amphenolcanada.com', 'phone': '+1 (647) 577-3880', 'street': '605 Milner Ave', 'city': 'Toronto', 'zip': 'M1B 5X6', 'country_id': env.ref('base.ca').id, 'state_id': env.ref('base.state_ca_on').id, 'website': 'amphenolcanada.com', }) # Give Amphenol their trademark blue-block logo if not amphenol.image_1920: amphenol.image_1920 = _make_badge( ['Amphenol'], width=320, height=200, bg='#005EB8', border_color='#003c75', font_size=46, subtitle='Canada Corp.', subtitle_size=20, ) magellan = ensure_partner('FPD-MAGELLAN', { 'name': 'Magellan Aerospace Ltd', 'email': 'quality@magellan.aero', 'phone': '+1 (204) 231-8000', 'street': '3160 Derry Rd East', 'city': 'Mississauga', 'zip': 'L4T 1A9', 'country_id': env.ref('base.ca').id, 'state_id': env.ref('base.state_ca_on').id, }) cyclone = ensure_partner('FPD-CYCLONE', { 'name': 'Cyclone Manufacturing Inc.', 'email': 'buyer@cyclone-mfg.com', 'phone': '+1 (905) 677-4441', 'street': '380 Sheldon Dr', 'city': 'Cambridge', 'zip': 'N1T 2B6', 'country_id': env.ref('base.ca').id, 'state_id': env.ref('base.state_ca_on').id, }) honeywell = ensure_partner('FPD-HONEYWELL', { 'name': 'Honeywell Aerospace Toronto', 'email': 'procurement.toronto@honeywell.com', 'phone': '+1 (416) 798-2100', 'street': '3333 Unity Dr', 'city': 'Mississauga', 'zip': 'L5L 3S6', 'country_id': env.ref('base.ca').id, 'state_id': env.ref('base.state_ca_on').id, }) westin = ensure_partner('FPD-WESTIN', { 'name': 'Westin Manufacturing Ltd', 'email': 'shop@westinmfg.ca', 'phone': '+1 (905) 791-8300', 'street': '200 Rexdale Blvd', 'city': 'Toronto', 'zip': 'M9W 1R1', 'country_id': env.ref('base.ca').id, 'state_id': env.ref('base.state_ca_on').id, }) delinquent = ensure_partner('FPD-DELINQUENT', { 'name': 'Delinquent Industries (DEMO — On Hold)', 'email': 'ap@delinquent-demo.com', 'phone': '+1 (416) 555-0199', 'street': '1 Overdue Lane', 'city': 'Toronto', 'zip': 'M1M 1M1', 'country_id': env.ref('base.ca').id, 'state_id': env.ref('base.state_ca_on').id, }) # Put this one on account hold if 'x_fc_account_hold' in delinquent._fields: delinquent.write({ 'x_fc_account_hold': True, 'x_fc_account_hold_reason': '$45,320 past due > 90 days. Cleared manager override only.', }) # Child contacts for Amphenol (used by contact_partner_id on CoC) jimi = env['res.partner'].search([ ('parent_id', '=', amphenol.id), ('name', '=', 'Jimi Patel'), ], limit=1) if not jimi: jimi = env['res.partner'].create({ 'name': 'Jimi Patel', 'parent_id': amphenol.id, 'function': 'Quality Manager', 'email': 'jimip@amphenolcanada.com', 'phone': '+1 (647) 577-3880', 'type': 'contact', }) LOG(f" 6 customers ready: {amphenol.name}, {magellan.name}, {cyclone.name}, " f"{honeywell.name}, {westin.name}, {delinquent.name}") # ============================================================ # Phase 2: Coating Configurations + Recipes # ============================================================ LOG("Phase 2: Coating configs") def ensure_process_type(code, name, family='plating'): pt = env['fusion.plating.process.type'].search([('code', '=', code)], limit=1) if pt: return pt cat = env['fusion.plating.process.category'].search([], limit=1) return env['fusion.plating.process.type'].create({ 'code': code, 'name': name, 'category_id': cat.id if cat else False, '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') 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') def ensure_coating(name, process_type, **extras): cfg = env['fp.coating.config'].search([('name', '=', name)], limit=1) vals = { 'name': name, 'process_type_id': process_type.id, } vals.update(extras) if cfg: cfg.write(vals) return cfg return env['fp.coating.config'].create(vals) coating_en_mid = ensure_coating( 'EN Mid-Phos AMS 2404', en_midphos, phosphorus_level='mid_phos', thickness_min=0.5, thickness_max=1.0, thickness_uom='mils', spec_reference='AMS 2404', certification_level='nadcap', requires_bake_relief=True, bake_window_hours=4.0, bake_temperature=375.0, bake_duration_hours=23.0, ) coating_en_low = ensure_coating( 'EN Low-Phos MIL-C-26074', en_lowphos, phosphorus_level='low_phos', thickness_min=0.3, thickness_max=0.6, thickness_uom='mils', spec_reference='MIL-C-26074', certification_level='mil_spec', ) coating_pass = ensure_coating( 'Passivation AMS 2700 Rev G', passivation, thickness_min=0, thickness_max=0, thickness_uom='mils', spec_reference='AMS 2700 Rev G Method 1 Type 2 Class 2', certification_level='nadcap', ) coating_chrome = ensure_coating( 'Hard Chrome AMS 2406', hard_chrome, thickness_min=1.0, thickness_max=3.0, thickness_uom='mils', spec_reference='AMS 2406', certification_level='mil_spec', requires_bake_relief=True, bake_window_hours=4.0, bake_temperature=375.0, bake_duration_hours=23.0, ) LOG(f" 4 coating configs: {coating_en_mid.name}, {coating_en_low.name}, " f"{coating_pass.name}, {coating_chrome.name}") # ============================================================ # Phase 3: Parts Catalog # ============================================================ LOG("Phase 3: Part catalog") PARTS = [ # (partner, pn, name, substrate, complexity, surface_area, revisions) (amphenol, 'VS-ESMC6H00801P01', 'Shell, Connector Backshell', 'stainless', 'moderate', 4.2, 2), (amphenol, 'VS-PQR8440', 'Pin Block Assembly', 'stainless', 'complex', 6.8, 3), (amphenol, 'VS-HSA201-B', 'Heat Sink Arm', 'aluminium', 'moderate', 12.4, 1), (amphenol, 'VS-COV-2401', 'RF Shield Cover', 'aluminium', 'simple', 8.9, 1), (magellan, 'MG-WG-8801', 'Engine Mount Bracket', 'steel', 'complex', 18.2, 2), (magellan, 'MG-CYL-550', 'Hydraulic Cylinder Housing', 'steel', 'complex', 22.1, 1), (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), (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), (westin, 'WM-HSG-201', 'Sensor Housing', 'aluminium', 'moderate', 5.3, 1), (westin, 'WM-PLT-308', 'Mounting Plate', 'aluminium', 'simple', 6.1, 1), (delinquent, 'DL-TEST-01', 'Demo Hold-Blocked Part', 'steel', 'simple', 3.0, 1), ] created_parts = {} for partner, pn, name, sub, cx, sa, revs in PARTS: existing = env['fp.part.catalog'].search([ ('partner_id', '=', partner.id), ('part_number', '=', pn), ], limit=1) if existing: created_parts[pn] = existing continue part = env['fp.part.catalog'].create({ 'partner_id': partner.id, 'part_number': pn, 'name': name, 'substrate_material': sub, 'complexity': cx, 'surface_area': sa, 'surface_area_uom': 'sq_in', 'revision': 'Rev 1', 'revision_number': 1, 'is_latest_revision': True, 'geometry_source': 'pdf_drawing', }) # Create extra revisions (Rev 2, Rev 3) for r in range(2, revs + 1): # Mark previous latest false env['fp.part.catalog'].search([ '|', ('id', '=', part.id), ('parent_part_id', '=', part.id), ('is_latest_revision', '=', True), ]).write({'is_latest_revision': False}) env['fp.part.catalog'].create({ 'partner_id': partner.id, 'part_number': pn, 'name': name, 'substrate_material': sub, 'complexity': cx, 'surface_area': sa * (1.0 + 0.05 * (r - 1)), 'surface_area_uom': 'sq_in', 'revision': f'Rev {r}', 'revision_number': r, 'parent_part_id': part.id, 'is_latest_revision': (r == revs), 'geometry_source': 'pdf_drawing', 'revision_note': f'Revision {r} — minor geometry update', }) created_parts[pn] = part LOG(f" {len(created_parts)} parts with revisions") # ============================================================ # Phase 4: Customer price lists # ============================================================ LOG("Phase 4: Customer price lists") PRICE_LISTS = [ (amphenol, coating_en_mid, 45.00, 'per_part', 1), (amphenol, coating_en_mid, 38.00, 'per_part', 50), (amphenol, coating_en_mid, 32.00, 'per_part', 200), (amphenol, coating_pass, 12.00, 'per_part', 1), (magellan, coating_en_mid, 65.00, 'per_part', 1), (magellan, coating_chrome, 120.00, 'per_part', 1), (cyclone, coating_en_mid, 52.00, 'per_part', 1), (cyclone, coating_pass, 15.00, 'per_part', 1), (honeywell, coating_en_low, 72.00, 'per_part', 1), (westin, coating_en_low, 48.00, 'per_part', 1), ] for partner, cfg, price, uom, minq in PRICE_LISTS: existing = env['fp.customer.price.list'].search([ ('partner_id', '=', partner.id), ('coating_config_id', '=', cfg.id), ('min_quantity', '=', minq), ], limit=1) if not existing: env['fp.customer.price.list'].create({ 'partner_id': partner.id, 'coating_config_id': cfg.id, 'unit_price': price, 'price_uom': uom, 'min_quantity': minq, 'effective_from': (datetime.now() - timedelta(days=365)).date(), }) LOG(f" {len(PRICE_LISTS)} price list entries") # ============================================================ # Phase 5: Invoice strategy defaults per customer # ============================================================ LOG("Phase 5: Invoice strategy defaults") def ensure_strategy(partner, strategy, **extras): rec = env['fp.invoice.strategy.default'].search([('partner_id', '=', partner.id)], limit=1) vals = {'partner_id': partner.id, 'default_strategy': strategy} vals.update(extras) if rec: rec.write(vals) else: env['fp.invoice.strategy.default'].create(vals) ensure_strategy(amphenol, 'net_terms') ensure_strategy(magellan, 'progress', default_deposit_percent=40.0) ensure_strategy(cyclone, 'deposit', default_deposit_percent=50.0) ensure_strategy(honeywell, 'net_terms') ensure_strategy(westin, 'cod_prepay') LOG(" 5 customer strategy defaults") # ============================================================ # Phase 6: Baths, bath logs, replenishment suggestions # ============================================================ 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', 'code': 'MAIN', 'company_id': env.company.id, }) # Tanks def ensure_tank(code, facility_, proc_type): tk = env['fusion.plating.tank'].search([('code', '=', code)], limit=1) if tk: return tk return env['fusion.plating.tank'].create({ 'name': f'Tank {code}', 'code': code, 'facility_id': facility_.id, 'volume': 1000.0, }) tank_en = ensure_tank('TK-EN-01', facility, en_midphos) tank_pass = ensure_tank('TK-PASS-01', facility, passivation) def ensure_bath(name, tank, proc_type, state='operational'): b = env['fusion.plating.bath'].search([('name', '=', name)], limit=1) if b: return b return env['fusion.plating.bath'].create({ 'name': name, 'tank_id': tank.id, 'process_type_id': proc_type.id, 'state': state, 'makeup_date': datetime.now() - timedelta(days=45), 'volume': 950.0, }) bath_en = ensure_bath('EN-BATH-A', tank_en, en_midphos) bath_pass = ensure_bath('PASS-BATH-A', tank_pass, passivation) # Ensure there are bath parameters def ensure_param(name, code, uom, target_min, target_max, param_type='concentration'): p = env['fusion.plating.bath.parameter'].search([('code', '=', code)], limit=1) if p: return p return env['fusion.plating.bath.parameter'].create({ 'name': name, 'code': code, 'uom': uom, 'target_min': target_min, 'target_max': target_max, 'parameter_type': param_type, 'warning_tolerance': 10.0, }) p_nickel = ensure_param('Nickel Concentration', 'Ni', 'g/L', 4.5, 6.5) p_ph = ensure_param('pH', 'PH', 'pH', 4.6, 5.2, 'ph') p_temp = ensure_param('Temperature', 'TEMP', '°C', 87.0, 91.0, 'temperature') # Attach parameters to process type if not already 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 for bath, params in [(bath_en, [p_nickel, p_ph, p_temp])]: for p in params: existing = env['fusion.plating.bath.target'].search([ ('bath_id', '=', bath.id), ('parameter_id', '=', p.id), ], limit=1) if not existing: env['fusion.plating.bath.target'].create({ 'bath_id': bath.id, 'parameter_id': p.id, 'target_min': p.target_min, 'target_max': p.target_max, }) # 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', 'bath_id': bath_en.id, 'parameter_id': p_nickel.id, 'trigger': 'below_min', 'product_name': 'Nickel Sulfamate 30% (Elnic 101)', 'dose_rate': 0.25, 'dose_uom': 'ml', 'min_dose': 50.0, }) # 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 # Most readings in-spec, last 2 out-of-spec on nickel if days_ago <= 3: ni_val = 4.0 # Below min 4.5 → triggers replenishment suggestion ph_val = 5.0 temp_val = 89.0 else: ni_val = random.uniform(5.0, 6.0) ph_val = random.uniform(4.8, 5.1) temp_val = random.uniform(88.0, 90.0) log_date = datetime.now() - timedelta(days=days_ago) log = env['fusion.plating.bath.log'].create({ 'bath_id': bath_en.id, 'log_date': log_date, 'operator_id': env.user.id, 'shift': random.choice(['day', 'evening']), 'line_ids': [ Command.create({'parameter_id': p_nickel.id, 'value': ni_val}), Command.create({'parameter_id': p_ph.id, 'value': ph_val}), Command.create({'parameter_id': p_temp.id, 'value': temp_val}), ], }) backdate(log, days_ago) LOG(f" 2 baths, 3 parameters, 1 replenishment rule, historical logs seeded") # ============================================================ # Phase 7: Racks (1 needs strip, 3 active) # ============================================================ LOG("Phase 7: Racks") def ensure_rack(name, state='active', mto=0.0): r = env['fusion.plating.rack'].search([ ('facility_id', '=', facility.id), ('name', '=', name), ], limit=1) if r: return r return env['fusion.plating.rack'].create({ 'name': name, 'facility_id': facility.id, 'rack_type': 'rack', 'capacity': 24, 'contact_points': 8, 'mto_count': mto, 'strip_interval_mto': 3.0, 'state': state, 'last_stripped_date': datetime.now() - timedelta(days=random.randint(30, 90)), 'strips_count': random.randint(2, 8), }) rack_1 = ensure_rack('RACK-001', 'active', 1.2) rack_2 = ensure_rack('RACK-002', 'active', 2.1) rack_3 = ensure_rack('RACK-003', 'active', 0.4) rack_old = ensure_rack('RACK-004', 'active', 3.2) # Force the needs_strip compute to run by rewriting mto_count rack_old.write({'mto_count': 3.2}) barrel_1 = ensure_rack('BARREL-001', 'active', 1.8) LOG(" 5 racks seeded (1 will show 'Needs Strip')") # ============================================================ # Phase 8: Generic Plating Service product (used by SOs) # ============================================================ LOG("Phase 8: Ensure FP-SERVICE product") product = env['product.product'].search([('default_code', '=', 'FP-SERVICE')], limit=1) if not product: product = env['product.product'].create({ 'name': 'Plating Service', 'default_code': 'FP-SERVICE', 'type': 'service', 'sale_ok': True, 'purchase_ok': False, 'list_price': 0, }) # Also a storable product for manufacturing (MOs need a BOM-routed product) widget = env['product.product'].search([('default_code', '=', 'FP-WIDGET')], limit=1) if not widget: widget = env['product.product'].create({ 'name': 'Plated Widget (Generic)', 'default_code': 'FP-WIDGET', 'type': 'consu', 'is_storable': True, 'sale_ok': True, 'purchase_ok': False, 'list_price': 0, }) # ============================================================ # Phase 9: Historical closed orders (3 months of activity) # ============================================================ LOG("Phase 9: Historical closed orders") def create_so_line(so_vals, desc, qty, price): so_vals.setdefault('order_line', []) so_vals['order_line'].append(Command.create({ 'product_id': product.id, 'name': desc, 'product_uom_qty': qty, 'price_unit': price, })) def register_payment(invoice): """Post + fully pay an invoice.""" if invoice.state == 'draft': invoice.action_post() try: wiz = env['account.payment.register'].with_context( active_model='account.move', active_ids=invoice.ids, active_id=invoice.id, ).create({'payment_date': invoice.invoice_date or fields.Date.today()}) return wiz._create_payments() except Exception as e: print(f" [register_payment] {invoice.name} failed: {e}") return False # 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): # 1. SO so_vals = { 'partner_id': partner.id, 'x_fc_invoice_strategy': strategy, 'x_fc_deposit_percent': deposit_pct, 'x_fc_coating_config_id': coating.id, 'x_fc_part_catalog_id': part_cat.id if part_cat else False, 'x_fc_po_number': f'PO-{random.randint(10000, 99999)}', '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})', 'product_uom_qty': qty, 'price_unit': unit_price, })], } # Bypass account hold for the seeder so = env['sale.order'].create(so_vals) try: so.action_confirm() except Exception as e: print(f" closed_job confirm failed: {e}") return None # 2. MO (manual create since FP-SERVICE has no BOM) mo = env['mrp.production'].create({ 'product_id': widget.id, 'product_qty': qty, 'origin': so.name, }) try: mo.action_confirm() except Exception as e: print(f" mo confirm: {e}") # 3. MO done (auto-creates delivery + cert) try: mo.button_mark_done() except Exception: mo.write({'state': 'done'}) # 4. Deliver delivery = env['fusion.plating.delivery'].search([ ('job_ref', '=', mo.x_fc_portal_job_id.name if mo.x_fc_portal_job_id else mo.name), ], limit=1) if delivery: try: delivery.action_schedule(); delivery.action_start_route(); delivery.action_mark_delivered() except Exception as e: print(f" delivery: {e}") # 5. Post + pay all invoices on the SO for inv in so.invoice_ids: if inv.state == 'draft': try: inv.action_post() except Exception: pass if inv.state == 'posted': register_payment(inv) # 6. Certificate cert = env['fp.certificate'].search([('production_id', '=', mo.id)], limit=1) if cert: # Add thickness readings for some (SPC data) if random.random() > 0.3 and coating.thickness_min and coating.thickness_max: target = (coating.thickness_min + coating.thickness_max) / 2 for n in range(1, random.randint(5, 9) + 1): env['fp.thickness.reading'].create({ 'certificate_id': cert.id, 'reading_number': n, 'nip_mils': random.uniform( target * 0.95, target * 1.05, ), 'ni_percent': random.uniform(93.5, 94.5), 'p_percent': random.uniform(6.0, 8.0), }) try: cert.action_issue() except Exception: cert.state = 'issued' # 7. Backdate everything backdate(so, days_ago) backdate(mo, days_ago) if delivery: backdate(delivery, days_ago - 2) set_date_field(delivery, 'delivered_at', days_ago - 2) for inv in so.invoice_ids: backdate(inv, days_ago - 1) inv.write({'invoice_date': (datetime.now() - timedelta(days=days_ago - 1)).date()}) if cert: backdate(cert, days_ago - 1) cert.write({'issue_date': (datetime.now() - timedelta(days=days_ago - 1)).date()}) return so # Only build history if we don't already have lots of closed orders closed_count = env['sale.order'].search_count([ ('partner_id', 'in', [amphenol.id, magellan.id, cyclone.id, honeywell.id, westin.id]), ('state', '=', 'sale'), ]) if closed_count < 10: # 8 closed jobs over 4 months, mostly Amphenol history_jobs = [ (amphenol, coating_en_mid, created_parts.get('VS-ESMC6H00801P01'), 2010, 32.00, 120, 'net_terms', 0), (amphenol, coating_en_mid, created_parts.get('VS-PQR8440'), 500, 38.00, 95, 'net_terms', 0), (amphenol, coating_pass, created_parts.get('VS-COV-2401'), 150, 12.00, 75, 'net_terms', 0), (amphenol, coating_en_mid, created_parts.get('VS-HSA201-B'), 300, 38.00, 55, 'net_terms', 0), (magellan, coating_chrome, created_parts.get('MG-WG-8801'), 45, 120.00, 90, 'progress', 40), (cyclone, coating_en_mid, created_parts.get('CY-STR-101'), 80, 52.00, 65, 'deposit', 50), (honeywell, coating_en_low, created_parts.get('HW-TBO-7001'), 25, 72.00, 40, 'net_terms', 0), (westin, coating_en_low, created_parts.get('WM-HSG-201'), 60, 48.00, 30, 'cod_prepay', 0), ] for row in history_jobs: try: make_closed_job(*row) except Exception as e: 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") # ============================================================ # Phase 10: Active orders in progress (showcase stages) # ============================================================ LOG("Phase 10: Active orders at various stages") def quick_so(partner, coating, part_cat, qty, price, strategy, deposit_pct=0.0, po_number=None, confirm=True): so = env['sale.order'].create({ 'partner_id': partner.id, 'x_fc_invoice_strategy': strategy, 'x_fc_deposit_percent': deposit_pct, 'x_fc_coating_config_id': coating.id, 'x_fc_part_catalog_id': part_cat.id if part_cat else False, 'x_fc_po_number': po_number or f'PO-{random.randint(10000, 99999)}', '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})', 'product_uom_qty': qty, 'price_unit': price, })], }) if confirm: try: so.action_confirm() except Exception as e: print(f" confirm: {e}") return so # Only seed active orders once active_marker = env['sale.order'].search_count([ ('x_fc_po_number', '=', 'FPDEMO-ACTIVE-MAGELLAN'), ]) if active_marker == 0: # 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, # 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') for inv in so_cyc.invoice_ids: try: inv.action_post() register_payment(inv) except: pass mo_cyc = env['mrp.production'].create({ 'product_id': widget.id, 'product_qty': 40, 'origin': so_cyc.name, }) try: mo_cyc.action_confirm() except: pass # 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') # Westin COD re-order from Direct Order path 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) 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({ 'product_id': widget.id, 'product_qty': 500, 'origin': so_am.name, }) try: mo_am.action_confirm() mo_am.button_mark_done() except Exception as e: print(f" amphenol ready mo: {e}") mo_am.write({'state': 'done'}) LOG(" 5 active orders across workflow stages") else: LOG(" Active demo orders already present — skipping") # ============================================================ # Phase 11: Exception case — Delinquent Industries # ============================================================ 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 env['sale.order'].create({ 'partner_id': delinquent.id, 'x_fc_invoice_strategy': 'net_terms', 'x_fc_po_number': 'FPDEMO-HOLD-TEST', 'order_line': [Command.create({ 'product_id': product.id, 'name': 'Test order (will raise account hold UserError)', 'product_uom_qty': 10, 'price_unit': 100.0, })], }) LOG(" Draft SO for Delinquent Industries — click Confirm to demo the hold") # ============================================================ # Phase 12: Bake window states (awaiting / missed / baked) # ============================================================ 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 env['fusion.plating.bake.window'].create({ 'bath_id': bath_en.id, 'part_ref': 'MG-WG-8801', 'lot_ref': 'LOT-BW-001', 'customer_ref': 'Magellan Aerospace', 'quantity': 45, 'window_hours': 4.0, 'plate_exit_time': datetime.now() - timedelta(hours=1), }) # Missed (for alert demo) bw_missed = env['fusion.plating.bake.window'].create({ 'bath_id': bath_en.id, 'part_ref': 'CY-STR-101', 'lot_ref': 'LOT-BW-MISSED', 'customer_ref': 'Cyclone Manufacturing', 'quantity': 80, 'window_hours': 4.0, 'plate_exit_time': datetime.now() - timedelta(hours=8), 'state': 'missed_window', }) # Completed historical bw_done = env['fusion.plating.bake.window'].create({ 'bath_id': bath_en.id, 'part_ref': 'VS-PQR8440', 'lot_ref': 'LOT-BW-DONE', 'customer_ref': 'Amphenol Canada', 'quantity': 500, 'window_hours': 4.0, 'plate_exit_time': datetime.now() - timedelta(days=14, hours=2), 'bake_start_time': datetime.now() - timedelta(days=14, hours=1), 'bake_end_time': datetime.now() - timedelta(days=13, hours=2), 'bake_temp': 375.0, 'bake_duration_hours': 23.0, 'state': 'baked', }) backdate(bw_done, 14) LOG(" 3 bake windows: 1 awaiting, 1 missed, 1 baked") else: LOG(f" Already has {bw_count} bake windows — skipping") # ============================================================ # Phase 12.5: Shop-floor stations + first-piece gates + variety # Feeds the Tablet Station + First-Piece Gates + bake Kanban demos. # ============================================================ LOG("Phase 12.5: Shop-floor stations + first-piece gates") Station = env['fusion.plating.shopfloor.station'] stn_count = Station.search_count([]) if stn_count < 5: station_defs = [ ('Plating Room Tablet 1', 'TAB-PL-01', 'tablet', 'Plating'), ('Bake Oven Tablet', 'TAB-BK-01', 'tablet', 'Bake Oven'), ('Inspection Kiosk', 'TAB-QA-01', 'kiosk', 'Inspection'), ('Shipping Desktop', 'TAB-SH-01', 'desktop', 'Shipping'), ('Receiving Mobile', 'TAB-RC-01', 'mobile', 'Receiving'), ] for sname, scode, stype, wc_name in station_defs: if Station.search_count([('code', '=', scode)]): continue fp_wc = env['fusion.plating.work.center'].search( [('name', '=', wc_name)], limit=1, ) if wc_name else False Station.create({ 'name': sname, 'code': scode, 'station_type': stype, 'facility_id': facility.id, 'work_center_id': fp_wc.id if fp_wc else False, 'last_ping': datetime.now() - timedelta(minutes=random.randint(0, 45)), }) LOG(f" 5 shop-floor stations created") else: LOG(f" Already has {stn_count} stations — skipping") # 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, 'part_ref': 'HW-TOR-5501', 'lot_ref': 'LOT-BW-HW-01', 'customer_ref': 'Honeywell Toronto', 'quantity': 120, 'window_hours': 4.0, 'plate_exit_time': datetime.now() - timedelta(hours=2, minutes=15), }) env['fusion.plating.bake.window'].create({ 'bath_id': bath_en.id, 'part_ref': 'AP-WGL-7200', 'lot_ref': 'LOT-BW-AP-01', 'customer_ref': 'Amphenol Canada', 'quantity': 300, 'window_hours': 4.0, 'plate_exit_time': datetime.now() - timedelta(hours=3, minutes=30), 'bake_start_time': datetime.now() - timedelta(minutes=40), 'state': 'bake_in_progress', }) env['fusion.plating.bake.window'].create({ 'bath_id': bath_en.id, 'part_ref': 'CY-STR-240', 'lot_ref': 'LOT-BW-CY-02', 'customer_ref': 'Cyclone Manufacturing', 'quantity': 60, 'window_hours': 4.0, 'plate_exit_time': datetime.now() - timedelta(minutes=45), }) LOG(" +3 additional bake windows (awaiting / in-progress)") # First-piece inspection gates — seed 4 variants Gate = env['fusion.plating.first.piece.gate'] if Gate.search_count([]) < 4: Gate.create({ 'bath_id': bath_en.id, 'part_ref': 'HW-TOR-5501', 'customer_ref': 'Honeywell Toronto', 'routing_first_run': True, 'first_piece_produced': datetime.now() - timedelta(minutes=35), 'result': 'pending', }) Gate.create({ 'bath_id': bath_en.id, 'part_ref': 'AP-WGL-7200', 'customer_ref': 'Amphenol Canada', 'routing_first_run': False, 'first_piece_produced': datetime.now() - timedelta(hours=2), 'first_piece_inspected': datetime.now() - timedelta(hours=1, minutes=40), 'inspector_id': env.user.id, 'result': 'pass', 'rest_of_lot_released': True, }) Gate.create({ 'bath_id': bath_en.id, 'part_ref': 'MG-WG-8801', 'customer_ref': 'Magellan Aerospace', 'routing_first_run': True, 'first_piece_produced': datetime.now() - timedelta(hours=4), 'first_piece_inspected': datetime.now() - timedelta(hours=3, minutes=30), 'inspector_id': env.user.id, 'result': 'pass', 'rest_of_lot_released': False, # passed but awaiting release 'notes': '

Thickness 1.95 mils — within tolerance. Lot released pending planner signoff.

', }) Gate.create({ 'bath_id': bath_en.id, 'part_ref': 'CY-STR-240', 'customer_ref': 'Cyclone Manufacturing', 'routing_first_run': True, 'first_piece_produced': datetime.now() - timedelta(hours=6), 'first_piece_inspected': datetime.now() - timedelta(hours=5, minutes=30), 'inspector_id': env.user.id, 'result': 'fail', 'notes': '

Thickness 0.8 mils — below spec (min 1.2). Rework required.

', }) LOG(" 4 first-piece gates: 1 pending / 1 passed+released / 1 passed-holding / 1 failed") else: LOG(f" Already has {Gate.search_count([])} first-piece gates — skipping") # 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( [('state', 'in', ('progress', 'confirmed'))], limit=3, ) hold_reasons = ['out_of_spec', 'damaged', 'contamination'] for i, mo in enumerate(active_mos[:2]): wo = mo.workorder_ids[:1] Hold.create({ 'part_ref': mo.product_id.default_code or mo.product_id.name, '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}.', '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, 'facility_id': facility.id, 'operator_id': env.user.id, 'state': 'on_hold' if i == 0 else 'under_review', }) LOG(f" {Hold.search_count([])} quality holds seeded") else: LOG(f" Already has {Hold.search_count([])} quality holds — skipping") # ============================================================ # Phase 13: Quote configurator + win/loss variety # ============================================================ LOG("Phase 13: Quote configurator sessions (won/lost/expired/draft)") quote_marker = env['fp.quote.configurator'].search_count([('name', 'like', 'FP-QC-%')]) if quote_marker < 5: import math def make_quote(partner, part_cat, coating, qty, state='draft', lost_reason=None, days_ago=0): q = env['fp.quote.configurator'].create({ 'partner_id': partner.id, 'part_catalog_id': part_cat.id if part_cat else False, 'coating_config_id': coating.id, 'quantity': qty, 'substrate_material': 'steel', 'surface_area': (part_cat.surface_area if part_cat else 10.0), 'surface_area_uom': 'sq_in', 'calculated_price': qty * 42.50, }) if state == 'lost': q.write({ 'lost_reason': lost_reason or 'price', 'lost_details': 'Competitor came in 18% lower on unit price.', 'state': 'lost', 'lost_date': (datetime.now() - timedelta(days=days_ago)).date(), }) elif state == 'expired': q.write({'state': 'expired', 'lost_date': (datetime.now() - timedelta(days=days_ago)).date()}) if days_ago: backdate(q, days_ago) return q make_quote(magellan, created_parts.get('MG-BRK-112'), coating_en_mid, 120, 'draft') make_quote(cyclone, created_parts.get('CY-PIN-880'), coating_en_mid, 500, 'draft') make_quote(honeywell, created_parts.get('HW-GRB-22'), coating_en_low, 80, 'lost', 'price', 22) make_quote(westin, created_parts.get('WM-PLT-308'), coating_en_low, 40, 'lost', 'lead_time', 35) make_quote(amphenol, created_parts.get('VS-HSA201-B'), coating_en_mid, 200, 'expired', None, 55) make_quote(magellan, created_parts.get('MG-CYL-550'), coating_chrome, 25, 'lost', 'competitor', 18) LOG(" 6 quote configurator sessions (2 draft, 3 lost, 1 expired)") # ============================================================ # Phase 14: Operator certifications # ============================================================ LOG("Phase 14: Operator certifications") employee = env['hr.employee'].search([], limit=1) if not employee: employee = env['hr.employee'].create({'name': 'Kris Pathinather'}) for proc_type in (en_midphos, en_lowphos, passivation): existing = env['fp.operator.certification'].search([ ('employee_id', '=', employee.id), ('process_type_id', '=', proc_type.id), ], limit=1) if not existing: env['fp.operator.certification'].create({ 'employee_id': employee.id, 'process_type_id': proc_type.id, 'issued_date': (datetime.now() - timedelta(days=120)).date(), 'expires_date': (datetime.now() + timedelta(days=245)).date(), }) # Operator who has only 1 cert (for the "gap" demo) try: emp2 = env['hr.employee'].search([], offset=1, limit=1) if emp2 and emp2 != employee: existing = env['fp.operator.certification'].search([ ('employee_id', '=', emp2.id), ('process_type_id', '=', en_midphos.id), ], limit=1) if not existing: env['fp.operator.certification'].create({ 'employee_id': emp2.id, 'process_type_id': en_midphos.id, 'issued_date': (datetime.now() - timedelta(days=60)).date(), 'expires_date': (datetime.now() + timedelta(days=305)).date(), }) except Exception: pass LOG(f" Certifications seeded for {employee.name}") # ============================================================ # Phase 14.5: Work centres + recipe wiring + WO backfill # ============================================================ LOG("Phase 14.5: Work centres + recipe wiring") # Create MRP work centres matching the ENP-ALUM-BASIC recipe operations # (names must match the node names for the auto-mapping to find them) WC_DEFS = [ ('Ready for processing', 'FP-QUEUE', 25.0), ('Racking', 'FP-RACK', 35.0), ('Masking', 'FP-MASK', 35.0), ('E-Nickel Plating', 'FP-EN', 55.0), ('Oven baking', 'FP-BAKE', 30.0), ('Post-plate Inspection', 'FP-INSP', 45.0), ('De-racking', 'FP-DERACK', 30.0), ('De-Masking', 'FP-DEMASK', 30.0), ('Oven bake (Post de-rack)', 'FP-POSTBAKE', 30.0), ] # Ensure one FP facility for mapping if not facility: facility = env['fusion.plating.facility'].search([], limit=1) # Create MRP work centres first mrp_wc_by_code = {} for name, code, rate in WC_DEFS: mrp_wc = env['mrp.workcenter'].search([('code', '=', code)], limit=1) if not mrp_wc: mrp_wc = env['mrp.workcenter'].create({ 'name': name, 'code': code, 'costs_hour': rate, 'time_efficiency': 100.0, }) mrp_wc_by_code[code] = mrp_wc # Create matching FP work centres, linked to their MRP counterparts fp_wc_by_name = {} for name, code, _rate in WC_DEFS: fp_wc = env['fusion.plating.work.center'].search([('code', '=', code)], limit=1) if not fp_wc: fp_wc = env['fusion.plating.work.center'].create({ 'name': name, 'code': code, 'facility_id': facility.id, 'x_fc_mrp_workcenter_id': mrp_wc_by_code[code].id, }) else: fp_wc.x_fc_mrp_workcenter_id = mrp_wc_by_code[code].id fp_wc_by_name[name] = fp_wc LOG(f" {len(WC_DEFS)} MRP + FP work centres (paired)") # Wire recipe operation nodes to FP work centres by matching names recipe_main = env['fusion.plating.process.node'].search( [('node_type', '=', 'recipe')], limit=1, ) wired = 0 for op in env['fusion.plating.process.node'].search([('node_type', '=', 'operation')]): if op.work_center_id: continue wc = fp_wc_by_name.get(op.name) if wc: op.work_center_id = wc.id wired += 1 LOG(f" Wired {wired} recipe operations to work centres (recipe: {recipe_main.name if recipe_main else 'none'})") # Link every coating config to the main recipe if recipe_main: for cfg in env['fp.coating.config'].search([('recipe_id', '=', False)]): cfg.recipe_id = recipe_main.id LOG(f" Linked coating configs → recipe") # ============================================================ # Phase 14.6: Backfill work orders on existing demo MOs # ============================================================ LOG("Phase 14.6: Backfill work orders on existing MOs") def _populate_workorders(mo): """Assign recipe from SO coating config + generate WOs.""" if not mo.x_fc_recipe_id and mo.origin: so = env['sale.order'].search([('name', '=', mo.origin)], limit=1) if so and so.x_fc_coating_config_id and so.x_fc_coating_config_id.recipe_id: mo.x_fc_recipe_id = so.x_fc_coating_config_id.recipe_id.id if mo.x_fc_recipe_id and not mo.workorder_ids: mo._generate_workorders_from_recipe() wo_created = 0 for mo in env['mrp.production'].search([('state', 'in', ['confirmed', 'progress', 'done'])]): before = len(mo.workorder_ids) _populate_workorders(mo) wo_created += len(mo.workorder_ids) - before LOG(f" Generated {wo_created} work orders across existing MOs") # Drive work orders into a mix of states for the demo: # - For done MOs: mark all WOs done with backdated durations # - For one active MO (Cyclone): first WO done, second in progress, rest ready # - Others: leave in pending import random as _r for mo in env['mrp.production'].search([('state', '=', 'done')]): for wo in mo.workorder_ids: if wo.state != 'done': wo.write({ 'state': 'done', 'duration': _r.uniform(25, 90), 'duration_expected': 45.0, }) cyc_mo = env['mrp.production'].search( [('origin', 'in', env['sale.order'].search( [('x_fc_po_number', '=', 'FPDEMO-ACTIVE-CYCLONE')] ).mapped('name'))], limit=1, ) if cyc_mo and cyc_mo.workorder_ids: wos = cyc_mo.workorder_ids.sorted('sequence') for i, wo in enumerate(wos): if i == 0: wo.write({'state': 'done', 'duration': 42.0}) elif i == 1: wo.write({'state': 'progress', 'duration': 18.0, 'x_fc_priority': '1'}) elif i == 2: wo.write({'state': 'ready'}) else: wo.write({'state': 'ready'}) LOG(f" Cyclone MO {cyc_mo.name}: WO progression set (done / in progress / ready / pending)") # Give all work orders reasonable expected durations if blank env.cr.execute( "UPDATE mrp_workorder SET duration_expected = 45 WHERE duration_expected IS NULL OR duration_expected = 0" ) final_wo_count = env['mrp.workorder'].search_count([]) LOG(f" TOTAL work orders now in DB: {final_wo_count}") # ============================================================ # Phase 15: Final commit # ============================================================ env.cr.commit() LOG("=" * 60) LOG("DEMO SEED COMPLETE") LOG("=" * 60) LOG(f" Customers: 6") LOG(f" Parts: {env['fp.part.catalog'].search_count([])}") LOG(f" Price lists: {env['fp.customer.price.list'].search_count([])}") LOG(f" Quotes: {env['fp.quote.configurator'].search_count([])}") LOG(f" Sale orders: {env['sale.order'].search_count([('partner_id', 'in', [amphenol.id, magellan.id, cyclone.id, honeywell.id, westin.id, delinquent.id])])}") LOG(f" MOs: {env['mrp.production'].search_count([])}") LOG(f" Deliveries: {env['fusion.plating.delivery'].search_count([])}") LOG(f" Certificates: {env['fp.certificate'].search_count([])}") LOG(f" Invoices: {env['account.move'].search_count([('move_type', '=', 'out_invoice')])}") LOG(f" Payments: {env['account.payment'].search_count([('payment_type', '=', 'inbound')])}") LOG(f" Bath logs: {env['fusion.plating.bath.log'].search_count([])}") LOG(f" Replenishments: {env['fusion.plating.bath.replenishment.suggestion'].search_count([])}") LOG(f" Bake windows: {env['fusion.plating.bake.window'].search_count([])}") LOG(f" Stations: {env['fusion.plating.shopfloor.station'].search_count([])}") LOG(f" First-piece: {env['fusion.plating.first.piece.gate'].search_count([])}") LOG(f" Quality holds: {env['fusion.plating.quality.hold'].search_count([])}") LOG(f" Racks: {env['fusion.plating.rack'].search_count([])}") LOG(f" Operator certs: {env['fp.operator.certification'].search_count([])}") LOG("") LOG("Key demo stops:") LOG(" 1. Amphenol Canada → 8+ closed jobs, paid invoices, issued certs w/ SPC data") LOG(" 2. Magellan → active progress-billing order, 40% invoice posted") LOG(" 3. Cyclone → active deposit order, 50% paid, MO in progress") LOG(" 4. Honeywell → just-confirmed net-terms SO, receiving pending") LOG(" 5. Westin → direct-order path (COD)") LOG(" 6. Delinquent Industries → click Confirm to demo account hold block") LOG(" 7. Bake windows → 1 awaiting / 1 missed / 1 baked") LOG(" 8. EN-BATH-A → pending replenishment suggestions (nickel low)") LOG(" 9. RACK-004 → shows Needs Strip (MTO 3.2 / 3.0)") LOG("")