This commit is contained in:
gsinghpal
2026-03-01 14:42:49 -05:00
parent b925766966
commit a3e85a23ef
28 changed files with 2283 additions and 195 deletions

View File

@@ -81,7 +81,6 @@ Integrates natively with Odoo's hr.attendance module for full payroll compatibil
'web.assets_frontend': [
'fusion_clock/static/src/css/portal_clock.css',
'fusion_clock/static/src/js/fusion_clock_portal.js',
'fusion_clock/static/src/js/fusion_clock_portal_fab.js',
'fusion_clock/static/src/js/fusion_clock_kiosk.js',
],
'web.assets_backend': [

View File

@@ -57,29 +57,34 @@ class FusionClockAPI(http.Controller):
if not locations:
return False, 0, 'no_locations', 'gps'
gps_available = latitude != 0 or longitude != 0
geocoded = locations.filtered(lambda l: l.latitude and l.longitude
and not (l.latitude == 0.0 and l.longitude == 0.0))
if not geocoded:
return False, 0, 'no_geocoded', 'gps'
nearest_location = False
nearest_distance = float('inf')
for loc in geocoded:
dist = haversine_distance(latitude, longitude, loc.latitude, loc.longitude)
if dist <= loc.radius:
return loc, dist, None, 'gps'
if dist < nearest_distance:
nearest_distance = dist
nearest_location = loc
if gps_available and geocoded:
for loc in geocoded:
dist = haversine_distance(latitude, longitude, loc.latitude, loc.longitude)
if dist <= loc.radius:
return loc, dist, None, 'gps'
if dist < nearest_distance:
nearest_distance = dist
# IP fallback check
# IP fallback -- try when GPS is unavailable OR GPS is outside all geofences
ICP = request.env['ir.config_parameter'].sudo()
if ICP.get_param('fusion_clock.enable_ip_fallback', 'False') == 'True' and client_ip:
if client_ip:
for loc in locations:
if loc.check_ip_whitelist(client_ip):
return loc, 0, None, 'ip'
if not gps_available:
return False, 0, 'gps_unavailable', 'gps'
if not geocoded:
return False, 0, 'no_geocoded', 'gps'
return False, nearest_distance, 'outside', 'gps'
def _location_error_message(self, error_type, distance=0):
@@ -87,6 +92,8 @@ class FusionClockAPI(http.Controller):
return 'No clock locations configured. Ask your manager to set up locations in Fusion Clock > Locations.'
elif error_type == 'no_geocoded':
return 'Clock locations exist but have no GPS coordinates. Ask your manager to geocode them.'
elif error_type == 'gps_unavailable':
return 'Could not determine your location. Please enable GPS/location services in your browser and device settings, then try again.'
else:
dist_str = f"{int(distance)}m" if distance < 1000 else f"{distance/1000:.1f}km"
return f'You are {dist_str} away from the nearest clock location. Please clock in/out within the allowed area.'

View File

@@ -125,6 +125,53 @@ class FusionClockLocation(models.Model):
return False
return False
def action_detect_ip(self):
"""Detect the current public IP and append it to the whitelist."""
self.ensure_one()
try:
resp = requests.get('https://ipapi.co/json/', timeout=10)
data = resp.json()
ip = data.get('ip', '')
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))
existing = (self.ip_whitelist or '').strip()
existing_lines = [l.strip() for l in existing.split('\n') if l.strip()] if existing else []
if ip in existing_lines:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Already Whitelisted'),
'message': _('%s is already in the whitelist.') % ip,
'type': 'warning',
'sticky': False,
},
}
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 += ")"
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('IP Detected & Added'),
'message': _('Added %s to the whitelist.') % detail,
'type': 'success',
'sticky': False,
},
}
def action_geocode_address(self):
"""Geocode the address to get lat/lng using Google Geocoding API.
Falls back to Nominatim (OpenStreetMap) if Google fails.

View File

@@ -180,7 +180,21 @@ export class FusionClockKiosk extends Interaction {
lat = pos.coords.latitude;
lng = pos.coords.longitude;
} catch {
// GPS unavailable on kiosk device
// Native GPS unavailable -- try IP geolocation
}
if (lat === 0 && lng === 0) {
try {
const ipResp = await fetch("https://ipapi.co/json/");
if (ipResp.ok) {
const ipData = await ipResp.json();
if (ipData.latitude && ipData.longitude) {
lat = ipData.latitude;
lng = ipData.longitude;
}
}
} catch {
// IP geolocation also unavailable
}
}
const resp = await fetch("/fusion_clock/kiosk/clock", {

View File

@@ -199,15 +199,22 @@ export class FusionClockPortal extends Interaction {
(pos) => {
this._performClockAction(pos.coords.latitude, pos.coords.longitude, pos.coords.accuracy);
},
(err) => {
async () => {
let lat = 0, lng = 0;
try {
const ipResp = await fetch("https://ipapi.co/json/");
if (ipResp.ok) {
const ipData = await ipResp.json();
if (ipData.latitude && ipData.longitude) {
lat = ipData.latitude;
lng = ipData.longitude;
}
}
} catch {
// IP geolocation also unavailable
}
this._hideGPSOverlay();
let msg = "Could not get your location. ";
if (err.code === 1) msg += "Please allow location access.";
else if (err.code === 2) msg += "Location unavailable.";
else if (err.code === 3) msg += "Location request timed out.";
this._showToast(msg, "error");
this._shakeButton();
btn.disabled = false;
this._performClockAction(lat, lng, lat ? 5000 : 0);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
);

View File

@@ -398,10 +398,23 @@ export class FusionClockPortalFAB extends Interaction {
lat = pos.coords.latitude;
lng = pos.coords.longitude;
acc = pos.coords.accuracy;
} catch (geoErr) {
this._showError("Location access denied. Please enable GPS.");
if (this.actionBtn) this.actionBtn.disabled = false;
return;
} catch {
// Native GPS unavailable (common on desktops) -- try IP geolocation
}
}
if (lat === 0 && lng === 0) {
try {
const ipResp = await fetch("https://ipapi.co/json/");
if (ipResp.ok) {
const ipData = await ipResp.json();
if (ipData.latitude && ipData.longitude) {
lat = ipData.latitude;
lng = ipData.longitude;
acc = 5000;
}
}
} catch {
// IP geolocation also unavailable
}
}

View File

@@ -134,10 +134,23 @@ export class FusionClockFAB extends Component {
lat = pos.coords.latitude;
lng = pos.coords.longitude;
acc = pos.coords.accuracy;
} catch (geoErr) {
this.state.error = "Location access denied.";
this.state.loading = false;
return;
} catch {
// Native GPS unavailable (common on desktops) -- try IP geolocation
}
}
if (lat === 0 && lng === 0) {
try {
const ipResp = await fetch("https://ipapi.co/json/");
if (ipResp.ok) {
const ipData = await ipResp.json();
if (ipData.latitude && ipData.longitude) {
lat = ipData.latitude;
lng = ipData.longitude;
acc = 5000;
}
}
} catch {
// IP geolocation also unavailable
}
}

View File

@@ -52,6 +52,12 @@
</group>
<group>
<group string="IP Whitelist">
<div colspan="2">
<button name="action_detect_ip" type="object"
string="Detect My IP" class="btn-secondary mb-2"
icon="fa-crosshairs"
title="Detect your current public IP and add it to the whitelist"/>
</div>
<field name="ip_whitelist" nolabel="1" colspan="2"
placeholder="One IP or CIDR per line, e.g.&#10;192.168.1.0/24&#10;10.0.0.100"/>
</group>