# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # # Portal "My Schedule" tab. Folded in from the retired fusion_planning bridge — # now reads ONLY the native fusion.clock.schedule (no planning.slot), so it # works on Community Odoo. import logging from collections import OrderedDict from datetime import timedelta from urllib.parse import quote from odoo import http, fields from odoo.exceptions import ValidationError from odoo.http import request _logger = logging.getLogger(__name__) class FusionClockSchedulePortal(http.Controller): """Exposes the employee's published shifts on the portal Schedule tab.""" @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') now_utc = fields.Datetime.now() today_local = fields.Datetime.context_timestamp(request.env.user, now_utc).date() horizon_local = today_local + timedelta(days=60) Schedule = request.env['fusion.clock.schedule'].sudo() cutoff = employee.company_id.fclk_self_unassign_days_before or 0 entries = [] 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.role_id.name if sch.role_id else '', 'role_color': sch.role_id._get_color_from_code() if sch.role_id else '', 'note': sch.note or '', 'schedule_id': sch.id, 'releasable': (day - today_local).days >= cutoff, }, )) entries.sort(key=lambda e: e[0]) # Open shifts the employee may claim: company-scoped, future, and either # role-eligible (allowed-role list contains the shift role) or roleless. open_shifts = [] for row in Schedule.search([ ('is_open', '=', True), ('state', '=', 'posted'), ('company_id', '=', employee.company_id.id), ('schedule_date', '>=', today_local), ('schedule_date', '<=', horizon_local), ], order='schedule_date asc, start_time asc', limit=100): if row.role_id and employee.x_fclk_role_ids and row.role_id not in employee.x_fclk_role_ids: continue d = row.schedule_date open_shifts.append({ 'id': row.id, 'date_full': d.strftime('%a, %b %d'), 'time_range': '%s - %s' % ( Schedule.fclk_float_to_display(row.start_time), Schedule.fclk_float_to_display(row.end_time), ), 'role_name': row.role_id.name if row.role_id else '', 'duration_hours': round(row.planned_hours or 0.0, 1), }) 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, 'open_shifts': open_shifts, 'error': kw.get('err'), 'success': kw.get('ok'), 'page_name': 'fusion_clock_schedule', 'show_payslips': 'hr.payslip' in request.env, } return request.render('fusion_clock.portal_schedule_page', values) @http.route('/my/clock/schedule/claim', type='http', auth='user', methods=['POST'], website=True) def claim_open_shift(self, schedule_id=None, **kw): employee = request.env.user.employee_id if not employee or not schedule_id: return request.redirect('/my/clock/schedule') Schedule = request.env['fusion.clock.schedule'].sudo() sch = Schedule.browse(int(schedule_id)) try: Schedule.fclk_claim_open_shift(sch, employee) return request.redirect('/my/clock/schedule?ok=claimed') except ValidationError as exc: return request.redirect( '/my/clock/schedule?err=' + quote(str(exc.args[0] if exc.args else exc))) @http.route('/my/clock/schedule/release', type='http', auth='user', methods=['POST'], website=True) def release_shift(self, schedule_id=None, **kw): employee = request.env.user.employee_id if not employee or not schedule_id: return request.redirect('/my/clock/schedule') Schedule = request.env['fusion.clock.schedule'].sudo() sch = Schedule.browse(int(schedule_id)) try: Schedule.fclk_release_shift(sch, employee) return request.redirect('/my/clock/schedule?ok=released') except ValidationError as exc: return request.redirect( '/my/clock/schedule?err=' + quote(str(exc.args[0] if exc.args else exc)))