Files
Odoo-Modules/fusion_planning/controllers/portal_schedule.py
gsinghpal 0acd2251e6 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>
2026-05-31 01:37:18 -04:00

138 lines
5.4 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import logging
from collections import OrderedDict
from datetime import timedelta
import pytz
from odoo import http, fields
from odoo.http import request
_logger = logging.getLogger(__name__)
class FusionPlanningPortal(http.Controller):
"""Portal controller exposing the employee's published Planning shifts."""
@http.route('/my/clock/schedule', type='http', auth='user', website=True)
def portal_schedule(self, **kw):
employee = request.env.user.employee_id
if not employee:
return request.redirect('/my')
tz_name = employee.tz or request.env.user.tz or 'UTC'
try:
local_tz = pytz.timezone(tz_name)
except pytz.UnknownTimeZoneError:
local_tz = pytz.UTC
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()
slot_domain = [
('state', '=', 'published'),
('end_datetime', '>=', now_utc),
('start_datetime', '<=', horizon_utc),
]
if employee.resource_id:
slot_domain.append(('resource_id', '=', employee.resource_id.id))
else:
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)
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 = day.strftime('%A')
else:
bucket_key = day.strftime('%b %d')
groups.setdefault(bucket_key, []).append(item)
next_slot_data = None
if entries:
first = entries[0][2]
next_slot_data = {
'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(entries),
'next_slot': next_slot_data,
'page_name': 'fusion_clock_schedule',
# Match the other portal pages so the Payslips nav tab appears
# consistently when payroll is installed.
'show_payslips': 'hr.payslip' in request.env,
}
return request.render('fusion_planning.portal_schedule_page', values)