This commit is contained in:
gsinghpal
2026-03-09 15:21:22 -04:00
parent a3e85a23ef
commit acd3fc455e
243 changed files with 20459 additions and 4197 deletions

View File

@@ -8,6 +8,7 @@ import logging
import requests
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.http import request as http_request
_logger = logging.getLogger(__name__)
@@ -126,16 +127,18 @@ class FusionClockLocation(models.Model):
return False
def action_detect_ip(self):
"""Detect the current public IP and append it to the whitelist."""
"""Detect the IP the Odoo server sees from your browser and add it."""
self.ensure_one()
try:
resp = requests.get('https://ipapi.co/json/', timeout=10)
data = resp.json()
ip = data.get('ip', '')
ip = ''
if http_request:
ip = http_request.httprequest.environ.get(
'HTTP_X_FORWARDED_FOR', ''
).split(',')[0].strip()
if not ip:
raise UserError(_("Could not detect public IP."))
except requests.exceptions.RequestException as e:
raise UserError(_("Network error detecting IP: %s") % str(e))
ip = http_request.httprequest.remote_addr or ''
if not ip:
raise UserError(_("Could not detect your IP from the request."))
existing = (self.ip_whitelist or '').strip()
existing_lines = [l.strip() for l in existing.split('\n') if l.strip()] if existing else []
@@ -145,7 +148,7 @@ class FusionClockLocation(models.Model):
'tag': 'display_notification',
'params': {
'title': _('Already Whitelisted'),
'message': _('%s is already in the whitelist.') % ip,
'message': _('IP %s is already in the whitelist.') % ip,
'type': 'warning',
'sticky': False,
},
@@ -153,20 +156,25 @@ class FusionClockLocation(models.Model):
existing_lines.append(ip)
self.ip_whitelist = '\n'.join(existing_lines)
city = data.get('city', '')
org = data.get('org', '')
detail = f"{ip}"
if city:
detail += f" ({city}"
if org:
detail += f" - {org}"
detail += ")"
extra = ''
try:
resp = requests.get(f'https://ipapi.co/{ip}/json/', timeout=5)
if resp.ok:
data = resp.json()
city = data.get('city', '')
org = data.get('org', '')
if city or org:
extra = f" ({', '.join(filter(None, [city, org]))})"
except Exception:
pass
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('IP Detected & Added'),
'message': _('Added %s to the whitelist.') % detail,
'message': _('Added %s%s to the whitelist. This is the IP the server sees from your browser.') % (ip, extra),
'type': 'success',
'sticky': False,
},

View File

@@ -170,17 +170,79 @@ class FusionClockReport(models.Model):
})
def _send_report_email(self):
"""Send the report via mail template."""
"""Send the report with the PDF attached."""
self.ensure_one()
if self.employee_id:
template = self.env.ref('fusion_clock.mail_template_clock_employee_report', raise_if_not_found=False)
else:
template = self.env.ref('fusion_clock.mail_template_clock_batch_report', raise_if_not_found=False)
company_email = self.company_id.email or ''
if template:
template.send_mail(self.id, force_send=True)
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>'
)
else:
_logger.warning("Fusion Clock: Mail template not found for report %s", self.id)
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>'
)
if not email_to:
_logger.warning("Fusion Clock: No email recipient for report %s", self.id)
return
mail_vals = {
'subject': subject,
'email_from': company_email,
'email_to': email_to,
'body_html': body,
'auto_delete': True,
}
if self.report_pdf:
mail_vals['attachment_ids'] = [(0, 0, {
'name': self.report_pdf_filename or 'report.pdf',
'datas': self.report_pdf,
'mimetype': 'application/pdf',
})]
try:
self.env['mail.mail'].sudo().create(mail_vals).send()
except Exception as e:
_logger.error("Fusion Clock: Failed to send report email: %s", e)
def action_export_csv(self):
"""Export the report data as a CSV file for payroll."""

View File

