Adds a 'My Schedule' tab to the Fusion Clock portal that lists the current employee's published planning.slot records, grouped by day. Reuses the fusion_clock dark theme and reuses Odoo Planning's stock backend UI (Gantt, send wizard, recurrence) unchanged. - Controller /my/clock/schedule: pulls published slots in next 60 days - Portal template with next-shift hero card, summary stats, grouped list - Bottom-nav xpath inherits target the nav bar specifically (not the Recent Activity 'View All' link, which also linked to /my/clock/timesheets) - 4-tab nav fits via reduced padding and flex sizing Module depends on stock 'planning' (Enterprise) + fusion_clock. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
3.5 KiB
Python
98 lines
3.5 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)
|
|
|
|
Slot = request.env['planning.slot'].sudo()
|
|
domain = [
|
|
('state', '=', 'published'),
|
|
('end_datetime', '>=', now_utc),
|
|
('start_datetime', '<=', horizon_utc),
|
|
]
|
|
if employee.resource_id:
|
|
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:
|
|
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()
|
|
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')
|
|
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 '',
|
|
})
|
|
|
|
next_slot_data = None
|
|
if slots:
|
|
next_slot = slots[0]
|
|
local_start = pytz.UTC.localize(next_slot.start_datetime).astimezone(local_tz)
|
|
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 '',
|
|
}
|
|
|
|
values = {
|
|
'employee': employee,
|
|
'groups': groups,
|
|
'slot_count': len(slots),
|
|
'next_slot': next_slot_data,
|
|
'page_name': 'fusion_clock_schedule',
|
|
}
|
|
return request.render('fusion_planning.portal_schedule_page', values)
|