# -*- 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)