changes
This commit is contained in:
@@ -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': [
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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. 192.168.1.0/24 10.0.0.100"/>
|
||||
</group>
|
||||
|
||||
Reference in New Issue
Block a user