442 lines
14 KiB
Python
442 lines
14 KiB
Python
# Run with:
|
|
# odoo shell -c /etc/odoo/odoo.conf -d <db_name> < 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()
|