fix(fusion_planning): My Schedule shows posted fusion_clock shifts, not just Planning slots

The "My Schedule" portal page read only published planning.slot (Odoo Planning),
but team leads post in the fusion_clock Shift Planner, which writes
fusion.clock.schedule -> so posted schedules never appeared. Merge both sources:
the page now lists published planning.slot AND posted fusion.clock.schedule
(employee, state=posted, not OFF, within the 60-day horizon), sorted together.
Verified on entech: Garry's 7 posted shifts (Jun 1-7) now render. 19.0.1.5.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-31 01:37:18 -04:00
parent defa7250e1
commit 0acd2251e6
2 changed files with 73 additions and 36 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Planning',
'version': '19.0.1.4.0',
'version': '19.0.1.5.0',
'category': 'Human Resources/Planning',
'summary': 'Fusion Clock bridge to Odoo Planning - employee schedule on the portal',
'description': """

View File

@@ -31,66 +31,103 @@ class FusionPlanningPortal(http.Controller):
now_utc = fields.Datetime.now()
horizon_utc = now_utc + timedelta(days=60)
today_local = fields.Datetime.context_timestamp(request.env.user, now_utc).date()
horizon_local = today_local + timedelta(days=60)
# Upcoming shifts come from BOTH sources: published Odoo Planning slots
# AND posted Fusion Clock shift-planner entries (the planner the team
# leads use day-to-day). Each is normalised to a
# (sort_key, date, display_dict) tuple so they merge + sort together.
entries = []
# 1) Published Odoo Planning slots.
Slot = request.env['planning.slot'].sudo()
domain = [
slot_domain = [
('state', '=', 'published'),
('end_datetime', '>=', now_utc),
('start_datetime', '<=', horizon_utc),
]
if employee.resource_id:
domain.append(('resource_id', '=', employee.resource_id.id))
slot_domain.append(('resource_id', '=', employee.resource_id.id))
else:
domain.append(('resource_id', '=', -1))
slots = Slot.search(domain, order='start_datetime asc', limit=200)
groups = OrderedDict()
today_local = fields.Datetime.context_timestamp(
request.env.user, now_utc
).date()
for slot in slots:
slot_domain.append(('resource_id', '=', -1))
for slot in Slot.search(slot_domain, order='start_datetime asc', limit=200):
local_start = pytz.UTC.localize(slot.start_datetime).astimezone(local_tz)
local_end = pytz.UTC.localize(slot.end_datetime).astimezone(local_tz)
day = local_start.date()
entries.append((
(local_start.date(), local_start.hour * 60 + local_start.minute),
local_start.date(),
{
'day_label': local_start.strftime('%a').upper(),
'day_num': local_start.strftime('%d'),
'date_full': local_start.strftime('%b %d, %Y'),
'time_range': '%s - %s' % (
local_start.strftime('%I:%M %p').lstrip('0'),
local_end.strftime('%I:%M %p').lstrip('0'),
),
'duration_hours': round(slot.allocated_hours or 0.0, 1),
'role_name': slot.role_id.name if slot.role_id else '',
'role_color': slot.role_id.color if slot.role_id else 0,
'note': slot.name or '',
},
))
# 2) Posted Fusion Clock shift-planner schedule (local clock times).
Schedule = request.env['fusion.clock.schedule'].sudo()
for sch in Schedule.search([
('employee_id', '=', employee.id),
('state', '=', 'posted'),
('is_off', '=', False),
('schedule_date', '>=', today_local),
('schedule_date', '<=', horizon_local),
], order='schedule_date asc', limit=200):
day = sch.schedule_date
entries.append((
(day, int(round((sch.start_time or 0.0) * 60))),
day,
{
'day_label': day.strftime('%a').upper(),
'day_num': day.strftime('%d'),
'date_full': day.strftime('%b %d, %Y'),
'time_range': '%s - %s' % (
Schedule.fclk_float_to_display(sch.start_time),
Schedule.fclk_float_to_display(sch.end_time),
),
'duration_hours': round(sch.planned_hours or 0.0, 1),
'role_name': sch.shift_id.name if sch.shift_id else '',
'role_color': 0,
'note': sch.note or '',
},
))
entries.sort(key=lambda e: e[0])
groups = OrderedDict()
for _key, day, item in entries:
delta_days = (day - today_local).days
if delta_days == 0:
bucket_key = 'Today'
elif delta_days == 1:
bucket_key = 'Tomorrow'
elif 0 <= delta_days <= 6:
bucket_key = local_start.strftime('%A')
bucket_key = day.strftime('%A')
else:
bucket_key = local_start.strftime('%b %d')
groups.setdefault(bucket_key, []).append({
'slot': slot,
'day_label': local_start.strftime('%a').upper(),
'day_num': local_start.strftime('%d'),
'date_full': local_start.strftime('%b %d, %Y'),
'time_range': '%s - %s' % (
local_start.strftime('%I:%M %p').lstrip('0'),
local_end.strftime('%I:%M %p').lstrip('0'),
),
'duration_hours': round(slot.allocated_hours or 0.0, 1),
'role_name': slot.role_id.name if slot.role_id else '',
'role_color': slot.role_id.color if slot.role_id else 0,
'note': slot.name or '',
})
bucket_key = day.strftime('%b %d')
groups.setdefault(bucket_key, []).append(item)
next_slot_data = None
if slots:
next_slot = slots[0]
local_start = pytz.UTC.localize(next_slot.start_datetime).astimezone(local_tz)
if entries:
first = entries[0][2]
next_slot_data = {
'date': local_start.strftime('%a, %b %d'),
'time': local_start.strftime('%I:%M %p').lstrip('0'),
'role': next_slot.role_id.name if next_slot.role_id else '',
'date': entries[0][1].strftime('%a, %b %d'),
'time': first['time_range'].split(' - ')[0],
'role': first['role_name'],
}
values = {
'employee': employee,
'groups': groups,
'slot_count': len(slots),
'slot_count': len(entries),
'next_slot': next_slot_data,
'page_name': 'fusion_clock_schedule',
# Match the other portal pages so the Payslips nav tab appears