Initial commit
This commit is contained in:
182
fusion_clock/models/clock_location.py
Normal file
182
fusion_clock/models/clock_location.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionClockLocation(models.Model):
|
||||
_name = 'fusion.clock.location'
|
||||
_description = 'Clock-In Location'
|
||||
_order = 'sequence, name'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(string='Location Name', required=True)
|
||||
address = fields.Char(string='Address')
|
||||
latitude = fields.Float(string='Latitude', digits=(10, 7))
|
||||
longitude = fields.Float(string='Longitude', digits=(10, 7))
|
||||
radius = fields.Integer(
|
||||
string='Radius (meters)',
|
||||
default=100,
|
||||
help="Geofence radius in meters. Employees must be within this distance to clock in/out.",
|
||||
)
|
||||
sequence = fields.Integer(default=10)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
required=True,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
employee_ids = fields.Many2many(
|
||||
'hr.employee',
|
||||
'fusion_clock_location_employee_rel',
|
||||
'location_id',
|
||||
'employee_id',
|
||||
string='Allowed Employees',
|
||||
)
|
||||
all_employees = fields.Boolean(
|
||||
string='All Employees',
|
||||
default=True,
|
||||
help="If checked, all employees can clock in/out at this location.",
|
||||
)
|
||||
color = fields.Char(string='Map Color', default='#10B981')
|
||||
note = fields.Text(string='Notes')
|
||||
timezone = fields.Selection(
|
||||
'_tz_get',
|
||||
string='Timezone',
|
||||
default=lambda self: self.env.user.tz or 'UTC',
|
||||
)
|
||||
|
||||
# Computed
|
||||
attendance_count = fields.Integer(
|
||||
string='Total Attendances',
|
||||
compute='_compute_attendance_count',
|
||||
)
|
||||
map_url = fields.Char(
|
||||
string='Map Preview URL',
|
||||
compute='_compute_map_url',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _tz_get(self):
|
||||
import pytz
|
||||
return [(tz, tz) for tz in sorted(pytz.all_timezones_set)]
|
||||
|
||||
@api.depends('latitude', 'longitude', 'radius')
|
||||
def _compute_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:
|
||||
rec.map_url = (
|
||||
f"https://maps.googleapis.com/maps/api/staticmap?"
|
||||
f"center={rec.latitude},{rec.longitude}&zoom=16&size=600x300&maptype=roadmap"
|
||||
f"&markers=color:green%7C{rec.latitude},{rec.longitude}"
|
||||
f"&key={api_key}"
|
||||
)
|
||||
else:
|
||||
rec.map_url = False
|
||||
|
||||
def _compute_attendance_count(self):
|
||||
for rec in self:
|
||||
rec.attendance_count = self.env['hr.attendance'].search_count([
|
||||
('x_fclk_location_id', '=', rec.id),
|
||||
])
|
||||
|
||||
def action_geocode_address(self):
|
||||
"""Geocode the address to get lat/lng using Google Geocoding API.
|
||||
Falls back to Nominatim (OpenStreetMap) if Google fails.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.address:
|
||||
raise UserError(_("Please enter an address first."))
|
||||
|
||||
# Try Google first
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('fusion_clock.google_maps_api_key', '')
|
||||
if api_key:
|
||||
try:
|
||||
url = 'https://maps.googleapis.com/maps/api/geocode/json'
|
||||
params = {'address': self.address, 'key': api_key}
|
||||
response = requests.get(url, params=params, timeout=10)
|
||||
data = response.json()
|
||||
|
||||
if data.get('status') == 'OK' and data.get('results'):
|
||||
location = data['results'][0]['geometry']['location']
|
||||
self.write({
|
||||
'latitude': location['lat'],
|
||||
'longitude': location['lng'],
|
||||
})
|
||||
formatted = data['results'][0].get('formatted_address', '')
|
||||
if formatted:
|
||||
self.address = formatted
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Geocoding Successful'),
|
||||
'message': _('Location set to: %s, %s') % (location['lat'], location['lng']),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
elif data.get('status') == 'REQUEST_DENIED':
|
||||
_logger.warning("Google Geocoding API denied. Enable the Geocoding API in Google Cloud Console. Falling back to Nominatim.")
|
||||
else:
|
||||
_logger.warning("Google geocoding returned: %s. Trying Nominatim fallback.", data.get('status'))
|
||||
except requests.exceptions.RequestException as e:
|
||||
_logger.warning("Google geocoding network error: %s. Trying Nominatim fallback.", e)
|
||||
|
||||
# Fallback: Nominatim (OpenStreetMap) - free, no API key needed
|
||||
try:
|
||||
url = 'https://nominatim.openstreetmap.org/search'
|
||||
params = {
|
||||
'q': self.address,
|
||||
'format': 'json',
|
||||
'limit': 1,
|
||||
}
|
||||
headers = {'User-Agent': 'FusionClock/1.0 (Odoo Module)'}
|
||||
response = requests.get(url, params=params, headers=headers, timeout=10)
|
||||
data = response.json()
|
||||
|
||||
if data and len(data) > 0:
|
||||
lat = float(data[0]['lat'])
|
||||
lng = float(data[0]['lon'])
|
||||
display_name = data[0].get('display_name', '')
|
||||
self.write({
|
||||
'latitude': lat,
|
||||
'longitude': lng,
|
||||
})
|
||||
if display_name:
|
||||
self.address = display_name
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Geocoding Successful (via OpenStreetMap)'),
|
||||
'message': _('Location set to: %s, %s') % (lat, lng),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
else:
|
||||
raise UserError(_("Could not geocode address. No results found. Try a more specific address."))
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise UserError(_("Network error during geocoding: %s") % str(e))
|
||||
|
||||
def action_view_attendances(self):
|
||||
"""Open attendance records for this location."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Attendances at %s') % self.name,
|
||||
'res_model': 'hr.attendance',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('x_fclk_location_id', '=', self.id)],
|
||||
'context': {'default_x_fclk_location_id': self.id},
|
||||
}
|
||||
Reference in New Issue
Block a user