feat(workorders): move address to contact and auto-compute zone

This commit is contained in:
Ruslan Bakiev
2026-02-13 18:07:08 +07:00
parent 111afdd885
commit 2b7a9457dd
9 changed files with 71 additions and 54 deletions

View File

@@ -2,6 +2,7 @@
from . import dsrpt_communication_type
from . import dsrpt_contact
from . import dsrpt_contact_address
from . import dsrpt_contact_communication
from . import contact_event
from . import contact_source

View File

@@ -38,6 +38,11 @@ class Contact(models.Model):
'contact_id',
string='Events'
)
address_ids = fields.One2many(
'dsrpt.contact.address',
'contact_id',
string='Addresses'
)
# call_ids moved to dsrpt_calls module to avoid circular dependencies
# Computed fields

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class ContactAddress(models.Model):
_name = "dsrpt.contact.address"
_description = "Contact Address"
_order = "id desc"
_rec_name = "description"
contact_id = fields.Many2one("dsrpt.contact", required=True, ondelete="cascade", index=True)
description = fields.Char(required=True)
latitude = fields.Float(digits=(10, 6))
longitude = fields.Float(digits=(10, 6))

View File

@@ -5,6 +5,8 @@ access_dsrpt_communication_type_user,dsrpt.communication.type.user,model_dsrpt_c
access_dsrpt_communication_type_admin,dsrpt.communication.type.admin,model_dsrpt_communication_type,group_dsrpt_address_book_admin,1,1,1,1
access_dsrpt_contact_communication_user,dsrpt.contact.communication.user,model_dsrpt_contact_communication,group_dsrpt_address_book_user,1,1,1,1
access_dsrpt_contact_communication_admin,dsrpt.contact.communication.admin,model_dsrpt_contact_communication,group_dsrpt_address_book_admin,1,1,1,1
access_dsrpt_contact_address_user,dsrpt.contact.address.user,model_dsrpt_contact_address,group_dsrpt_address_book_user,1,1,1,1
access_dsrpt_contact_address_admin,dsrpt.contact.address.admin,model_dsrpt_contact_address,group_dsrpt_address_book_admin,1,1,1,1
access_contact_event_user,contact.event.user,model_contact_event,group_dsrpt_address_book_user,1,1,1,0
access_contact_event_admin,contact.event.admin,model_contact_event,group_dsrpt_address_book_admin,1,1,1,1
access_contact_source_user,contact.source.user,model_contact_source,group_dsrpt_address_book_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
5 access_dsrpt_communication_type_admin dsrpt.communication.type.admin model_dsrpt_communication_type group_dsrpt_address_book_admin 1 1 1 1
6 access_dsrpt_contact_communication_user dsrpt.contact.communication.user model_dsrpt_contact_communication group_dsrpt_address_book_user 1 1 1 1
7 access_dsrpt_contact_communication_admin dsrpt.contact.communication.admin model_dsrpt_contact_communication group_dsrpt_address_book_admin 1 1 1 1
8 access_dsrpt_contact_address_user dsrpt.contact.address.user model_dsrpt_contact_address group_dsrpt_address_book_user 1 1 1 1
9 access_dsrpt_contact_address_admin dsrpt.contact.address.admin model_dsrpt_contact_address group_dsrpt_address_book_admin 1 1 1 1
10 access_contact_event_user contact.event.user model_contact_event group_dsrpt_address_book_user 1 1 1 0
11 access_contact_event_admin contact.event.admin model_contact_event group_dsrpt_address_book_admin 1 1 1 1
12 access_contact_source_user contact.source.user model_contact_source group_dsrpt_address_book_user 1 1 1 0

View File

@@ -96,7 +96,18 @@
<group name="requests_placeholder"/>
</group>
<!-- Third row: Communications (full width) -->
<!-- Third row: Addresses (full width) -->
<group col="1" string="Addresses">
<field name="address_ids" nolabel="1" context="{'default_contact_id': id}">
<list editable="bottom">
<field name="description"/>
<field name="latitude"/>
<field name="longitude"/>
</list>
</field>
</group>
<!-- Fourth row: Communications (full width) -->
<group col="1" string="Communications">
<field name="communication_ids" nolabel="1" context="{'default_contact_id': id}">
<list editable="bottom">
@@ -107,7 +118,7 @@
</field>
</group>
<!-- Fourth row: Calls (full width) - will be added by dsrpt_calls module -->
<!-- Fifth row: Calls (full width) - will be added by dsrpt_calls module -->
<group col="1" name="calls_section"/>
</sheet>
<chatter/>

View File

@@ -10,10 +10,20 @@ class RepairWorkOrder(models.Model):
name = fields.Char(default="New", copy=False, readonly=True, tracking=True)
contact_id = fields.Many2one("dsrpt.contact", required=True, tracking=True)
service_address = fields.Char(tracking=True)
service_latitude = fields.Float(digits=(10, 6), tracking=True)
service_longitude = fields.Float(digits=(10, 6), tracking=True)
zone_id = fields.Many2one("repair.fsm.zone", string="FSM Zone", tracking=True)
contact_address_id = fields.Many2one(
"dsrpt.contact.address",
string="Address",
tracking=True,
domain="[('contact_id', '=', contact_id)]",
)
zone_id = fields.Many2one(
"repair.fsm.zone",
string="FSM Zone",
compute="_compute_zone_id",
store=True,
readonly=True,
tracking=True,
)
description = fields.Text(tracking=True)
requested_datetime = fields.Datetime(default=fields.Datetime.now, tracking=True)
scheduled_datetime = fields.Datetime(tracking=True)
@@ -42,15 +52,7 @@ class RepairWorkOrder(models.Model):
for vals in vals_list:
if vals.get("name", "New") == "New":
vals["name"] = self.env["ir.sequence"].next_by_code("repair.work.order") or "New"
records = super().create(vals_list)
for record, vals in zip(records, vals_list):
if vals.get("zone_id"):
continue
if record._has_service_point():
zone = record._find_zone_for_point(record.service_latitude, record.service_longitude)
if zone and record.zone_id != zone:
record.zone_id = zone.id
return records
return super().create(vals_list)
def _group_expand_states(self, states, domain, order):
return [key for key, _label in self._fields["state"].selection]
@@ -61,44 +63,30 @@ class RepairWorkOrder(models.Model):
rec.total_time_hours = sum(rec.time_line_ids.mapped("hours"))
rec.total_material_cost = sum(rec.material_line_ids.mapped("subtotal"))
def _has_service_point(self):
self.ensure_one()
return self.service_latitude is not False and self.service_longitude is not False
def _find_zone_for_point(self, latitude, longitude):
zones = self.env["repair.fsm.zone"].search([("state", "=", "active")])
if latitude is False or longitude is False or latitude is None or longitude is None:
return self.env["repair.fsm.zone"]
zone_domain = [("state", "=", "active")] if "state" in self.env["repair.fsm.zone"]._fields else []
zones = self.env["repair.fsm.zone"].search(zone_domain)
for zone in zones:
if zone.contains_point(latitude, longitude):
return zone
return self.env["repair.fsm.zone"]
@api.onchange("service_latitude", "service_longitude")
def _onchange_service_coordinates(self):
@api.depends("contact_address_id.latitude", "contact_address_id.longitude")
def _compute_zone_id(self):
for rec in self:
if rec._has_service_point():
zone = rec._find_zone_for_point(rec.service_latitude, rec.service_longitude)
if zone:
rec.zone_id = zone
if not rec.contact_address_id:
rec.zone_id = False
continue
zone = rec._find_zone_for_point(rec.contact_address_id.latitude, rec.contact_address_id.longitude)
rec.zone_id = zone.id if zone else False
def action_detect_zone(self):
@api.onchange("contact_id")
def _onchange_contact_id(self):
for rec in self:
if not rec._has_service_point():
raise ValidationError("Service coordinates are required to detect FSM Zone.")
zone = rec._find_zone_for_point(rec.service_latitude, rec.service_longitude)
if not zone:
raise ValidationError("No active FSM Zone contains this point.")
rec.zone_id = zone.id
return True
def write(self, vals):
result = super().write(vals)
if ("service_latitude" in vals or "service_longitude" in vals) and "zone_id" not in vals:
for rec in self:
if rec._has_service_point():
zone = rec._find_zone_for_point(rec.service_latitude, rec.service_longitude)
if zone and rec.zone_id != zone:
super(RepairWorkOrder, rec).write({"zone_id": zone.id})
return result
if rec.contact_address_id and rec.contact_address_id.contact_id != rec.contact_id:
rec.contact_address_id = False
def action_confirm(self):
self.write({"state": "confirmed"})

View File

@@ -8,7 +8,6 @@
<header>
<button name="action_confirm" type="object" string="Confirm" class="btn-primary" invisible="state != 'draft'"/>
<button name="action_assign_to_me" type="object" string="Assign to me" class="btn-primary" invisible="state not in ('confirmed','assigned')"/>
<button name="action_detect_zone" type="object" string="Detect Zone" invisible="service_latitude == False or service_longitude == False"/>
<button name="action_start" type="object" string="Start" invisible="state != 'assigned'"/>
<button name="action_done" type="object" string="Done" class="btn-primary" invisible="state not in ('in_progress','assigned')"/>
<button name="action_cancel" type="object" string="Cancel" invisible="state in ('done','cancelled')"/>
@@ -20,10 +19,8 @@
<group>
<field name="name" readonly="1"/>
<field name="contact_id"/>
<field name="service_address"/>
<field name="service_latitude"/>
<field name="service_longitude"/>
<field name="zone_id"/>
<field name="contact_address_id" domain="[('contact_id', '=', contact_id)]" options="{'no_create': True, 'no_create_edit': True}"/>
<field name="zone_id" readonly="1"/>
</group>
<group>
<field name="requested_datetime"/>

View File

@@ -7,7 +7,7 @@
<kanban default_group_by="state">
<field name="name"/>
<field name="contact_id"/>
<field name="service_address"/>
<field name="contact_address_id"/>
<field name="scheduled_datetime"/>
<field name="technician_id"/>
<field name="state"/>
@@ -21,7 +21,7 @@
<field name="contact_id"/>
</div>
<div>
<field name="service_address"/>
<field name="contact_address_id"/>
</div>
<div>
<field name="scheduled_datetime"/>

View File

@@ -7,15 +7,13 @@
<list>
<field name="name"/>
<field name="contact_id" optional="show"/>
<field name="service_address" optional="show"/>
<field name="contact_address_id" optional="show"/>
<field name="zone_id" optional="show"/>
<field name="scheduled_datetime" optional="show"/>
<field name="technician_id" optional="show"/>
<field name="state" widget="badge" optional="show"/>
<field name="total_time_hours" optional="show"/>
<field name="total_material_cost" optional="show"/>
<field name="service_latitude" optional="hide"/>
<field name="service_longitude" optional="hide"/>
<field name="assigned_user_id" optional="hide"/>
<field name="requested_datetime" optional="hide"/>
</list>