feat(fusion_clock): Publish & Notify range + portal Schedule fold-in [A6-A7]
Generalise post_week into fclk_publish_range/fclk_email_posted_range + planner Publish… panel + publish_range endpoint. Fold the /my/clock/schedule controller+template+css from fusion_planning into fusion_clock (native schedule only, role colour); inline Schedule nav across all portal pages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import portal_clock
|
||||
from . import portal_schedule
|
||||
from . import clock_api
|
||||
from . import clock_kiosk
|
||||
from . import clock_nfc_kiosk
|
||||
|
||||
92
fusion_clock/controllers/portal_schedule.py
Normal file
92
fusion_clock/controllers/portal_schedule.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# -*- 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 odoo import http, fields
|
||||
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()
|
||||
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 '',
|
||||
},
|
||||
))
|
||||
|
||||
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',
|
||||
'show_payslips': 'hr.payslip' in request.env,
|
||||
}
|
||||
return request.render('fusion_clock.portal_schedule_page', values)
|
||||
@@ -166,23 +166,7 @@ class FusionClockShiftPlanner(http.Controller):
|
||||
end = start + timedelta(days=6)
|
||||
employees = self._manager_employees()
|
||||
Schedule = request.env['fusion.clock.schedule'].sudo()
|
||||
|
||||
entries = Schedule.search([
|
||||
('employee_id', 'in', employees.ids),
|
||||
('schedule_date', '>=', start),
|
||||
('schedule_date', '<=', end),
|
||||
('state', '!=', 'posted'),
|
||||
])
|
||||
posted_count = len(entries)
|
||||
affected = entries.mapped('employee_id')
|
||||
if entries:
|
||||
entries.write({'state': 'posted', 'posted_date': fields.Datetime.now()})
|
||||
|
||||
notified = 0
|
||||
for employee in affected:
|
||||
if Schedule.fclk_email_posted_week(employee, start, end):
|
||||
notified += 1
|
||||
|
||||
posted_count, notified = Schedule.fclk_publish_range(employees, start, end)
|
||||
return {
|
||||
'success': True,
|
||||
'posted': posted_count,
|
||||
@@ -190,6 +174,30 @@ class FusionClockShiftPlanner(http.Controller):
|
||||
'data': self._load_week_data(start),
|
||||
}
|
||||
|
||||
@http.route('/fusion_clock/shift_planner/publish_range', type='jsonrpc', auth='user', methods=['POST'])
|
||||
def publish_range(self, date_from=None, date_to=None, employee_ids=None, message=None,
|
||||
week_start=None, **kw):
|
||||
"""Publish & Notify over an arbitrary date range, optionally limited to a
|
||||
subset of employees, with an optional custom message in the email."""
|
||||
if not self._check_manager():
|
||||
return {'error': 'Access denied.'}
|
||||
start = fields.Date.to_date(date_from) or self._week_start(week_start)
|
||||
end = fields.Date.to_date(date_to) or (start + timedelta(days=6))
|
||||
if end < start:
|
||||
return {'success': False, 'message': 'End date must be on or after the start date.'}
|
||||
employees = self._manager_employees()
|
||||
if employee_ids:
|
||||
wanted = {int(eid) for eid in employee_ids}
|
||||
employees = employees.filtered(lambda e: e.id in wanted)
|
||||
Schedule = request.env['fusion.clock.schedule'].sudo()
|
||||
posted_count, notified = Schedule.fclk_publish_range(employees, start, end, message=message)
|
||||
return {
|
||||
'success': True,
|
||||
'posted': posted_count,
|
||||
'notified': notified,
|
||||
'data': self._load_week_data(week_start),
|
||||
}
|
||||
|
||||
@http.route('/fusion_clock/shift_planner/copy_previous_week', type='jsonrpc', auth='user', methods=['POST'])
|
||||
def copy_previous_week(self, week_start=None, **kw):
|
||||
if not self._check_manager():
|
||||
|
||||
Reference in New Issue
Block a user