From 2db4beb5aeae47f148f7d5bd0a219767c76d89b3 Mon Sep 17 00:00:00 2001
From: Ruslan Bakiev <572431+veikab@users.noreply.github.com>
Date: Fri, 13 Feb 2026 19:12:12 +0700
Subject: [PATCH] feat(workorders): zone-driven slots UI and two-zone demo seed
---
.../views/repair_work_order_view_form.xml | 9 +-
odoo/scripts/generate_pro_appliance_demo.py | 188 +++++++++++++++---
2 files changed, 159 insertions(+), 38 deletions(-)
diff --git a/odoo/addons/dsrpt_repair_work_orders/views/repair_work_order_view_form.xml b/odoo/addons/dsrpt_repair_work_orders/views/repair_work_order_view_form.xml
index c38dec5..2657c9d 100644
--- a/odoo/addons/dsrpt_repair_work_orders/views/repair_work_order_view_form.xml
+++ b/odoo/addons/dsrpt_repair_work_orders/views/repair_work_order_view_form.xml
@@ -24,15 +24,14 @@
-
-
-
+
+
-
+
+
-
diff --git a/odoo/scripts/generate_pro_appliance_demo.py b/odoo/scripts/generate_pro_appliance_demo.py
index de55810..9829ed9 100644
--- a/odoo/scripts/generate_pro_appliance_demo.py
+++ b/odoo/scripts/generate_pro_appliance_demo.py
@@ -1,6 +1,7 @@
# 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
@@ -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,39 +147,104 @@ 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()
-
- 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)
- technician = technician_model.create(
- {
- "name": "Alex Pro Appliance",
- "state": "active",
- "active": True,
- "work_type_ids": [(6, 0, work_types.ids)],
- "zone_ids": [(6, 0, zones.ids)],
- }
- )
-
- schedule_model = env["repair.technician.schedule"].sudo()
- for day in ["0", "1", "2", "3", "4", "5"]:
- schedule_model.create(
+ zones = zone_model.browse()
+ for payload in DEMO_ZONES:
+ polygon_geojson = json.dumps(
{
- "technician_id": technician.id,
- "day_of_week": day,
- "hour_from": 9.0,
- "hour_to": 17.0,
+ "type": "Polygon",
+ "coordinates": [payload["polygon_points"]],
}
)
- return technician
+ 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):
@@ -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,11 +397,25 @@ 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:
created_contacts += 1
@@ -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)}")