update
This commit is contained in:
@@ -4,9 +4,12 @@
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import pytz
|
||||
from datetime import timedelta
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from .hr_attendance import _fclk_email_wrap, _fclk_utc_to_local_str
|
||||
from .tz_utils import get_local_today, get_local_day_boundaries, _resolve_tz
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -67,6 +70,7 @@ class FusionClockReport(models.Model):
|
||||
total_breaks = fields.Float(string='Total Breaks (min)', compute='_compute_totals', store=True)
|
||||
total_penalties = fields.Integer(string='Penalty Count', compute='_compute_totals', store=True)
|
||||
days_worked = fields.Integer(string='Days Worked', compute='_compute_totals', store=True)
|
||||
leave_days = fields.Integer(string='Leave Days', compute='_compute_totals', store=True)
|
||||
attendance_ids = fields.Many2many(
|
||||
'hr.attendance',
|
||||
'fusion_clock_report_attendance_rel',
|
||||
@@ -74,6 +78,13 @@ class FusionClockReport(models.Model):
|
||||
'attendance_id',
|
||||
string='Attendance Records',
|
||||
)
|
||||
leave_request_ids = fields.Many2many(
|
||||
'fusion.clock.leave.request',
|
||||
'fusion_clock_report_leave_rel',
|
||||
'report_id',
|
||||
'leave_request_id',
|
||||
string='Leave Requests',
|
||||
)
|
||||
|
||||
# PDF
|
||||
report_pdf = fields.Binary(string='Report PDF', attachment=True)
|
||||
@@ -93,7 +104,8 @@ class FusionClockReport(models.Model):
|
||||
rec.is_batch = not bool(rec.employee_id)
|
||||
|
||||
@api.depends('attendance_ids', 'attendance_ids.worked_hours',
|
||||
'attendance_ids.x_fclk_net_hours', 'attendance_ids.x_fclk_break_minutes')
|
||||
'attendance_ids.x_fclk_net_hours', 'attendance_ids.x_fclk_break_minutes',
|
||||
'leave_request_ids')
|
||||
def _compute_totals(self):
|
||||
for rec in self:
|
||||
atts = rec.attendance_ids
|
||||
@@ -105,12 +117,14 @@ class FusionClockReport(models.Model):
|
||||
('date', '>=', rec.date_start),
|
||||
('date', '<=', rec.date_end),
|
||||
]) if rec.employee_id else 0
|
||||
# Count unique dates
|
||||
tz = _resolve_tz(rec.env, rec.employee_id)
|
||||
dates = set()
|
||||
for a in atts:
|
||||
if a.check_in:
|
||||
dates.add(a.check_in.date())
|
||||
local_dt = pytz.UTC.localize(a.check_in).astimezone(tz)
|
||||
dates.add(local_dt.date())
|
||||
rec.days_worked = len(dates)
|
||||
rec.leave_days = len(rec.leave_request_ids)
|
||||
|
||||
def action_generate_report(self):
|
||||
"""Generate the PDF report for this record."""
|
||||
@@ -119,6 +133,11 @@ class FusionClockReport(models.Model):
|
||||
self._generate_pdf()
|
||||
self.state = 'generated'
|
||||
|
||||
def action_reset_draft(self):
|
||||
"""Reset the report back to draft so the user can make changes."""
|
||||
for rec in self:
|
||||
rec.state = 'draft'
|
||||
|
||||
def action_send_report(self):
|
||||
"""Send the report via email."""
|
||||
self.ensure_one()
|
||||
@@ -128,11 +147,14 @@ class FusionClockReport(models.Model):
|
||||
self.state = 'sent'
|
||||
|
||||
def _collect_attendance_records(self):
|
||||
"""Link attendance records for the period and employee."""
|
||||
"""Link attendance and leave records for the period and employee."""
|
||||
self.ensure_one()
|
||||
employee = self.employee_id or None
|
||||
start_utc, _ = get_local_day_boundaries(self.env, self.date_start, employee)
|
||||
_, end_utc = get_local_day_boundaries(self.env, self.date_end, employee)
|
||||
domain = [
|
||||
('check_in', '>=', fields.Datetime.to_datetime(self.date_start)),
|
||||
('check_in', '<', fields.Datetime.to_datetime(self.date_end + timedelta(days=1))),
|
||||
('check_in', '>=', start_utc),
|
||||
('check_in', '<', end_utc),
|
||||
('check_out', '!=', False),
|
||||
]
|
||||
if self.employee_id:
|
||||
@@ -143,6 +165,18 @@ class FusionClockReport(models.Model):
|
||||
attendances = self.env['hr.attendance'].search(domain)
|
||||
self.attendance_ids = [(6, 0, attendances.ids)]
|
||||
|
||||
leave_domain = [
|
||||
('leave_date', '>=', self.date_start),
|
||||
('leave_date', '<=', self.date_end),
|
||||
]
|
||||
if self.employee_id:
|
||||
leave_domain.append(('employee_id', '=', self.employee_id.id))
|
||||
else:
|
||||
leave_domain.append(('company_id', '=', self.company_id.id))
|
||||
|
||||
leaves = self.env['fusion.clock.leave.request'].search(leave_domain)
|
||||
self.leave_request_ids = [(6, 0, leaves.ids)]
|
||||
|
||||
def _generate_pdf(self):
|
||||
"""Render the QWeb report to PDF and store it."""
|
||||
self.ensure_one()
|
||||
@@ -172,52 +206,63 @@ class FusionClockReport(models.Model):
|
||||
def _send_report_email(self):
|
||||
"""Send the report with the PDF attached."""
|
||||
self.ensure_one()
|
||||
company_email = self.company_id.email or ''
|
||||
company = self.company_id or self.env.company
|
||||
company_email = company.email or ''
|
||||
company_name = company.name or ''
|
||||
ds_fmt = self.date_start.strftime('%b %d, %Y') if self.date_start else ''
|
||||
de_fmt = self.date_end.strftime('%b %d, %Y') if self.date_end else ''
|
||||
|
||||
if self.employee_id:
|
||||
email_to = self.employee_id.work_email or ''
|
||||
subject = f"Your Attendance Report - {self.date_start} to {self.date_end}"
|
||||
body = (
|
||||
'<div style="margin:0;padding:0;font-family:Arial,Helvetica,sans-serif;">'
|
||||
'<table width="600" style="margin:0 auto;background:#ffffff;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;">'
|
||||
'<tr><td style="padding:24px 32px;background:#1a1d23;">'
|
||||
'<h2 style="color:#10B981;margin:0;">Fusion Clock</h2>'
|
||||
'<p style="color:#9ca3af;margin:4px 0 0;">Attendance Report</p>'
|
||||
'</td></tr><tr><td style="padding:24px 32px;">'
|
||||
f'<p>Hello <strong>{self.employee_id.name}</strong>,</p>'
|
||||
f'<p>Your attendance report for <strong>{self.date_start}</strong> to '
|
||||
f'<strong>{self.date_end}</strong> is ready.</p>'
|
||||
'<table width="100%" style="margin:16px 0;border-collapse:collapse;">'
|
||||
f'<tr style="background:#f8f9fa;"><td style="padding:10px 14px;border:1px solid #e0e0e0;font-weight:600;">Days Worked</td>'
|
||||
f'<td style="padding:10px 14px;border:1px solid #e0e0e0;">{self.days_worked}</td></tr>'
|
||||
f'<tr><td style="padding:10px 14px;border:1px solid #e0e0e0;font-weight:600;">Total Hours</td>'
|
||||
f'<td style="padding:10px 14px;border:1px solid #e0e0e0;">{self.total_hours:.1f}h</td></tr>'
|
||||
f'<tr style="background:#f8f9fa;"><td style="padding:10px 14px;border:1px solid #e0e0e0;font-weight:600;">Net Hours</td>'
|
||||
f'<td style="padding:10px 14px;border:1px solid #e0e0e0;">{self.net_hours:.1f}h</td></tr>'
|
||||
f'<tr><td style="padding:10px 14px;border:1px solid #e0e0e0;font-weight:600;">Total Breaks</td>'
|
||||
f'<td style="padding:10px 14px;border:1px solid #e0e0e0;">{self.total_breaks:.0f} min</td></tr>'
|
||||
'</table>'
|
||||
'<p>The full PDF report is attached.</p>'
|
||||
'<p style="color:#6b7280;font-size:12px;margin-top:16px;">This is an automated message from Fusion Clock.</p>'
|
||||
'</td></tr></table></div>'
|
||||
subject = f"Your Attendance Report - {ds_fmt} to {de_fmt}"
|
||||
body = _fclk_email_wrap(
|
||||
company_name=company_name,
|
||||
title='Attendance Report',
|
||||
summary=(
|
||||
f'Hello <strong>{self.employee_id.name}</strong>, your attendance '
|
||||
f'report for <strong>{ds_fmt}</strong> to '
|
||||
f'<strong>{de_fmt}</strong> is ready.'
|
||||
),
|
||||
sections=[('Report Summary', [
|
||||
('Pay Period', f'{ds_fmt} — {de_fmt}'),
|
||||
('Days Worked', str(self.days_worked)),
|
||||
('Leave Days', str(self.leave_days)),
|
||||
('Total Hours', f'{self.total_hours:.1f}h'),
|
||||
('Net Hours', f'{self.net_hours:.1f}h'),
|
||||
('Total Breaks', f'{self.total_breaks:.0f} min'),
|
||||
('Penalties', str(self.total_penalties)),
|
||||
])],
|
||||
note='The full PDF report is attached. You can also download '
|
||||
'it from <a href="/my/clock" style="color:#10B981;">'
|
||||
'your portal</a> at any time.',
|
||||
attachments_note='Attendance Report (PDF)',
|
||||
)
|
||||
else:
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
email_to = ICP.get_param('fusion_clock.report_recipient_emails', '')
|
||||
subject = f"Employee Attendance Batch Report - {self.date_start} to {self.date_end}"
|
||||
body = (
|
||||
'<div style="margin:0;padding:0;font-family:Arial,Helvetica,sans-serif;">'
|
||||
'<table width="600" style="margin:0 auto;background:#ffffff;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;">'
|
||||
'<tr><td style="padding:24px 32px;background:#1a1d23;">'
|
||||
'<h2 style="color:#10B981;margin:0;">Fusion Clock</h2>'
|
||||
'<p style="color:#9ca3af;margin:4px 0 0;">Batch Attendance Report</p>'
|
||||
'</td></tr><tr><td style="padding:24px 32px;">'
|
||||
f'<p>The attendance batch report for <strong>{self.date_start}</strong> to '
|
||||
f'<strong>{self.date_end}</strong> is attached.</p>'
|
||||
'<p>This report includes all employees\' attendance summaries with daily breakdowns, '
|
||||
'total hours, and penalty information.</p>'
|
||||
'<p style="color:#6b7280;font-size:12px;margin-top:16px;">This is an automated message from Fusion Clock.</p>'
|
||||
'</td></tr></table></div>'
|
||||
user_ids_str = ICP.get_param('fusion_clock.report_recipient_user_ids', '')
|
||||
if user_ids_str:
|
||||
try:
|
||||
user_ids = [int(x) for x in user_ids_str.split(',') if x.strip()]
|
||||
users = self.env['res.users'].sudo().browse(user_ids).filtered('email')
|
||||
user_emails = ','.join(u.email for u in users if u.email)
|
||||
if email_to and user_emails:
|
||||
email_to = f"{email_to},{user_emails}"
|
||||
elif user_emails:
|
||||
email_to = user_emails
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
subject = f"Employee Attendance Batch Report - {ds_fmt} to {de_fmt}"
|
||||
body = _fclk_email_wrap(
|
||||
company_name=company_name,
|
||||
title='Batch Attendance Report',
|
||||
summary=(
|
||||
f'The attendance batch report for <strong>{ds_fmt}</strong> to '
|
||||
f'<strong>{de_fmt}</strong> is attached.'
|
||||
),
|
||||
note='This report includes all employees\' attendance summaries '
|
||||
'with daily breakdowns, total hours, and penalty information.',
|
||||
attachments_note='Batch Attendance Report (PDF)',
|
||||
)
|
||||
|
||||
if not email_to:
|
||||
@@ -283,8 +328,8 @@ class FusionClockReport(models.Model):
|
||||
|
||||
for att in self.attendance_ids.sorted(key=lambda a: a.check_in):
|
||||
date_str = att.check_in.strftime('%Y-%m-%d') if att.check_in else ''
|
||||
in_str = att.check_in.strftime('%H:%M') if att.check_in else ''
|
||||
out_str = att.check_out.strftime('%H:%M') if att.check_out else ''
|
||||
in_str = _fclk_utc_to_local_str(att.check_in, att.employee_id, '%H:%M') if att.check_in else ''
|
||||
out_str = _fclk_utc_to_local_str(att.check_out, att.employee_id, '%H:%M') if att.check_out else ''
|
||||
penalties = self.env['fusion.clock.penalty'].search_count([
|
||||
('attendance_id', '=', att.id),
|
||||
])
|
||||
@@ -301,6 +346,20 @@ class FusionClockReport(models.Model):
|
||||
att.x_fclk_location_id.name or '',
|
||||
])
|
||||
|
||||
for lv in self.leave_request_ids.sorted(key=lambda l: l.leave_date):
|
||||
writer.writerow([
|
||||
lv.employee_id.name or '',
|
||||
str(lv.leave_date),
|
||||
'LEAVE',
|
||||
'LEAVE',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
lv.reason or '',
|
||||
])
|
||||
|
||||
csv_data = output.getvalue().encode('utf-8')
|
||||
output.close()
|
||||
|
||||
@@ -333,11 +392,10 @@ class FusionClockReport(models.Model):
|
||||
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()
|
||||
period_start, period_end = self._calculate_current_period(schedule_type, period_start_str, today)
|
||||
|
||||
# Only generate if yesterday was the end of a period
|
||||
if period_end != today - timedelta(days=1):
|
||||
today = get_local_today(self.env)
|
||||
yesterday = today - timedelta(days=1)
|
||||
period_start, period_end = self._calculate_current_period(schedule_type, period_start_str, yesterday)
|
||||
if period_end != yesterday:
|
||||
return
|
||||
|
||||
_logger.info("Fusion Clock: Generating reports for period %s to %s", period_start, period_end)
|
||||
@@ -443,3 +501,86 @@ class FusionClockReport(models.Model):
|
||||
period_end = period_start + timedelta(days=13)
|
||||
|
||||
return period_start, period_end
|
||||
|
||||
@api.model
|
||||
def action_generate_historical_reports(self):
|
||||
"""Generate reports for all past pay periods from historical attendance data.
|
||||
|
||||
Iterates per-employee so each starts from their own earliest
|
||||
attendance record rather than the global earliest. Includes ALL
|
||||
employees who have completed attendance records, not only those
|
||||
with ``x_fclk_enable_clock``.
|
||||
"""
|
||||
ICP = self.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 = get_local_today(self.env)
|
||||
created_count = 0
|
||||
|
||||
emp_groups = self.env['hr.attendance'].sudo().read_group(
|
||||
[('check_out', '!=', False)],
|
||||
['employee_id'],
|
||||
['employee_id'],
|
||||
)
|
||||
|
||||
for emp_data in emp_groups:
|
||||
emp_id = emp_data['employee_id'][0]
|
||||
employee = self.env['hr.employee'].sudo().browse(emp_id)
|
||||
if not employee.exists():
|
||||
continue
|
||||
|
||||
earliest = self.env['hr.attendance'].sudo().search(
|
||||
[('employee_id', '=', emp_id), ('check_out', '!=', False)],
|
||||
order='check_in asc',
|
||||
limit=1,
|
||||
)
|
||||
if not earliest:
|
||||
continue
|
||||
|
||||
current = earliest.check_in.date()
|
||||
|
||||
while current <= today:
|
||||
period_start, period_end = self._calculate_current_period(
|
||||
schedule_type, period_start_str, current,
|
||||
)
|
||||
if period_end > today:
|
||||
break
|
||||
|
||||
existing = self.sudo().search([
|
||||
('employee_id', '=', emp_id),
|
||||
('date_start', '=', period_start),
|
||||
('date_end', '=', period_end),
|
||||
], limit=1)
|
||||
if not existing:
|
||||
att_count = self.env['hr.attendance'].sudo().search_count([
|
||||
('employee_id', '=', emp_id),
|
||||
('check_in', '>=', fields.Datetime.to_datetime(period_start)),
|
||||
('check_in', '<', fields.Datetime.to_datetime(
|
||||
period_end + timedelta(days=1),
|
||||
)),
|
||||
('check_out', '!=', False),
|
||||
])
|
||||
if att_count > 0:
|
||||
report = self.sudo().create({
|
||||
'date_start': period_start,
|
||||
'date_end': period_end,
|
||||
'schedule_type': schedule_type,
|
||||
'employee_id': emp_id,
|
||||
'company_id': employee.company_id.id,
|
||||
})
|
||||
report._collect_attendance_records()
|
||||
created_count += 1
|
||||
|
||||
current = period_end + timedelta(days=1)
|
||||
|
||||
_logger.info("Fusion Clock: Generated %d historical reports", created_count)
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Historical Reports',
|
||||
'message': f'{created_count} reports generated from historical data.',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user