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