changes
This commit is contained in:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user