264 lines
10 KiB
Python
264 lines
10 KiB
Python
# -*- 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/<int:report_id>/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}"'),
|
|
],
|
|
)
|