feat(fusion_planning): new module bridging fusion_clock with Odoo Planning
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>
This commit is contained in:
2
fusion_planning/controllers/__init__.py
Normal file
2
fusion_planning/controllers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import portal_schedule
|
||||
97
fusion_planning/controllers/portal_schedule.py
Normal file
97
fusion_planning/controllers/portal_schedule.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- 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)
|
||||
Reference in New Issue
Block a user