- Add 'nfc_kiosk' to x_fclk_clock_source selection on hr.attendance - Add x_fclk_check_in_photo and x_fclk_check_out_photo Binary fields (attachment=True) - Add 'card_enrollment' and 'unknown_card_tap' to activity log log_type selection - Add 'nfc_kiosk' to activity log source selection - Add TestNfcAttendanceFields test class (3 tests); all 6 fusion_clock tests pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
|
|
from odoo import models, fields, api
|
|
|
|
|
|
class FusionClockActivityLog(models.Model):
|
|
_name = 'fusion.clock.activity.log'
|
|
_description = 'Clock Activity Log'
|
|
_order = 'log_date desc, id desc'
|
|
_rec_name = 'display_name'
|
|
|
|
employee_id = fields.Many2one(
|
|
'hr.employee',
|
|
string='Employee',
|
|
required=True,
|
|
index=True,
|
|
ondelete='cascade',
|
|
)
|
|
log_type = fields.Selection(
|
|
[
|
|
('clock_in', 'Clock In'),
|
|
('clock_out', 'Clock Out'),
|
|
('late_clock_in', 'Late Clock-In'),
|
|
('early_clock_out', 'Early Clock-Out'),
|
|
('outside_geofence', 'Outside Geofence'),
|
|
('auto_clock_out', 'Auto Clock-Out'),
|
|
('missed_clock_out', 'Missed Clock-Out'),
|
|
('absent', 'Absent'),
|
|
('leave_request', 'Leave Request'),
|
|
('reason_provided', 'Reason Provided'),
|
|
('overtime', 'Overtime'),
|
|
('correction_request', 'Correction Request'),
|
|
('ip_fallback', 'IP Fallback Used'),
|
|
('streak_milestone', 'Streak Milestone'),
|
|
('card_enrollment', 'Card Enrollment'),
|
|
('unknown_card_tap', 'Unknown Card Tap'),
|
|
],
|
|
string='Log Type',
|
|
required=True,
|
|
index=True,
|
|
)
|
|
log_date = fields.Datetime(
|
|
string='Date/Time',
|
|
required=True,
|
|
default=fields.Datetime.now,
|
|
index=True,
|
|
)
|
|
description = fields.Text(string='Description')
|
|
attendance_id = fields.Many2one(
|
|
'hr.attendance',
|
|
string='Attendance',
|
|
ondelete='set null',
|
|
index=True,
|
|
)
|
|
location_id = fields.Many2one(
|
|
'fusion.clock.location',
|
|
string='Location',
|
|
ondelete='set null',
|
|
)
|
|
latitude = fields.Float(string='Latitude', digits=(10, 7))
|
|
longitude = fields.Float(string='Longitude', digits=(10, 7))
|
|
distance = fields.Float(
|
|
string='Distance (m)',
|
|
digits=(10, 2),
|
|
help="Distance from location center in meters.",
|
|
)
|
|
source = fields.Selection(
|
|
[
|
|
('portal', 'Portal'),
|
|
('portal_fab', 'Portal FAB'),
|
|
('systray', 'Systray'),
|
|
('backend_fab', 'Backend FAB'),
|
|
('kiosk', 'Kiosk'),
|
|
('system', 'System (Cron)'),
|
|
('nfc_kiosk', 'NFC Kiosk'),
|
|
],
|
|
string='Source',
|
|
)
|
|
company_id = fields.Many2one(
|
|
'res.company',
|
|
string='Company',
|
|
related='employee_id.company_id',
|
|
store=True,
|
|
)
|
|
attempt_map_url = fields.Char(
|
|
string='Map Preview',
|
|
compute='_compute_attempt_map_url',
|
|
)
|
|
display_name = fields.Char(
|
|
compute='_compute_display_name',
|
|
store=True,
|
|
)
|
|
|
|
LOG_TYPE_LABELS = {
|
|
'clock_in': 'Clock In',
|
|
'clock_out': 'Clock Out',
|
|
'late_clock_in': 'Late Clock-In',
|
|
'early_clock_out': 'Early Clock-Out',
|
|
'outside_geofence': 'Outside Geofence',
|
|
'auto_clock_out': 'Auto Clock-Out',
|
|
'missed_clock_out': 'Missed Clock-Out',
|
|
'absent': 'Absent',
|
|
'leave_request': 'Leave Request',
|
|
'reason_provided': 'Reason Provided',
|
|
'overtime': 'Overtime',
|
|
'correction_request': 'Correction Request',
|
|
'ip_fallback': 'IP Fallback Used',
|
|
'streak_milestone': 'Streak Milestone',
|
|
}
|
|
|
|
@api.depends('latitude', 'longitude')
|
|
def _compute_attempt_map_url(self):
|
|
api_key = self.env['ir.config_parameter'].sudo().get_param(
|
|
'fusion_clock.google_maps_api_key', ''
|
|
)
|
|
for rec in self:
|
|
if rec.latitude and rec.longitude and api_key:
|
|
lat, lng = rec.latitude, rec.longitude
|
|
rec.attempt_map_url = (
|
|
f"https://maps.googleapis.com/maps/api/staticmap?"
|
|
f"center={lat},{lng}&zoom=16&size=600x250&maptype=roadmap"
|
|
f"&markers=color:red%7C{lat},{lng}"
|
|
f"&key={api_key}"
|
|
)
|
|
else:
|
|
rec.attempt_map_url = False
|
|
|
|
@api.depends('employee_id', 'log_type', 'log_date')
|
|
def _compute_display_name(self):
|
|
for rec in self:
|
|
emp = rec.employee_id.name or ''
|
|
ltype = self.LOG_TYPE_LABELS.get(rec.log_type, rec.log_type or '')
|
|
date_str = rec.log_date.strftime('%Y-%m-%d %H:%M') if rec.log_date else ''
|
|
rec.display_name = f"{emp} - {ltype} ({date_str})"
|