# Run with: # odoo shell -c /etc/odoo/odoo.conf -d < odoo/scripts/generate_pro_appliance_demo.py import json from datetime import timedelta from odoo import fields SEED_TAG = "[SEED:pro-appliance.repair]" SOURCE_URL = "https://pro-appliance.repair/" CONTACTS = [ {"name": "Christina Gomez", "phone": "+1-302-503-0047"}, {"name": "Clinton Parker", "phone": "+1-302-503-0048"}, {"name": "Mike Kreller", "phone": "+1-302-503-0049"}, {"name": "Frank Smith", "phone": "+1-302-503-0050"}, {"name": "Mandy Cekine", "phone": "+1-302-503-0051"}, ] LOCATIONS = [ { "description": "Newark Downtown, DE 19713", "latitude": 39.683723, "longitude": -75.749657, "zone_name": "Newark West Demo Zone", }, { "description": "Christiana Area, Newark, DE", "latitude": 39.678002, "longitude": -75.659408, "zone_name": "Newark East Demo Zone", }, { "description": "Bear Area, New Castle County, DE", "latitude": 39.629276, "longitude": -75.658262, "zone_name": "Newark East Demo Zone", }, ] DEMO_ZONES = [ { "name": "Newark West Demo Zone", "polygon_points": [ [-75.800000, 39.640000], [-75.690000, 39.640000], [-75.690000, 39.740000], [-75.800000, 39.740000], [-75.800000, 39.640000], ], }, { "name": "Newark East Demo Zone", "polygon_points": [ [-75.700000, 39.600000], [-75.600000, 39.600000], [-75.600000, 39.710000], [-75.700000, 39.710000], [-75.700000, 39.600000], ], }, ] WORK_TYPES = [ ("Clothes Dryer", 90), ("Dishwasher", 120), ("Range", 120), ("Refrigerator", 120), ("Microwave", 90), ] FALLBACK_MATERIALS = [ ("Dryer Belt", "DRY-BELT", 35.0), ("Dishwasher Drain Pump", "DW-PUMP", 72.0), ("Range Igniter", "RANGE-IGN", 48.0), ("Refrigerator Thermistor", "FRIDGE-THRM", 29.0), ("Microwave Door Switch", "MW-SWITCH", 17.0), ] def ensure_source(): source_model = env["contact.source"].sudo() source = source_model.search([("name", "=", "pro-appliance.repair Website")], limit=1) if not source: source = source_model.create( { "name": "pro-appliance.repair Website", "description": f"Generated from {SOURCE_URL} {SEED_TAG}", "state": "active", "active": True, } ) return source def ensure_phone_type(): phone_type = env["dsrpt.communication.type"].sudo().search([("code", "=", "phone")], limit=1) return phone_type def ensure_work_types(): model = env["repair.work.type"].sudo() result = [] for name, duration in WORK_TYPES: rec = model.search([("name", "=", name)], limit=1) if not rec: rec = model.create({"name": name, "duration_min": duration, "active": True}) else: values = {} if not rec.duration_min: values["duration_min"] = duration if not rec.active: values["active"] = True if values: rec.write(values) result.append(rec) return result def ensure_materials(): material_model = env["repair.material"].sudo() materials = material_model.search([("state", "=", "active")], order="id", limit=20) if materials: return materials existing_any = material_model.search([], order="id", limit=20) if existing_any: for rec in existing_any.filtered(lambda m: m.state != "active"): rec.action_set_active() return material_model.search([("state", "=", "active")], order="id", limit=20) created = material_model.browse() for name, code, cost in FALLBACK_MATERIALS: rec = material_model.create( { "name": name, "default_code": code, "uom_name": "pcs", "standard_cost": cost, "state": "active", "active": True, } ) created |= rec return created def ensure_demo_zones(): zone_model = env["repair.fsm.zone"].sudo() zones = zone_model.browse() for payload in DEMO_ZONES: polygon_geojson = json.dumps( { "type": "Polygon", "coordinates": [payload["polygon_points"]], } ) zone = zone_model.search([("name", "=", payload["name"])], limit=1) if not zone: zone = zone_model.create( { "name": payload["name"], "polygon_geojson": polygon_geojson, "state": "active", } ) else: values = {} if zone.polygon_geojson != polygon_geojson: values["polygon_geojson"] = polygon_geojson if zone.state != "active": values["state"] = "active" if values: zone.write(values) zones |= zone return zones def ensure_technicians(work_types, zones): technician_model = env["repair.technician"].sudo() schedule_model = env["repair.technician.schedule"].sudo() # Archive legacy seed technician from older script versions to keep zone split clear. legacy = technician_model.search([("name", "=", "Alex Pro Appliance"), ("state", "=", "active")]) if legacy: legacy.write({"state": "archived", "active": False}) technicians_by_zone = {} for idx, zone in enumerate(zones.sorted("name")): name = f"Demo Tech {idx + 1} ({zone.name})" technician = technician_model.search([("name", "=", name)], limit=1) if not technician: technician = technician_model.create( { "name": name, "state": "active", "active": True, "work_type_ids": [(6, 0, work_types.ids)], "zone_ids": [(6, 0, [zone.id])], } ) else: technician.write( { "state": "active", "active": True, "work_type_ids": [(6, 0, work_types.ids)], "zone_ids": [(6, 0, [zone.id])], } ) if not technician.schedule_ids: for day in ["0", "1", "2", "3", "4", "5"]: schedule_model.create( { "technician_id": technician.id, "day_of_week": day, "hour_from": 9.0, "hour_to": 17.0, } ) technicians_by_zone[zone.name] = technician return technicians_by_zone def resolve_zone_for_location(location, zones): preferred_name = location.get("zone_name") if preferred_name: preferred = zones.filtered(lambda z: z.name == preferred_name)[:1] if preferred: return preferred for zone in zones: if zone.contains_point(location["latitude"], location["longitude"]): return zone return env["repair.fsm.zone"] def ensure_location_is_in_zone(location, zone): if not zone or not zone.id: raise ValueError(f"No zone found for location {location['description']}") if not zone.contains_point(location["latitude"], location["longitude"]): raise ValueError( f"Location {location['description']} ({location['latitude']},{location['longitude']}) is outside zone {zone.name}" ) def ensure_contact_and_address(payload, location, source, phone_type): contact_model = env["dsrpt.contact"].sudo() address_model = env["dsrpt.contact.address"].sudo() comm_model = env["dsrpt.contact.communication"].sudo() contact = contact_model.search([("name", "=", payload["name"])], limit=1) if not contact: contact = contact_model.create( { "name": payload["name"], "status": "has_request", "source_id": source.id, "note": f"Imported from {SOURCE_URL} {SEED_TAG}", } ) address = address_model.search( [ ("contact_id", "=", contact.id), ("description", "=", location["description"]), ], limit=1, ) if not address: address = address_model.create( { "contact_id": contact.id, "description": location["description"], "latitude": location["latitude"], "longitude": location["longitude"], } ) if phone_type: existing_phone = comm_model.search( [ ("contact_id", "=", contact.id), ("communication_type_id", "=", phone_type.id), ("value", "=", payload["phone"]), ], limit=1, ) if not existing_phone: comm_model.create( { "contact_id": contact.id, "communication_type_id": phone_type.id, "value": payload["phone"], "is_preferred": True if not contact.communication_ids else False, } ) return contact, address def ensure_work_order(contact, address, work_type, technician, materials, index): order_model = env["repair.work.order"].sudo() time_model = env["repair.work.order.time"].sudo() mat_line_model = env["repair.work.order.material"].sudo() desc = f"{SEED_TAG} {work_type.name} service request from {SOURCE_URL}" existing = order_model.search( [ ("contact_id", "=", contact.id), ("work_type_id", "=", work_type.id), ("description", "=", desc), ], limit=1, ) if existing: update_vals = {} if existing.contact_address_id != address: update_vals["contact_address_id"] = address.id if technician and existing.technician_id != technician: update_vals["technician_id"] = technician.id if existing.state in ("draft", "confirmed"): update_vals["state"] = "assigned" if update_vals: existing.write(update_vals) existing._compute_zone_id() existing._refresh_available_slots_saved() return existing, False start_dt = fields.Datetime.now() + timedelta(days=index + 1, hours=index) duration_min = max(work_type.duration_min or 90, 30) end_dt = start_dt + timedelta(minutes=duration_min) order = order_model.create( { "contact_id": contact.id, "contact_address_id": address.id, "work_type_id": work_type.id, "description": desc, "requested_datetime": fields.Datetime.now(), "scheduled_datetime": start_dt, "scheduled_end": end_dt, "slot_day": fields.Date.to_date(start_dt), "technician_id": technician.id if technician else False, "state": "assigned" if technician else "confirmed", } ) order._compute_zone_id() order._refresh_available_slots_saved() time_model.create( { "work_order_id": order.id, "technician_id": technician.id if technician else False, "description": f"{work_type.name} diagnostics and repair", "hours": round(duration_min / 60.0, 2), } ) if materials: mat1 = materials[index % len(materials)] mat_line_model.create( { "work_order_id": order.id, "material_id": mat1.id, "qty": 1.0 + (index % 2), } ) if len(materials) > 1: mat2 = materials[(index + 1) % len(materials)] mat_line_model.create( { "work_order_id": order.id, "material_id": mat2.id, "qty": 1.0, } ) return order, True def run(): source = ensure_source() phone_type = ensure_phone_type() work_types = ensure_work_types() work_type_recs = env["repair.work.type"].sudo().browse([wt.id for wt in work_types]) zones = ensure_demo_zones() technicians_by_zone = ensure_technicians(work_type_recs, zones) materials = ensure_materials() created_contacts = 0 created_addresses = 0 created_orders = 0 for idx, payload in enumerate(CONTACTS): location = LOCATIONS[idx % len(LOCATIONS)] contact_model = env["dsrpt.contact"].sudo() address_model = env["dsrpt.contact.address"].sudo() existing_contact = contact_model.search([("name", "=", payload["name"])], limit=1) contact_before = 1 if existing_contact else 0 address_before = ( address_model.search_count( [ ("contact_id", "=", existing_contact.id), ("description", "=", location["description"]), ] ) if existing_contact else 0 ) target_zone = resolve_zone_for_location(location, zones) ensure_location_is_in_zone(location, target_zone) technician = technicians_by_zone.get(target_zone.name) contact, address = ensure_contact_and_address(payload, location, source, phone_type) if contact_before == 0: created_contacts += 1 if address_before == 0: created_addresses += 1 work_type = work_types[idx % len(work_types)] _order, was_created = ensure_work_order(contact, address, work_type, technician, materials, idx) if was_created: created_orders += 1 env.cr.commit() print("=== Pro Appliance demo seed completed ===") print(f"Source: {SOURCE_URL}") print(f"Contacts created: {created_contacts}") print(f"Addresses created: {created_addresses}") print(f"Work orders created: {created_orders}") print(f"Demo zones active: {len(zones)}") print(f"Materials available for linking: {len(materials)}") run()