# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) import base64 import json import logging from datetime import datetime, timedelta from odoo import http, fields, _ from odoo.http import request from odoo.addons.portal.controllers.portal import CustomerPortal _logger = logging.getLogger(__name__) class FusionClockPortal(CustomerPortal): """Portal controller for Fusion Clock pages.""" def _prepare_portal_layout_values(self): """Inject clock FAB data into every portal page context.""" values = super()._prepare_portal_layout_values() employee = self._get_portal_employee() if employee and employee.x_fclk_enable_clock: is_checked_in = employee.attendance_state == 'checked_in' check_in_time = '' location_name = '' if is_checked_in: att = request.env['hr.attendance'].sudo().search([ ('employee_id', '=', employee.id), ('check_out', '=', False), ], limit=1) if att: check_in_time = att.check_in.isoformat() if att.check_in else '' location_name = att.x_fclk_location_id.name if att.x_fclk_location_id else '' values.update({ 'fclk_employee': employee, 'fclk_checked_in': is_checked_in, 'fclk_check_in_time': check_in_time, 'fclk_location_name': location_name, }) else: values['fclk_employee'] = False return values def _prepare_home_portal_values(self, counters): """Add clock counters to the portal home page.""" values = super()._prepare_home_portal_values(counters) if 'clock_count' in counters: employee = self._get_portal_employee() if employee: today_start = datetime.combine(fields.Date.today(), datetime.min.time()) count = request.env['hr.attendance'].sudo().search_count([ ('employee_id', '=', employee.id), ('check_in', '>=', today_start), ]) values['clock_count'] = count return values def _get_portal_employee(self): """Get the employee record for the current portal/internal user.""" user = request.env.user employee = request.env['hr.employee'].sudo().search([ ('user_id', '=', user.id), ], limit=1) return employee # ========================================================================= # Clock Page # ========================================================================= @http.route('/my/clock', type='http', auth='user', website=True) def portal_clock(self, **kw): """Main clock-in/out portal page.""" employee = self._get_portal_employee() if not employee: return request.redirect('/my') ICP = request.env['ir.config_parameter'].sudo() google_maps_key = ICP.get_param('fusion_clock.google_maps_api_key', '') enable_sounds = ICP.get_param('fusion_clock.enable_sounds', 'True') == 'True' # Get locations Location = request.env['fusion.clock.location'].sudo() locations = Location.search([ ('active', '=', True), ('company_id', '=', employee.company_id.id), ]) locations = locations.filtered( lambda loc: loc.all_employees or employee.id in loc.employee_ids.ids ) # Current attendance status is_checked_in = employee.attendance_state == 'checked_in' current_attendance = False if is_checked_in: current_attendance = request.env['hr.attendance'].sudo().search([ ('employee_id', '=', employee.id), ('check_out', '=', False), ], limit=1) # Today stats today_start = datetime.combine(fields.Date.today(), datetime.min.time()) today_atts = request.env['hr.attendance'].sudo().search([ ('employee_id', '=', employee.id), ('check_in', '>=', today_start), ('check_out', '!=', False), ]) today_hours = sum(a.x_fclk_net_hours or 0 for a in today_atts) # Week stats today = fields.Date.today() week_start = today - timedelta(days=today.weekday()) week_start_dt = datetime.combine(week_start, datetime.min.time()) week_atts = request.env['hr.attendance'].sudo().search([ ('employee_id', '=', employee.id), ('check_in', '>=', week_start_dt), ('check_out', '!=', False), ]) week_hours = sum(a.x_fclk_net_hours or 0 for a in week_atts) # Recent activity recent = request.env['hr.attendance'].sudo().search([ ('employee_id', '=', employee.id), ('check_out', '!=', False), ], order='check_in desc', limit=10) # Prepare locations JSON for JS locations_json = json.dumps([{ 'id': loc.id, 'name': loc.name, 'address': loc.address or '', 'latitude': loc.latitude, 'longitude': loc.longitude, 'radius': loc.radius, } for loc in locations]) values = { 'employee': employee, 'locations': locations, 'is_checked_in': is_checked_in, 'current_attendance': current_attendance, 'today_hours': round(today_hours, 1), 'week_hours': round(week_hours, 1), 'recent_attendances': recent, 'google_maps_key': google_maps_key, 'enable_sounds': enable_sounds, 'locations_json': locations_json, 'page_name': 'clock', } return request.render('fusion_clock.portal_clock_page', values) # ========================================================================= # Timesheet Page # ========================================================================= @http.route('/my/clock/timesheets', type='http', auth='user', website=True) def portal_timesheets(self, period='current', **kw): """Read-only timesheet view.""" employee = self._get_portal_employee() if not employee: return request.redirect('/my') ICP = request.env['ir.config_parameter'].sudo() schedule_type = ICP.get_param('fusion_clock.pay_period_type', 'biweekly') period_start_str = ICP.get_param('fusion_clock.pay_period_start', '') today = fields.Date.today() # Calculate period dates FusionReport = request.env['fusion.clock.report'].sudo() period_start, period_end = FusionReport._calculate_current_period( schedule_type, period_start_str, today ) if period == 'last': # Go back one period if schedule_type == 'weekly': period_start -= timedelta(days=7) period_end -= timedelta(days=7) elif schedule_type == 'biweekly': period_start -= timedelta(days=14) period_end -= timedelta(days=14) elif schedule_type == 'monthly': from dateutil.relativedelta import relativedelta period_start -= relativedelta(months=1) period_end = period_start.replace(day=28) + timedelta(days=4) period_end -= timedelta(days=period_end.day) else: period_start -= timedelta(days=14) period_end -= timedelta(days=14) # Get attendance records attendances = request.env['hr.attendance'].sudo().search([ ('employee_id', '=', employee.id), ('check_in', '>=', datetime.combine(period_start, datetime.min.time())), ('check_in', '<', datetime.combine(period_end + timedelta(days=1), datetime.min.time())), ], order='check_in desc') total_hours = sum(a.worked_hours or 0 for a in attendances if a.check_out) net_hours = sum(a.x_fclk_net_hours or 0 for a in attendances if a.check_out) total_breaks = sum(a.x_fclk_break_minutes or 0 for a in attendances if a.check_out) values = { 'employee': employee, 'attendances': attendances, 'period_start': period_start, 'period_end': period_end, 'period': period, 'schedule_type': schedule_type, 'total_hours': round(total_hours, 1), 'net_hours': round(net_hours, 1), 'total_breaks': round(total_breaks, 0), 'page_name': 'timesheets', } return request.render('fusion_clock.portal_timesheet_page', values) # ========================================================================= # Reports Page # ========================================================================= @http.route('/my/clock/reports', type='http', auth='user', website=True) def portal_reports(self, **kw): """View and download attendance reports.""" employee = self._get_portal_employee() if not employee: return request.redirect('/my') reports = request.env['fusion.clock.report'].sudo().search([ ('employee_id', '=', employee.id), ('state', 'in', ['generated', 'sent']), ], order='date_end desc') values = { 'employee': employee, 'reports': reports, 'page_name': 'clock_reports', } return request.render('fusion_clock.portal_report_page', values) @http.route('/my/clock/reports//download', type='http', auth='user', website=True) def portal_report_download(self, report_id, **kw): """Download a specific report PDF.""" employee = self._get_portal_employee() if not employee: return request.redirect('/my') report = request.env['fusion.clock.report'].sudo().browse(report_id) if not report.exists() or report.employee_id.id != employee.id: return request.redirect('/my/clock/reports') if not report.report_pdf: return request.redirect('/my/clock/reports') pdf_data = base64.b64decode(report.report_pdf) filename = report.report_pdf_filename or f"report_{report.id}.pdf" return request.make_response( pdf_data, headers=[ ('Content-Type', 'application/pdf'), ('Content-Disposition', f'attachment; filename="{filename}"'), ], )