feat(workorders): zone-driven slots UI and two-zone demo seed
This commit is contained in:
@@ -24,15 +24,14 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="requested_datetime"/>
|
||||
<field name="scheduled_datetime"/>
|
||||
<field name="scheduled_end"/>
|
||||
<field name="technician_id"/>
|
||||
<field name="slot_day"/>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="zone_id" readonly="1"/>
|
||||
<field name="slot_day"/>
|
||||
<field name="scheduled_datetime"/>
|
||||
<field name="scheduled_end" readonly="1"/>
|
||||
<field name="assigned_user_id" readonly="1"/>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# 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
|
||||
@@ -23,16 +24,42 @@ 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],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -120,29 +147,70 @@ def ensure_materials():
|
||||
return created
|
||||
|
||||
|
||||
def ensure_technician(work_types):
|
||||
technician_model = env["repair.technician"].sudo()
|
||||
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
|
||||
|
||||
technician = technician_model.search([("state", "=", "active")], limit=1, order="id")
|
||||
if technician:
|
||||
missing_types = work_types - technician.work_type_ids
|
||||
if missing_types:
|
||||
technician.write({"work_type_ids": [(4, wt.id) for wt in missing_types]})
|
||||
return technician
|
||||
|
||||
zones = zone_model.search([("state", "=", "active")], limit=3)
|
||||
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": "Alex Pro Appliance",
|
||||
"name": name,
|
||||
"state": "active",
|
||||
"active": True,
|
||||
"work_type_ids": [(6, 0, work_types.ids)],
|
||||
"zone_ids": [(6, 0, zones.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])],
|
||||
}
|
||||
)
|
||||
|
||||
schedule_model = env["repair.technician.schedule"].sudo()
|
||||
if not technician.schedule_ids:
|
||||
for day in ["0", "1", "2", "3", "4", "5"]:
|
||||
schedule_model.create(
|
||||
{
|
||||
@@ -152,7 +220,31 @@ def ensure_technician(work_types):
|
||||
"hour_to": 17.0,
|
||||
}
|
||||
)
|
||||
return technician
|
||||
|
||||
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):
|
||||
@@ -225,6 +317,17 @@ def ensure_work_order(contact, address, work_type, technician, materials, index)
|
||||
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)
|
||||
@@ -245,6 +348,8 @@ def ensure_work_order(contact, address, work_type, technician, materials, index)
|
||||
"state": "assigned" if technician else "confirmed",
|
||||
}
|
||||
)
|
||||
order._compute_zone_id()
|
||||
order._refresh_available_slots_saved()
|
||||
|
||||
time_model.create(
|
||||
{
|
||||
@@ -281,8 +386,10 @@ 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()
|
||||
technician = ensure_technician(env["repair.work.type"].sudo().browse([wt.id for wt in work_types]))
|
||||
|
||||
created_contacts = 0
|
||||
created_addresses = 0
|
||||
@@ -290,10 +397,24 @@ def run():
|
||||
|
||||
for idx, payload in enumerate(CONTACTS):
|
||||
location = LOCATIONS[idx % len(LOCATIONS)]
|
||||
contact_before = env["dsrpt.contact"].sudo().search_count([("name", "=", payload["name"])])
|
||||
address_before = env["dsrpt.contact.address"].sudo().search_count(
|
||||
[("description", "=", location["description"])]
|
||||
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:
|
||||
@@ -313,6 +434,7 @@ def run():
|
||||
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)}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user