From 34f63454869135824a487123dcf017cbe6aebd07 Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Sat, 14 Mar 2026 02:24:14 +0000 Subject: [PATCH] Add server file path import mode to bypass upload size limits --- .../wizards/wt_import_timeline_wizard.py | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/work_trace/wizards/wt_import_timeline_wizard.py b/work_trace/wizards/wt_import_timeline_wizard.py index 40d94ea1..2837ca5d 100644 --- a/work_trace/wizards/wt_import_timeline_wizard.py +++ b/work_trace/wizards/wt_import_timeline_wizard.py @@ -2,6 +2,7 @@ from odoo import models, fields, api, _ from odoo.exceptions import UserError import json import base64 +import os import re from datetime import datetime, timedelta from math import radians, sin, cos, sqrt, atan2 @@ -41,7 +42,6 @@ def _get_travel_mode(activity_type): def _parse_latlng(latlng_str): - """Parse coordinate string like '30.0381046 deg, -95.5899101 deg' handling encoding issues.""" nums = re.findall(r'-?\d+\.\d+', latlng_str) if len(nums) >= 2: return float(nums[0]), float(nums[1]) @@ -49,7 +49,6 @@ def _parse_latlng(latlng_str): def _parse_ts(ts_str): - """Parse ISO 8601 timestamp to naive datetime.""" if not ts_str: return None try: @@ -62,17 +61,25 @@ class WtImportTimelineWizard(models.TransientModel): _name = 'wt.import.timeline.wizard' _description = 'Import Google Timeline' - timeline_file = fields.Binary(string='Timeline JSON File', required=True) + import_mode = fields.Selection([ + ('upload', 'Upload File'), + ('server_path', 'Server File Path'), + ], string='Import Mode', default='upload', required=True) + + timeline_file = fields.Binary(string='Timeline JSON File') timeline_filename = fields.Char(string='Filename') + server_file_path = fields.Char( + string='Server File Path', + help='Full path to Timeline.json on the Odoo server (bypasses upload size limits)' + ) min_stop_minutes = fields.Integer( string='Minimum Stop Duration (minutes)', default=5, - help='Ignore stops shorter than this duration' ) proximity_meters = fields.Integer( string='Location Proximity (meters)', default=200, - help='For raw signal files only: GPS positions within this distance are grouped as one location' + help='For Timeline Edits.json only: GPS positions within this distance are grouped as one location' ) geocode = fields.Boolean( string='Resolve Addresses via OpenStreetMap', @@ -81,11 +88,27 @@ class WtImportTimelineWizard(models.TransientModel): def action_import(self): self.ensure_one() - try: - raw = base64.b64decode(self.timeline_file) - data = json.loads(raw) - except Exception as e: - raise UserError(_('Invalid JSON file: %s') % str(e)) + + # Load data from file upload or server path + if self.import_mode == 'server_path': + if not self.server_file_path: + raise UserError(_('Please enter the server file path.')) + path = self.server_file_path.strip() + if not os.path.exists(path): + raise UserError(_('File not found on server: %s') % path) + try: + with open(path, 'r', encoding='utf-8') as f: + data = json.load(f) + except Exception as e: + raise UserError(_('Error reading file: %s') % str(e)) + else: + if not self.timeline_file: + raise UserError(_('Please upload a Timeline JSON file.')) + try: + raw = base64.b64decode(self.timeline_file) + data = json.loads(raw) + except Exception as e: + raise UserError(_('Invalid JSON file: %s') % str(e)) # Auto-detect format if 'semanticSegments' in data: @@ -96,7 +119,7 @@ class WtImportTimelineWizard(models.TransientModel): raise UserError(_('Unrecognized format. Expected Timeline.json (semanticSegments) or Timeline Edits.json (timelineEdits).')) if not stops: - raise UserError(_('No location stops found in the uploaded file.')) + raise UserError(_('No location stops found in the file.')) # Filter by minimum stop duration min_secs = self.min_stop_minutes * 60 @@ -109,16 +132,14 @@ class WtImportTimelineWizard(models.TransientModel): stops.sort(key=lambda s: s['arrived_at']) - # Compute distances and travel times between consecutive stops + # Compute distances and travel times for i, stop in enumerate(stops): if i > 0: prev = stops[i - 1] - if prev.get('lat') and stop.get('lat'): - stop['distance_from_previous'] = _haversine_miles( - prev['lat'], prev['lng'], stop['lat'], stop['lng'] - ) - else: - stop['distance_from_previous'] = 0.0 + stop['distance_from_previous'] = ( + _haversine_miles(prev['lat'], prev['lng'], stop['lat'], stop['lng']) + if prev.get('lat') and stop.get('lat') else 0.0 + ) travel_delta = stop['arrived_at'] - prev['departed_at'] stop['travel_time_from_previous'] = max(travel_delta.total_seconds() / 3600, 0.0) else: @@ -140,7 +161,6 @@ class WtImportTimelineWizard(models.TransientModel): if arrived_str in existing: skipped += 1 continue - log = LocationLog.create({ 'date': stop['arrived_at'].date(), 'arrived_at': stop['arrived_at'], @@ -174,31 +194,22 @@ class WtImportTimelineWizard(models.TransientModel): } def _parse_semantic_timeline(self, data): - """ - Parse Timeline.json semanticSegments format. - Only 'visit' segments are location stops. - 'timelinePath' segments are travel (ignored — distance calculated from stop coords). - """ stops = [] for seg in data.get('semanticSegments', []): visit = seg.get('visit') if not visit: continue - start_ts = _parse_ts(seg.get('startTime')) end_ts = _parse_ts(seg.get('endTime')) if not start_ts or not end_ts: continue - candidate = visit.get('topCandidate', {}) semantic_type = candidate.get('semanticType', '') latlng_str = candidate.get('placeLocation', {}).get('latLng', '') lat, lng = _parse_latlng(latlng_str) if latlng_str else (None, None) - category = SEMANTIC_TYPE_CATEGORY.get(semantic_type, '') if not category and semantic_type: category = semantic_type.replace('_', ' ').title() - stops.append({ 'arrived_at': start_ts, 'departed_at': end_ts, @@ -208,14 +219,11 @@ class WtImportTimelineWizard(models.TransientModel): 'category': category, 'travel_mode': 'unknown', }) - return stops def _parse_raw_timeline(self, data, proximity_meters=200): - """Parse Timeline Edits.json raw signal format using proximity clustering.""" positions = [] activities = [] - for entry in data.get('timelineEdits', []): raw = entry.get('rawSignal', {}).get('signal', {}) if 'position' in raw: @@ -251,7 +259,6 @@ class WtImportTimelineWizard(models.TransientModel): stops = [] current_cluster = [positions[0]] - for pos in positions[1:]: prev = current_cluster[-1] if _distance_meters(prev['lat'], prev['lng'], pos['lat'], pos['lng']) <= proximity_meters: