This commit is contained in:
gsinghpal
2026-02-23 00:32:20 -05:00
parent d6bac8e623
commit e8e554de95
549 changed files with 1330 additions and 124935 deletions

View File

@@ -14,6 +14,7 @@ from odoo.osv import expression
from markupsafe import Markup
import logging
import json
import uuid
import requests
from datetime import datetime as dt_datetime, timedelta
import urllib.parse
@@ -32,7 +33,7 @@ class FusionTechnicianTask(models.Model):
"""Richer display name: Client - Type | 9:00 AM - 10:00 AM."""
type_labels = dict(self._fields['task_type'].selection)
for task in self:
client = task.partner_id.name or ''
client = task.x_fc_sync_client_name if task.x_fc_sync_source else (task.partner_id.name or '')
ttype = type_labels.get(task.task_type, task.task_type or '')
start = self._float_to_time_str(task.time_start)
end = self._float_to_time_str(task.time_end)
@@ -70,6 +71,40 @@ class FusionTechnicianTask(models.Model):
)
active = fields.Boolean(default=True)
# Cross-instance sync fields
x_fc_sync_source = fields.Char(
'Source Instance', readonly=True, index=True,
help='Origin instance ID if this is a synced shadow task (e.g. westin, mobility)',
)
x_fc_sync_remote_id = fields.Integer(
'Remote Task ID', readonly=True,
help='ID of the task on the remote instance',
)
x_fc_sync_uuid = fields.Char(
'Sync UUID', readonly=True, index=True, copy=False,
help='Unique ID for cross-instance deduplication',
)
x_fc_is_shadow = fields.Boolean(
'Shadow Task', compute='_compute_is_shadow', store=True,
help='True if this task was synced from another instance',
)
x_fc_sync_client_name = fields.Char(
'Synced Client Name', readonly=True,
help='Client name from the remote instance (shadow tasks only)',
)
x_fc_source_label = fields.Char(
'Source', compute='_compute_is_shadow', store=True,
)
@api.depends('x_fc_sync_source')
def _compute_is_shadow(self):
local_id = self.env['ir.config_parameter'].sudo().get_param(
'fusion_claims.sync_instance_id', '')
for task in self:
task.x_fc_is_shadow = bool(task.x_fc_sync_source)
task.x_fc_source_label = task.x_fc_sync_source or local_id
technician_id = fields.Many2one(
'res.users',
string='Technician',
@@ -87,10 +122,9 @@ class FusionTechnicianTask(models.Model):
sale_order_id = fields.Many2one(
'sale.order',
string='Related Case',
required=True,
tracking=True,
ondelete='restrict',
help='Sale order / case linked to this task (required)',
help='Sale order / case linked to this task',
)
sale_order_name = fields.Char(
related='sale_order_id.name',
@@ -98,6 +132,19 @@ class FusionTechnicianTask(models.Model):
store=True,
)
purchase_order_id = fields.Many2one(
'purchase.order',
string='Related Purchase Order',
tracking=True,
ondelete='restrict',
help='Purchase order linked to this task (e.g. manufacturer pickup)',
)
purchase_order_name = fields.Char(
related='purchase_order_id.name',
string='PO Reference',
store=True,
)
task_type = fields.Selection([
('delivery', 'Delivery'),
('repair', 'Repair'),
@@ -854,30 +901,60 @@ class FusionTechnicianTask(models.Model):
def _onchange_sale_order_id(self):
"""Auto-fill client and address from the sale order's shipping address."""
if self.sale_order_id:
self.purchase_order_id = False
order = self.sale_order_id
if not self.partner_id:
self.partner_id = order.partner_id
# Use shipping address if different
addr = order.partner_shipping_id or order.partner_id
self.address_partner_id = addr.id
self.address_street = addr.street or ''
self.address_street2 = addr.street2 or ''
self.address_city = addr.city or ''
self.address_state_id = addr.state_id.id if addr.state_id else False
self.address_zip = addr.zip or ''
self.address_lat = addr.x_fc_latitude if hasattr(addr, 'x_fc_latitude') and addr.x_fc_latitude else 0
self.address_lng = addr.x_fc_longitude if hasattr(addr, 'x_fc_longitude') and addr.x_fc_longitude else 0
self._fill_address_from_partner(addr)
@api.onchange('purchase_order_id')
def _onchange_purchase_order_id(self):
"""Auto-fill client and address from the purchase order's vendor."""
if self.purchase_order_id:
self.sale_order_id = False
order = self.purchase_order_id
if not self.partner_id:
self.partner_id = order.partner_id
addr = order.dest_address_id or order.partner_id
self._fill_address_from_partner(addr)
def _fill_address_from_partner(self, addr):
"""Populate address fields from a partner record."""
if not addr:
return
self.address_partner_id = addr.id
self.address_street = addr.street or ''
self.address_street2 = addr.street2 or ''
self.address_city = addr.city or ''
self.address_state_id = addr.state_id.id if addr.state_id else False
self.address_zip = addr.zip or ''
self.address_lat = addr.x_fc_latitude if hasattr(addr, 'x_fc_latitude') and addr.x_fc_latitude else 0
self.address_lng = addr.x_fc_longitude if hasattr(addr, 'x_fc_longitude') and addr.x_fc_longitude else 0
# ------------------------------------------------------------------
# CONSTRAINTS + VALIDATION
# ------------------------------------------------------------------
@api.constrains('sale_order_id', 'purchase_order_id')
def _check_order_link(self):
for task in self:
if task.x_fc_sync_source:
continue
if not task.sale_order_id and not task.purchase_order_id:
raise ValidationError(_(
"A task must be linked to either a Sale Order (Case) or a Purchase Order."
))
@api.constrains('technician_id', 'scheduled_date', 'time_start', 'time_end')
def _check_no_overlap(self):
"""Prevent overlapping bookings for the same technician on the same date."""
for task in self:
if task.status == 'cancelled':
continue
if task.x_fc_sync_source:
continue
# Validate time range
if task.time_start >= task.time_end:
raise ValidationError(_("Start time must be before end time."))
@@ -889,8 +966,8 @@ class FusionTechnicianTask(models.Model):
raise ValidationError(_(
"Tasks must be scheduled within store hours (%s - %s)."
) % (open_str, close_str))
# Validate not in the past (only for new/scheduled tasks)
if task.status == 'scheduled' and task.scheduled_date:
# Validate not in the past (only for new/scheduled local tasks)
if task.status == 'scheduled' and task.scheduled_date and not task.x_fc_sync_source:
today = fields.Date.context_today(self)
if task.scheduled_date < today:
raise ValidationError(_("Cannot schedule tasks in the past."))
@@ -1138,6 +1215,8 @@ class FusionTechnicianTask(models.Model):
for vals in vals_list:
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('fusion.technician.task') or _('New')
if not vals.get('x_fc_sync_uuid') and not vals.get('x_fc_sync_source'):
vals['x_fc_sync_uuid'] = str(uuid.uuid4())
# Auto-populate address from sale order if not provided
if vals.get('sale_order_id') and not vals.get('address_street'):
order = self.env['sale.order'].browse(vals['sale_order_id'])
@@ -1146,15 +1225,23 @@ class FusionTechnicianTask(models.Model):
self._fill_address_vals(vals, addr)
if not vals.get('partner_id'):
vals['partner_id'] = order.partner_id.id
# Auto-populate address from partner if sale order not set
# Auto-populate address from purchase order if not provided
elif vals.get('purchase_order_id') and not vals.get('address_street'):
po = self.env['purchase.order'].browse(vals['purchase_order_id'])
addr = po.dest_address_id or po.partner_id
if addr:
self._fill_address_vals(vals, addr)
if not vals.get('partner_id'):
vals['partner_id'] = po.partner_id.id
# Auto-populate address from partner if no order set
elif vals.get('partner_id') and not vals.get('address_street'):
partner = self.env['res.partner'].browse(vals['partner_id'])
if partner.street:
self._fill_address_vals(vals, partner)
records = super().create(vals_list)
# Post creation notice to linked sale order chatter
# Post creation notice to linked order chatter
for rec in records:
rec._post_task_created_to_sale_order()
rec._post_task_created_to_linked_order()
# If created from "Ready for Delivery" flow, mark the sale order
if self.env.context.get('mark_ready_for_delivery'):
records._mark_sale_order_ready_for_delivery()
@@ -1170,6 +1257,10 @@ class FusionTechnicianTask(models.Model):
# Send "Appointment Scheduled" email
for rec in records:
rec._send_task_scheduled_email()
# Push new local tasks to remote instances
local_records = records.filtered(lambda r: not r.x_fc_sync_source)
if local_records and not self.env.context.get('skip_task_sync'):
self.env['fusion.task.sync.config']._push_tasks(local_records, 'create')
return records
def write(self, vals):
@@ -1226,6 +1317,15 @@ class FusionTechnicianTask(models.Model):
old_start=old['time_start'],
old_end=old['time_end'],
)
# Push updates to remote instances for local tasks
sync_fields = {'technician_id', 'scheduled_date', 'time_start', 'time_end',
'duration_hours', 'status', 'task_type', 'address_street',
'address_city', 'address_zip', 'address_lat', 'address_lng',
'partner_id'}
if sync_fields & set(vals.keys()) and not self.env.context.get('skip_task_sync'):
local_records = self.filtered(lambda r: not r.x_fc_sync_source)
if local_records:
self.env['fusion.task.sync.config']._push_tasks(local_records, 'write')
return res
@api.model
@@ -1242,10 +1342,11 @@ class FusionTechnicianTask(models.Model):
'address_lng': partner.x_fc_longitude if hasattr(partner, 'x_fc_longitude') else 0,
})
def _post_task_created_to_sale_order(self):
"""Post a brief task creation notice to the linked sale order's chatter."""
def _post_task_created_to_linked_order(self):
"""Post a brief task creation notice to the linked order's chatter."""
self.ensure_one()
if not self.sale_order_id:
order = self.sale_order_id or self.purchase_order_id
if not order:
return
task_type_label = dict(self._fields['task_type'].selection).get(self.task_type, self.task_type)
date_str = self.scheduled_date.strftime('%B %d, %Y') if self.scheduled_date else 'TBD'
@@ -1259,7 +1360,7 @@ class FusionTechnicianTask(models.Model):
f'<a href="{task_url}">View Task</a>'
f'</div>'
)
self.sale_order_id.message_post(
order.message_post(
body=body, message_type='notification', subtype_xmlid='mail.mt_note',
)
@@ -1462,6 +1563,19 @@ class FusionTechnicianTask(models.Model):
'res_id': self.sale_order_id.id,
}
def action_view_purchase_order(self):
"""Open the linked purchase order."""
self.ensure_one()
if not self.purchase_order_id:
return
return {
'name': self.purchase_order_id.name,
'type': 'ir.actions.act_window',
'res_model': 'purchase.order',
'view_mode': 'form',
'res_id': self.purchase_order_id.id,
}
def action_complete_task(self):
"""Mark task as Completed."""
for task in self:
@@ -1472,9 +1586,9 @@ class FusionTechnicianTask(models.Model):
'completion_datetime': fields.Datetime.now(),
})
task._post_status_message('completed')
# Post completion notes to sale order chatter if linked
if task.sale_order_id and task.completion_notes:
task._post_completion_to_sale_order()
# Post completion notes to linked order chatter
if task.completion_notes and (task.sale_order_id or task.purchase_order_id):
task._post_completion_to_linked_order()
# Notify the person who scheduled the task
task._notify_scheduler_on_completion()
# Auto-advance ODSP status for delivery tasks
@@ -1607,10 +1721,11 @@ class FusionTechnicianTask(models.Model):
)
self.message_post(body=body, message_type='notification', subtype_xmlid='mail.mt_note')
def _post_completion_to_sale_order(self):
"""Post the completion notes to the linked sale order's chatter."""
def _post_completion_to_linked_order(self):
"""Post the completion notes to the linked order's chatter."""
self.ensure_one()
if not self.sale_order_id or not self.completion_notes:
order = self.sale_order_id or self.purchase_order_id
if not order or not self.completion_notes:
return
task_type_label = dict(self._fields['task_type'].selection).get(self.task_type, self.task_type)
body = Markup(
@@ -1625,7 +1740,7 @@ class FusionTechnicianTask(models.Model):
f'{self.completion_notes}'
f'</div>'
)
self.sale_order_id.message_post(
order.message_post(
body=body,
message_type='notification',
subtype_xmlid='mail.mt_note',
@@ -1639,7 +1754,8 @@ class FusionTechnicianTask(models.Model):
task_type_label = dict(self._fields['task_type'].selection).get(self.task_type, self.task_type)
task_url = f'/web#id={self.id}&model=fusion.technician.task&view_type=form'
client_name = self.partner_id.name or 'N/A'
case_ref = self.sale_order_id.name if self.sale_order_id else ''
order = self.sale_order_id or self.purchase_order_id
case_ref = order.name if order else ''
# Build address string
addr_parts = [p for p in [
self.address_street,
@@ -1694,6 +1810,8 @@ class FusionTechnicianTask(models.Model):
]
if self.sale_order_id:
rows.append(('Case', self.sale_order_id.name))
if self.purchase_order_id:
rows.append(('Purchase Order', self.purchase_order_id.name))
if self.scheduled_date:
date_str = self.scheduled_date.strftime('%B %d, %Y')
start_str = self._float_to_time_str(self.time_start)
@@ -1963,9 +2081,10 @@ class FusionTechnicianTask(models.Model):
"""
api_key = self.env['ir.config_parameter'].sudo().get_param(
'fusion_claims.google_maps_api_key', '')
local_instance = self.env['ir.config_parameter'].sudo().get_param(
'fusion_claims.sync_instance_id', '')
base_domain = [
('status', 'not in', ['cancelled']),
('address_lat', '!=', 0), ('address_lng', '!=', 0),
]
if domain:
base_domain = expression.AND([base_domain, domain])
@@ -1974,13 +2093,18 @@ class FusionTechnicianTask(models.Model):
['name', 'partner_id', 'technician_id', 'task_type',
'address_lat', 'address_lng', 'address_display',
'time_start', 'time_start_display', 'time_end_display',
'status', 'scheduled_date', 'travel_time_minutes'],
'status', 'scheduled_date', 'travel_time_minutes',
'x_fc_sync_client_name', 'x_fc_is_shadow', 'x_fc_sync_source'],
order='scheduled_date asc, time_start asc',
limit=500,
)
# Also include live technician locations
locations = self.env['fusion.technician.location'].get_latest_locations()
return {'api_key': api_key, 'tasks': tasks, 'locations': locations}
return {
'api_key': api_key,
'tasks': tasks,
'locations': locations,
'local_instance_id': local_instance,
}
def _geocode_address(self):
"""Geocode the task address using Google Geocoding API."""