diff --git a/app.py b/app.py index 1f59116..cbd5133 100644 --- a/app.py +++ b/app.py @@ -5737,16 +5737,24 @@ def get_miss_punch_details(employee_id): # Convert to the format expected by the calculator converted_records = [] - for record in attendance_records: + for record in records: + # Get distance from the TimeAttendance record + distance_value = getattr(record, 'distance', None) + converted_record = type('Record', (), { 'id': record.id, 'employee_id': str(record.employee_id), - 'check_in_date': record.check_in_date, - 'check_in_time': record.check_in_time, - 'location_name': record.location_name or 'Unknown Location', - 'latitude': record.latitude, - 'longitude': record.longitude, - 'qr_code': record.qr_code + 'check_in_date': record.attendance_date, + 'check_in_time': record.attendance_time, + 'location_name': record.location_name, + 'latitude': None, + 'longitude': None, + 'distance': distance_value, # ADD THIS LINE + 'qr_code': type('QRCode', (), { + 'location': record.location_name, + 'location_address': record.recorded_address or '', + 'project': None + })() })() converted_records.append(converted_record) @@ -7067,7 +7075,7 @@ def download_import_template(): # Define headers headers = ['ID', 'Name', 'Platform', 'Date', 'Time', 'Location Name', - 'Action Description', 'Event Description', 'Recorded Address'] + 'Action Description', 'Event Description', 'Recorded Address', 'Distance'] # Style headers header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") @@ -7083,11 +7091,9 @@ def download_import_template(): # Add sample data rows sample_data = [ ['12345', 'John Doe', 'iPhone - iOS', '2025-10-06', '09:00:00', - 'HQ Suite 210', 'Check In', 'Morning Entry', '123 Main Street'], - ['12345', 'John Doe', 'iPhone - iOS', '2025-10-06', '17:30:00', - 'HQ Suite 210', 'Check Out', 'Evening Exit', '123 Main Street'], + 'HQ Suite 210', 'Check In', 'Main Office', '123 Main St', '0.125'], ['67890', 'Jane Smith', 'Android', '2025-10-06', '08:45:00', - 'Branch Office', 'Check In', 'Morning Entry', '456 Oak Avenue'], + 'Branch Office', 'Check In', 'Morning Entry', '456 Oak Avenue', '0.250'], ] for row_num, row_data in enumerate(sample_data, 2): @@ -7124,6 +7130,7 @@ def download_import_template(): ["- Platform: Device platform (e.g., iPhone - iOS, Android)"], ["- Event Description: Additional event details"], ["- Recorded Address: Physical address where attendance was recorded"], + ["- Distance: Distance in miles between Building and Recorded Address (optional)"], [""], ["Important Notes:"], ["- Do not modify the header row"], @@ -7575,8 +7582,8 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ '', '', getattr(record.qr_code, 'location_address', '') or '', - record.location_name, - '', + getattr(record.qr_code, 'location_address', '') or '', + getattr(record, 'distance', None) or '', 'No' ] @@ -7605,8 +7612,8 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ '', '', getattr(first_record.qr_code, 'location_address', '') or '', - first_record.location_name, - '', + getattr(first_record.qr_code, 'location_address', '') or '', + getattr(first_record, 'distance', None) or '', 'No' ] @@ -7633,8 +7640,8 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ '', '', getattr(last_record.qr_code, 'location_address', '') or '', - last_record.location_name, - '', + getattr(last_record.qr_code, 'location_address', '') or '', + getattr(last_record, 'distance', None) or '', 'No' ] @@ -7674,8 +7681,8 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ '', '', getattr(check_in_record.qr_code, 'location_address', '') or '', - check_in_record.location_name, - '', + getattr(check_in_record.qr_code, 'location_address', '') or '', + getattr(check_in_record, 'distance', None) or '', 'No' ] @@ -7722,13 +7729,13 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ out_time, last_record.location_name, '', - 'Missed Punch', # Only THIS unpaired record shows Missed Punch + 'Missed Punch', daily_total_display, # Show Daily Total on last row '', '', getattr(last_record.qr_code, 'location_address', '') or '', - last_record.location_name, - '', + getattr(last_record.qr_code, 'location_address', '') or '', + getattr(last_record, 'distance', None) or '', 'No' ] @@ -7759,8 +7766,8 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ '', '', getattr(start_record.qr_code, 'location_address', '') or '', - start_record.location_name, - '', + getattr(start_record.qr_code, 'location_address', '') or '', + getattr(start_record, 'distance', None) or '', 'No' ] @@ -7784,8 +7791,8 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ '', '', getattr(start_record.qr_code, 'location_address', '') if start_record else '', - start_record.location_name if start_record else '', - '', + getattr(start_record.qr_code, 'location_address', '') if start_record else '', + getattr(start_record, 'distance', None) if start_record else '', 'No' ] diff --git a/models/time_attendance.py b/models/time_attendance.py index 72defb4..5ae9f6d 100644 --- a/models/time_attendance.py +++ b/models/time_attendance.py @@ -36,6 +36,8 @@ class TimeAttendance(base.db.Model): action_description = base.db.Column(base.db.String(100), nullable=False) event_description = base.db.Column(base.db.Text, nullable=True) recorded_address = base.db.Column(base.db.Text, nullable=True) + # Distance/Location accuracy field (in miles) + distance = base.db.Column(base.db.Float, nullable=True) # Import tracking import_batch_id = base.db.Column(base.db.String(36), nullable=True, index=True) diff --git a/templates/time_attendance_import.html b/templates/time_attendance_import.html index a39a743..f54b919 100644 --- a/templates/time_attendance_import.html +++ b/templates/time_attendance_import.html @@ -313,6 +313,7 @@ Action Description Event Description Recorded Address + Distance @@ -326,6 +327,7 @@ Check In Main Office 123 Main St + 0.125 67890 @@ -337,6 +339,7 @@ Check Out Main Office 123 Main St + 0.125 diff --git a/time_attendance_import_service.py b/time_attendance_import_service.py index 0832155..1e3a3f8 100644 --- a/time_attendance_import_service.py +++ b/time_attendance_import_service.py @@ -157,6 +157,63 @@ class TimeAttendanceImportService: # Not a hyperlink formula, return as-is return cell_value if cell_value else None + def _parse_distance_field(self, row) -> Optional[float]: + """ + Parse Distance field from Excel (optional column) + + Args: + row: DataFrame row containing the 'Distance' column + + Returns: + Distance value as float in miles, or None if not present/invalid + """ + # Check if Distance column exists + if 'Distance' not in row.index: + return None + + distance_value = row.get('Distance') + + # Check if value exists and is not NaN + if pd.isna(distance_value): + return None + + # Try to parse as float + try: + # Handle string values + if isinstance(distance_value, str): + distance_str = distance_value.strip() + + # Skip empty strings + if not distance_str or distance_str.lower() in ['', 'n/a', 'na', 'none']: + return None + + # Remove any text like "miles", "mi", "m" + distance_str = distance_str.lower() + distance_str = distance_str.replace('miles', '').replace('mile', '').replace('mi', '').replace('m', '').strip() + + # Parse the number + distance_float = float(distance_str) + else: + # Already a number + distance_float = float(distance_value) + + # Validate reasonable range (0 to 100 miles) + if distance_float < 0: + if self.logger: + self.logger.logger.warning(f"Negative distance value {distance_float} converted to positive") + distance_float = abs(distance_float) + + if distance_float > 100: + if self.logger: + self.logger.logger.warning(f"Distance value {distance_float} exceeds 100 miles, may be invalid") + + return round(distance_float, 4) # Round to 4 decimal places + + except (ValueError, TypeError) as e: + if self.logger: + self.logger.logger.debug(f"Could not parse distance value '{distance_value}': {e}") + return None + def _process_recorded_address(self, row): """ Process recorded address field, handling Excel HYPERLINK formulas @@ -574,6 +631,7 @@ class TimeAttendanceImportService: 'action_description': str(row['Action Description']).strip(), 'event_description': str(row.get('Event Description', '')).strip() if pd.notna(row.get('Event Description')) else None, 'recorded_address': self._process_recorded_address(row), + 'distance': self._parse_distance_field(row), } # Check for duplicates @@ -805,6 +863,7 @@ class TimeAttendanceImportService: 'action_description': record.action_description, 'event_description': record.event_description, 'recorded_address': record.recorded_address, + 'distance': getattr(record, 'distance', None), 'import_batch_id': record.import_batch_id, 'import_date': record.import_date, 'import_source': record.import_source