@@ -360,7 +360,7 @@ class HrAttendance(models.Model):
('x_fclk_enable_clock', '=', True),
])
template = self.env.ref('fusion_clock.mail_template_weekly_summary', raise_if_not_found=False)
company_email = self.env.company.email or ''
for emp in employees:
if not emp.work_email:
@@ -373,35 +373,66 @@ class HrAttendance(models.Model):
('check_out', '!=', False),
])
total_net = sum(a.x_fclk_net_hours or 0 for a in atts)
total_ot = sum(a.x_fclk_overtime_hours or 0 for a in atts)
penalties = self.env['fusion.clock.penalty'].sudo().search_count([
total_net = round(sum(a.x_fclk_net_hours or 0 for a in atts), 1)
total_ot = round(sum(a.x_fclk_overtime_hours or 0 for a in atts), 1)
penalty_count = self.env['fusion.clock.penalty'].sudo().search_count([
('employee_id', '=', emp.id),
('date', '>=', week_start),
('date', '<=', week_end),
])
ActivityLog = self.env['fusion.clock.activity.log'].sudo()
absences = ActivityLog.search_count([
absence_count = ActivityLog.search_count([
('employee_id', '=', emp.id),
('log_type', '=', 'absent'),
('log_date', '>=', datetime.combine(week_start, datetime.min.time())),
('log_date', '<', datetime.combine(week_end + timedelta(days=1), datetime.min.time())),
])
if template:
try:
template.with_context(
week_start=week_start,
week_end=week_end,
total_hours=round(total_net, 1),
overtime_hours=round(total_ot, 1),
penalty_count=penalties,
absence_count=absences,
streak=emp.x_fclk_ontime_streak,
).send_mail(emp.id, force_send=False)
except Exception as e:
_logger.error("Fusion Clock: Failed to send weekly summary to %s: %s", emp.name, e)
streak = emp.x_fclk_ontime_streak or 0
def _row(label, value, bg=False):
bg_style = 'background:#f8f9fa;' if bg else ''
return (
f'<tr style="{bg_style}">'
f'<td style="padding:10px 14px;border:1px solid #e0e0e0;font-weight:600;">{label}</td>'
f'<td style="padding:10px 14px;border:1px solid #e0e0e0;">{value}</td>'
f'</tr>'
)
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;border-radius:8px 8px 0 0;">'
'<h2 style="color:#10B981;margin:0;">Fusion Clock</h2>'
'<p style="color:#9ca3af;margin:4px 0 0;">Weekly Summary</p>'
'</td></tr>'
'<tr><td style="padding:24px 32px;">'
f'<p>Hello <strong>{emp.name}</strong>,</p>'
f'<p>Here is your attendance summary for <strong>{week_start}</strong> to <strong>{week_end}</strong>:</p>'
'<table width="100%" style="margin:16px 0;border-collapse:collapse;">'
+ _row('Total Hours', f'{total_net}h', True)
+ _row('Overtime', f'{total_ot}h', False)
+ _row('Penalties', str(penalty_count), True)
+ _row('Absences', str(absence_count), False)
+ _row('On-Time Streak', f'{streak} days', True)
+ '</table>'
'<p>Log in to <a href="/my/clock" style="color:#10B981;">your portal</a> to view details.</p>'
'<p style="color:#6b7280;font-size:12px;margin-top:16px;">This is an automated message from Fusion Clock.</p>'
'</td></tr></table></div>'
)
try:
mail = self.env['mail.mail'].sudo().create({
'subject': f'Your Weekly Attendance Summary ({week_start} - {week_end})',
'email_from': company_email,
'email_to': emp.work_email,
'body_html': body,
'auto_delete': True,
})
mail.send()
except Exception as e:
_logger.error("Fusion Clock: Failed to send weekly summary to %s: %s", emp.name, e)
@api.model
def _fclk_notify_office(self, office_user_id, summary, note, res_model, res_id):