Initial commit
This commit is contained in:
263
fusion_clock/controllers/portal_clock.py
Normal file
263
fusion_clock/controllers/portal_clock.py
Normal file
@@ -0,0 +1,263 @@
|
||||
# -*- 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}"'),
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user