This commit is contained in:
gsinghpal
2026-03-16 08:14:56 -04:00
parent fdca9518ab
commit e56974d46f
196 changed files with 19739 additions and 3471 deletions

View File

@@ -492,6 +492,55 @@ class FusionTechnicianTask(models.Model):
# SLOT AVAILABILITY HELPERS
# ------------------------------------------------------------------
def _get_calendar_busy_intervals(self, tech_id, date):
"""Return busy intervals from calendar.event for a technician on a date.
Queries events where the technician is an attendee, excluding events
already linked to a fusion task (to avoid double-counting).
Returns list of (start_float, end_float) in local time.
"""
if not tech_id or not date:
return []
user = self.env['res.users'].browse(tech_id)
if not user.exists():
return []
partner_id = user.partner_id.id
import pytz
tz = self._get_local_tz()
day_start = tz.localize(dt_datetime.combine(date, dt_datetime.min.time()))
day_end = day_start + timedelta(days=1)
day_start_utc = day_start.astimezone(pytz.utc).replace(tzinfo=None)
day_end_utc = day_end.astimezone(pytz.utc).replace(tzinfo=None)
task_event_ids = self.sudo().search([
('calendar_event_id', '!=', False),
('scheduled_date', '=', date),
]).mapped('calendar_event_id').ids
domain = [
('partner_ids', 'in', [partner_id]),
('start', '<', day_end_utc),
('stop', '>', day_start_utc),
('active', '=', True),
]
if task_event_ids:
domain.append(('id', 'not in', task_event_ids))
events = self.env['calendar.event'].sudo().search(domain)
intervals = []
for ev in events:
ev_start = pytz.utc.localize(ev.start).astimezone(tz)
ev_end = pytz.utc.localize(ev.stop).astimezone(tz)
start_float = ev_start.hour + ev_start.minute / 60.0
end_float = ev_end.hour + ev_end.minute / 60.0
if ev_start.date() < date:
start_float = 0.0
if ev_end.date() > date:
end_float = 24.0
intervals.append((start_float, end_float))
return sorted(intervals, key=lambda x: x[0])
def _find_next_available_slot(self, tech_id, date, preferred_start=9.0,
duration=1.0, exclude_task_id=False,
dest_lat=0, dest_lng=0):
@@ -537,6 +586,13 @@ class FusionTechnicianTask(models.Model):
b.address_lng or 0,
))
for cal_start, cal_end in self._get_calendar_busy_intervals(tech_id, date):
clamped_start = max(cal_start, STORE_OPEN)
clamped_end = min(cal_end, STORE_CLOSE)
if clamped_start < clamped_end:
intervals.append((clamped_start, clamped_end, 0, 0))
intervals.sort(key=lambda x: x[0])
def _travel_hours(from_lat, from_lng, to_lat, to_lng):
"""Calculate travel time in hours between two locations.
Returns 0 if coordinates are missing. Rounds up to 15-min."""
@@ -708,7 +764,7 @@ class FusionTechnicianTask(models.Model):
"""Combine date + float time into proper Datetime fields for calendar.
time_start/time_end are LOCAL hours; datetime_start/end must be UTC for Odoo."""
import pytz
user_tz = pytz.timezone(self.env.user.tz or 'UTC')
user_tz = self._get_local_tz()
for task in self:
if task.scheduled_date:
# Build local datetime, then convert to UTC
@@ -1114,6 +1170,18 @@ class FusionTechnicianTask(models.Model):
end=end_str,
))
for cal_start, cal_end in self._get_calendar_busy_intervals(
tech_id, task.scheduled_date
):
if cal_start < task.time_end and cal_end > task.time_start:
raise ValidationError(_(
"%(tech)s has a calendar event conflict "
"(%(start)s - %(end)s). Please choose a different time.",
tech=tech_name,
start=self._float_to_time_str(cal_start),
end=self._float_to_time_str(cal_end),
))
# Check travel time gaps for lead technician only
# (additional techs travel with the lead, same destination)
next_task = self.sudo().search([
@@ -1488,11 +1556,19 @@ class FusionTechnicianTask(models.Model):
Falls back gracefully if external calendar validation fails (e.g.
Microsoft Calendar requires the organizer to have Outlook synced).
"""
CalendarEvent = self.env['calendar.event'].sudo()
silent_ctx = {
'dont_notify': True,
'mail_create_nosubscribe': True,
'mail_create_nolog': True,
'no_mail_notification': True,
'no_mail_to_attendees': True,
'skip_attendee_notification': True,
}
CalendarEvent = self.env['calendar.event'].sudo().with_context(**silent_ctx)
for task in self:
if not task.datetime_start or not task.datetime_end or not task.technician_id:
if task.calendar_event_id:
task.calendar_event_id.unlink()
task.calendar_event_id.with_context(**silent_ctx).unlink()
task.with_context(skip_travel_recalc=True).write({'calendar_event_id': False})
continue
@@ -1517,14 +1593,14 @@ class FusionTechnicianTask(models.Model):
'stop': task.datetime_end,
'user_id': task.technician_id.id,
'location': location,
'partner_ids': [(6, 0, [task.technician_id.partner_id.id])],
'partner_ids': [(6, 0, (task.technician_id | task.additional_technician_ids).mapped('partner_id').ids)],
'show_as': 'busy',
'description': '\n'.join(description_parts),
}
try:
if task.calendar_event_id:
task.calendar_event_id.write(vals)
task.calendar_event_id.with_context(**silent_ctx).write(vals)
else:
event = CalendarEvent.create(vals)
task.with_context(skip_travel_recalc=True).write({'calendar_event_id': event.id})
@@ -2904,17 +2980,17 @@ class FusionTechnicianTask(models.Model):
def _get_local_tz(self):
"""Return the pytz timezone for local time calculations.
Prefers company resource calendar, then user tz, then Eastern."""
Priority: company resource calendar > user tz > UTC."""
import pytz
tz_name = (
self.env.company.resource_calendar_id.tz
or self.env.user.tz
or 'America/Toronto'
or 'UTC'
)
try:
return pytz.timezone(tz_name)
except pytz.UnknownTimeZoneError:
return pytz.timezone('America/Toronto')
return pytz.timezone('UTC')
def _utc_to_local(self, dt_utc):
"""Convert a naive UTC datetime to a timezone-aware local datetime."""