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)}")