Updated export with special working hours (SP, PW, PT) that works fine
This commit is contained in:
@@ -8470,11 +8470,13 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
|||||||
converted_records
|
converted_records
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get employee names
|
# Get employee names - map BASE employee IDs to names for consolidated display
|
||||||
|
from working_hours_calculator import parse_employee_id_for_work_type
|
||||||
employee_names = {}
|
employee_names = {}
|
||||||
for record in records:
|
for record in records:
|
||||||
if record.employee_id not in employee_names:
|
base_id, _ = parse_employee_id_for_work_type(str(record.employee_id))
|
||||||
employee_names[record.employee_id] = record.employee_name
|
if base_id not in employee_names:
|
||||||
|
employee_names[base_id] = record.employee_name
|
||||||
|
|
||||||
# Setup styles
|
# Setup styles
|
||||||
header_font = Font(name='Arial', size=11, bold=True, color='FFFFFF')
|
header_font = Font(name='Arial', size=11, bold=True, color='FFFFFF')
|
||||||
@@ -8607,7 +8609,15 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
|||||||
|
|
||||||
# Group records by date AND location for separate rows per location
|
# Group records by date AND location for separate rows per location
|
||||||
daily_location_data = {}
|
daily_location_data = {}
|
||||||
employee_records = [r for r in converted_records if str(r.employee_id) == employee_id]
|
# Import parse function to match base employee ID with all variants (SP, PW, PT)
|
||||||
|
from working_hours_calculator import parse_employee_id_for_work_type
|
||||||
|
|
||||||
|
# Filter records where the BASE employee ID matches (includes 1234, 1234 SP, 1234 PW, 1234 PT)
|
||||||
|
employee_records = []
|
||||||
|
for r in converted_records:
|
||||||
|
record_base_id, _ = parse_employee_id_for_work_type(str(r.employee_id))
|
||||||
|
if record_base_id == employee_id:
|
||||||
|
employee_records.append(r)
|
||||||
|
|
||||||
for record in employee_records:
|
for record in employee_records:
|
||||||
date_key = record.check_in_date.strftime('%Y-%m-%d')
|
date_key = record.check_in_date.strftime('%Y-%m-%d')
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class SingleCheckInCalculator:
|
|||||||
daily_hours = {}
|
daily_hours = {}
|
||||||
weekly_totals = []
|
weekly_totals = []
|
||||||
current_date = start_date
|
current_date = start_date
|
||||||
current_week_hours = {'regular': [], 'SP': [], 'PW': []}
|
current_week_hours = {'regular': [], 'SP': [], 'PW': [], 'PT': []}
|
||||||
|
|
||||||
while current_date <= end_date:
|
while current_date <= end_date:
|
||||||
date_key = current_date.strftime('%Y-%m-%d')
|
date_key = current_date.strftime('%Y-%m-%d')
|
||||||
|
|||||||
+323
-137
@@ -10,6 +10,7 @@ based on the Java files provided. It handles:
|
|||||||
- Record pairing (check-in/check-out)
|
- Record pairing (check-in/check-out)
|
||||||
- Missing punch detection
|
- Missing punch detection
|
||||||
- Quarter-hour rounding
|
- Quarter-hour rounding
|
||||||
|
- SP/PW/PT (Special Project/Periodic Work/Part-Time) support with consolidation
|
||||||
|
|
||||||
Based on the Java classes:
|
Based on the Java classes:
|
||||||
- DailyTimeCalculator.java
|
- DailyTimeCalculator.java
|
||||||
@@ -21,12 +22,52 @@ from datetime import datetime, timedelta, time
|
|||||||
from typing import List, Dict, Optional, Tuple, Any
|
from typing import List, Dict, Optional, Tuple, Any
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
from logger_handler import log_database_operations
|
from logger_handler import log_database_operations
|
||||||
|
|
||||||
# Constants from Java implementation
|
# Constants from Java implementation
|
||||||
RECORD_GROUPING_MAX_MINUTES = 60 * 6 # 6 hours
|
RECORD_GROUPING_MAX_MINUTES = 60 * 6 # 6 hours
|
||||||
MAX_REGULAR_TIME_MINUTES = 60 * 40 # 40 hours per week
|
MAX_REGULAR_TIME_MINUTES = 60 * 40 # 40 hours per week
|
||||||
|
|
||||||
|
|
||||||
|
def parse_employee_id_for_work_type(employee_id: str) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Parse employee ID to extract base ID and work type (supports SP, PW, PT)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
employee_id: Employee ID string (e.g., "1234", "1234 SP", "1234 PW", "1234 PT")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (base_employee_id, work_type)
|
||||||
|
work_type is one of: 'regular', 'SP', 'PW', 'PT'
|
||||||
|
"""
|
||||||
|
if not employee_id:
|
||||||
|
return str(employee_id), 'regular'
|
||||||
|
|
||||||
|
employee_id_clean = str(employee_id).strip().upper()
|
||||||
|
|
||||||
|
# Check for SP (Special Project)
|
||||||
|
sp_pattern = r'^(\d+)\s*SP$'
|
||||||
|
sp_match = re.match(sp_pattern, employee_id_clean)
|
||||||
|
if sp_match:
|
||||||
|
return sp_match.group(1), 'SP'
|
||||||
|
|
||||||
|
# Check for PW (Periodic Work)
|
||||||
|
pw_pattern = r'^(\d+)\s*PW$'
|
||||||
|
pw_match = re.match(pw_pattern, employee_id_clean)
|
||||||
|
if pw_match:
|
||||||
|
return pw_match.group(1), 'PW'
|
||||||
|
|
||||||
|
# Check for PT (Part-Time)
|
||||||
|
pt_pattern = r'^(\d+)\s*PT$'
|
||||||
|
pt_match = re.match(pt_pattern, employee_id_clean)
|
||||||
|
if pt_match:
|
||||||
|
return pt_match.group(1), 'PT'
|
||||||
|
|
||||||
|
# Default to regular work
|
||||||
|
return employee_id_clean, 'regular'
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AttendanceRecord:
|
class AttendanceRecord:
|
||||||
"""Represents a single attendance record"""
|
"""Represents a single attendance record"""
|
||||||
@@ -37,12 +78,14 @@ class AttendanceRecord:
|
|||||||
location_name: str
|
location_name: str
|
||||||
record_type: str = 'check_in' # 'check_in' or 'check_out'
|
record_type: str = 'check_in' # 'check_in' or 'check_out'
|
||||||
timestamp: datetime = None
|
timestamp: datetime = None
|
||||||
|
action_description: str = ''
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if self.timestamp is None:
|
if self.timestamp is None:
|
||||||
# Combine date and time for timestamp
|
# Combine date and time for timestamp
|
||||||
self.timestamp = datetime.combine(self.check_in_date, self.check_in_time)
|
self.timestamp = datetime.combine(self.check_in_date, self.check_in_time)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RecordPair:
|
class RecordPair:
|
||||||
"""Represents a paired check-in/check-out record"""
|
"""Represents a paired check-in/check-out record"""
|
||||||
@@ -69,6 +112,7 @@ class RecordPair:
|
|||||||
duration = self.check_out.timestamp - self.check_in.timestamp
|
duration = self.check_out.timestamp - self.check_in.timestamp
|
||||||
return int(duration.total_seconds() / 60)
|
return int(duration.total_seconds() / 60)
|
||||||
|
|
||||||
|
|
||||||
class TimeCalculator:
|
class TimeCalculator:
|
||||||
"""Base time calculator with rounding functionality"""
|
"""Base time calculator with rounding functionality"""
|
||||||
|
|
||||||
@@ -81,6 +125,7 @@ class TimeCalculator:
|
|||||||
# Round to nearest 15-minute interval
|
# Round to nearest 15-minute interval
|
||||||
return round(minutes / 15) * 15
|
return round(minutes / 15) * 15
|
||||||
|
|
||||||
|
|
||||||
class DailyTimeCalculator(TimeCalculator):
|
class DailyTimeCalculator(TimeCalculator):
|
||||||
"""Calculate daily working hours with travel time options"""
|
"""Calculate daily working hours with travel time options"""
|
||||||
|
|
||||||
@@ -103,6 +148,7 @@ class DailyTimeCalculator(TimeCalculator):
|
|||||||
|
|
||||||
return self.round_time_to_nearest_quarter_hour(minute_total)
|
return self.round_time_to_nearest_quarter_hour(minute_total)
|
||||||
|
|
||||||
|
|
||||||
class WeeklyTimeCalculator(TimeCalculator):
|
class WeeklyTimeCalculator(TimeCalculator):
|
||||||
"""Calculate weekly regular and overtime hours"""
|
"""Calculate weekly regular and overtime hours"""
|
||||||
|
|
||||||
@@ -122,6 +168,8 @@ class WeeklyTimeCalculator(TimeCalculator):
|
|||||||
|
|
||||||
for daily_calc in self.daily_calculators:
|
for daily_calc in self.daily_calculators:
|
||||||
daily_minutes = daily_calc.get_minutes_total_exclude_travel_time()
|
daily_minutes = daily_calc.get_minutes_total_exclude_travel_time()
|
||||||
|
if daily_minutes > 0:
|
||||||
|
self.total_minutes += daily_minutes
|
||||||
|
|
||||||
# Calculate regular and overtime
|
# Calculate regular and overtime
|
||||||
if self.total_minutes > MAX_REGULAR_TIME_MINUTES:
|
if self.total_minutes > MAX_REGULAR_TIME_MINUTES:
|
||||||
@@ -146,66 +194,53 @@ class WeeklyTimeCalculator(TimeCalculator):
|
|||||||
"""Get overtime hours as decimal"""
|
"""Get overtime hours as decimal"""
|
||||||
return self.overtime_minutes / 60.0
|
return self.overtime_minutes / 60.0
|
||||||
|
|
||||||
|
|
||||||
class RecordPairBuilder:
|
class RecordPairBuilder:
|
||||||
"""Build record pairs from attendance records"""
|
"""Builds record pairs from attendance records"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_pairs_from_records(records: List[AttendanceRecord]) -> List[RecordPair]:
|
def build_pairs_from_records(records: List[AttendanceRecord]) -> List[RecordPair]:
|
||||||
"""
|
"""
|
||||||
Build check-in/check-out pairs from attendance records with missed punch handling
|
Build check-in/check-out pairs from a list of attendance records
|
||||||
|
|
||||||
Pairing Rules:
|
Args:
|
||||||
- Normal: IN - OUT - IN - OUT (2 pairs)
|
records: List of AttendanceRecord objects, should be for a single day
|
||||||
- Missed punch IN-IN-OUT: First IN to OUT (1 pair, marked as missed punch)
|
|
||||||
- Missed punch IN-OUT-OUT: IN to last OUT (1 pair, marked as missed punch)
|
Returns:
|
||||||
- General: If next to first IN isn't OUT, or before OUT isn't IN,
|
List of RecordPair objects
|
||||||
pair from first IN to last OUT
|
|
||||||
"""
|
"""
|
||||||
if not records:
|
if not records:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Sort records by timestamp
|
# Sort by timestamp
|
||||||
sorted_records = sorted(records, key=lambda r: r.timestamp)
|
sorted_records = sorted(records, key=lambda r: r.timestamp)
|
||||||
pairs = []
|
|
||||||
|
|
||||||
|
pairs = []
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
while i < len(sorted_records):
|
while i < len(sorted_records):
|
||||||
current_record = sorted_records[i]
|
current_record = sorted_records[i]
|
||||||
|
|
||||||
# Determine if current record is check-in or check-out
|
# Check if this is a check-in
|
||||||
is_check_in = current_record.record_type == 'check_in'
|
if current_record.record_type == 'check_in':
|
||||||
|
# Look for matching check-out
|
||||||
if is_check_in:
|
|
||||||
# Found a check-in, now look for matching check-out
|
|
||||||
check_out_record = None
|
check_out_record = None
|
||||||
is_miss_punch = False
|
is_miss_punch = False
|
||||||
j = i + 1
|
j = i + 1
|
||||||
|
|
||||||
# Look ahead for the matching OUT
|
|
||||||
while j < len(sorted_records):
|
while j < len(sorted_records):
|
||||||
next_record = sorted_records[j]
|
next_record = sorted_records[j]
|
||||||
|
|
||||||
if next_record.record_type == 'check_out':
|
if next_record.record_type == 'check_out':
|
||||||
# Found a check-out
|
|
||||||
check_out_record = next_record
|
check_out_record = next_record
|
||||||
|
i = j + 1 # Move past the check-out
|
||||||
# Check if there's a missed punch between IN and OUT
|
|
||||||
# (if j > i+1, there are records in between)
|
|
||||||
if j > i + 1:
|
|
||||||
is_miss_punch = True
|
|
||||||
# Log missed punch detection
|
|
||||||
print(f"⚠️ Missed punch detected: IN at {current_record.timestamp} paired with OUT at {check_out_record.timestamp} (skipped {j-i-1} record(s))")
|
|
||||||
|
|
||||||
i = j + 1 # Move past this OUT
|
|
||||||
break
|
break
|
||||||
else:
|
elif next_record.record_type == 'check_in':
|
||||||
# It's another IN, keep looking
|
# Another IN, keep looking
|
||||||
j += 1
|
j += 1
|
||||||
|
|
||||||
# If no OUT found, it's an incomplete pair (missed punch)
|
# If no OUT found, it's an incomplete pair (missed punch)
|
||||||
if check_out_record is None:
|
if check_out_record is None:
|
||||||
is_miss_punch = True
|
is_miss_punch = True
|
||||||
print(f"⚠️ Incomplete pair: IN at {current_record.timestamp} has no matching OUT")
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
# Create the pair
|
# Create the pair
|
||||||
@@ -218,7 +253,6 @@ class RecordPairBuilder:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# Orphaned check-out (OUT without preceding IN)
|
# Orphaned check-out (OUT without preceding IN)
|
||||||
print(f"⚠️ Orphaned check-out at {current_record.timestamp} (no preceding IN)")
|
|
||||||
pair = RecordPair(
|
pair = RecordPair(
|
||||||
check_in=None,
|
check_in=None,
|
||||||
check_out=current_record,
|
check_out=current_record,
|
||||||
@@ -227,15 +261,16 @@ class RecordPairBuilder:
|
|||||||
pairs.append(pair)
|
pairs.append(pair)
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
# Log pairing summary
|
|
||||||
total_pairs = len(pairs)
|
|
||||||
missed_pairs = sum(1 for p in pairs if p.is_miss_punch)
|
|
||||||
print(f"📊 Pairing complete: {total_pairs} total pairs, {missed_pairs} with missed punches")
|
|
||||||
|
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
class WorkingHoursCalculator:
|
class WorkingHoursCalculator:
|
||||||
"""Main calculator for employee working hours"""
|
"""
|
||||||
|
Main calculator for employee working hours with SP/PW/PT support.
|
||||||
|
|
||||||
|
This calculator consolidates employees by base ID, grouping records for
|
||||||
|
1234, 1234 SP, 1234 PW, 1234 PT under base employee 1234.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
@@ -244,170 +279,321 @@ class WorkingHoursCalculator:
|
|||||||
def calculate_employee_hours(self, employee_id: str, start_date: datetime, end_date: datetime,
|
def calculate_employee_hours(self, employee_id: str, start_date: datetime, end_date: datetime,
|
||||||
attendance_records: List[Dict]) -> Dict[str, Any]:
|
attendance_records: List[Dict]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Calculate working hours for an employee over a date range
|
Calculate working hours for an employee over a date range with SP/PW/PT support.
|
||||||
|
|
||||||
|
This method consolidates all records for base employee ID including SP, PW, PT variants.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
employee_id: Employee ID
|
employee_id: Base employee ID (without SP/PW/PT suffix)
|
||||||
start_date: Start date for calculation
|
start_date: Start date for calculation
|
||||||
end_date: End date for calculation
|
end_date: End date for calculation
|
||||||
attendance_records: List of attendance records from database
|
attendance_records: List of attendance records from database
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing daily and weekly hour calculations
|
Dictionary containing daily and weekly hour calculations with SP/PW/PT breakdown
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Convert database records to AttendanceRecord objects
|
print(f"🔍 Calculating hours for base employee {employee_id} with SP/PW/PT support")
|
||||||
records = []
|
|
||||||
|
# Parse base employee ID
|
||||||
|
base_employee_id, _ = parse_employee_id_for_work_type(employee_id)
|
||||||
|
|
||||||
|
# Filter and categorize records by work type
|
||||||
|
records_by_type = {'regular': [], 'SP': [], 'PW': [], 'PT': []}
|
||||||
|
|
||||||
for record in attendance_records:
|
for record in attendance_records:
|
||||||
# Handle both dictionary and SQLAlchemy object formats
|
try:
|
||||||
if hasattr(record, '__dict__'):
|
# Extract employee_id from record
|
||||||
# SQLAlchemy object
|
if hasattr(record, '__dict__'):
|
||||||
emp_id = str(record.employee_id)
|
record_emp_id = str(getattr(record, 'employee_id', '')).strip()
|
||||||
date_val = record.check_in_date
|
record_date = getattr(record, 'check_in_date', None)
|
||||||
time_val = record.check_in_time
|
record_time = getattr(record, 'check_in_time', None)
|
||||||
location = record.location_name
|
location = getattr(record, 'location_name', 'Unknown Location')
|
||||||
record_id = record.id
|
record_id = getattr(record, 'id', 0)
|
||||||
else:
|
record_type = getattr(record, 'record_type', 'check_in')
|
||||||
# Dictionary
|
|
||||||
emp_id = str(record['employee_id'])
|
|
||||||
date_val = record['check_in_date']
|
|
||||||
time_val = record['check_in_time']
|
|
||||||
location = record['location_name']
|
|
||||||
record_id = record['id']
|
|
||||||
|
|
||||||
if emp_id == employee_id:
|
|
||||||
# Determine record_type from action_description if available
|
|
||||||
record_type = 'check_in' # Default
|
|
||||||
if hasattr(record, 'action_description'):
|
|
||||||
action_desc = getattr(record, 'action_description', '')
|
action_desc = getattr(record, 'action_description', '')
|
||||||
if action_desc:
|
else:
|
||||||
action_lower = str(action_desc).lower()
|
record_emp_id = str(record.get('employee_id', '')).strip()
|
||||||
if 'out' in action_lower or 'checkout' in action_lower:
|
record_date = record.get('check_in_date')
|
||||||
record_type = 'check_out'
|
record_time = record.get('check_in_time')
|
||||||
elif isinstance(record, dict) and 'action_description' in record:
|
location = record.get('location_name', 'Unknown Location')
|
||||||
|
record_id = record.get('id', 0)
|
||||||
|
record_type = record.get('record_type', 'check_in')
|
||||||
action_desc = record.get('action_description', '')
|
action_desc = record.get('action_description', '')
|
||||||
|
|
||||||
|
# Skip invalid records
|
||||||
|
if not record_emp_id or record_date is None or record_time is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse work type from record's employee ID
|
||||||
|
record_base_id, work_type = parse_employee_id_for_work_type(record_emp_id)
|
||||||
|
|
||||||
|
# Only include records for this base employee
|
||||||
|
if record_base_id == base_employee_id:
|
||||||
|
# Determine record type from action_description if available
|
||||||
if action_desc:
|
if action_desc:
|
||||||
action_lower = str(action_desc).lower()
|
action_lower = action_desc.lower()
|
||||||
if 'out' in action_lower or 'checkout' in action_lower:
|
if 'out' in action_lower or 'checkout' in action_lower:
|
||||||
record_type = 'check_out'
|
record_type = 'check_out'
|
||||||
|
else:
|
||||||
|
record_type = 'check_in'
|
||||||
|
|
||||||
att_record = AttendanceRecord(
|
# Create AttendanceRecord object
|
||||||
id=record_id,
|
att_record = AttendanceRecord(
|
||||||
employee_id=emp_id,
|
id=record_id,
|
||||||
check_in_date=date_val,
|
employee_id=record_emp_id,
|
||||||
check_in_time=time_val,
|
check_in_date=record_date if isinstance(record_date, datetime) else datetime.combine(record_date, datetime.min.time()),
|
||||||
location_name=location,
|
check_in_time=record_time,
|
||||||
record_type=record_type # Now properly determined
|
location_name=location,
|
||||||
)
|
record_type=record_type,
|
||||||
records.append(att_record)
|
action_description=action_desc
|
||||||
|
)
|
||||||
|
records_by_type[work_type].append(att_record)
|
||||||
|
|
||||||
# Group records by date
|
except Exception as record_error:
|
||||||
daily_records = {}
|
print(f"⚠️ Error processing record: {record_error}")
|
||||||
for record in records:
|
continue
|
||||||
date_key = record.check_in_date.strftime('%Y-%m-%d')
|
|
||||||
if date_key not in daily_records:
|
|
||||||
daily_records[date_key] = []
|
|
||||||
daily_records[date_key].append(record)
|
|
||||||
|
|
||||||
# Calculate daily hours
|
total_records = sum(len(records_by_type[wt]) for wt in records_by_type)
|
||||||
|
print(f"📊 Found {total_records} records - Regular: {len(records_by_type['regular'])}, SP: {len(records_by_type['SP'])}, PW: {len(records_by_type['PW'])}, PT: {len(records_by_type['PT'])}")
|
||||||
|
|
||||||
|
# Group records by date for each work type
|
||||||
|
daily_records_by_type = {wt: {} for wt in ['regular', 'SP', 'PW', 'PT']}
|
||||||
|
|
||||||
|
for work_type in ['regular', 'SP', 'PW', 'PT']:
|
||||||
|
for record in records_by_type[work_type]:
|
||||||
|
date_key = record.check_in_date.strftime('%Y-%m-%d') if isinstance(record.check_in_date, datetime) else record.check_in_date.strftime('%Y-%m-%d')
|
||||||
|
if date_key not in daily_records_by_type[work_type]:
|
||||||
|
daily_records_by_type[work_type][date_key] = []
|
||||||
|
daily_records_by_type[work_type][date_key].append(record)
|
||||||
|
|
||||||
|
# Calculate daily hours for each work type
|
||||||
daily_hours = {}
|
daily_hours = {}
|
||||||
weekly_calculators = []
|
weekly_hours = []
|
||||||
|
current_week_hours = {'regular': 0, 'SP': 0, 'PW': 0, 'PT': 0}
|
||||||
|
|
||||||
current_date = start_date
|
current_date = start_date
|
||||||
while current_date <= end_date:
|
if isinstance(current_date, datetime):
|
||||||
|
current_date = current_date.date() if hasattr(current_date, 'date') else current_date
|
||||||
|
|
||||||
|
end_date_val = end_date
|
||||||
|
if isinstance(end_date_val, datetime):
|
||||||
|
end_date_val = end_date_val.date() if hasattr(end_date_val, 'date') else end_date_val
|
||||||
|
|
||||||
|
while current_date <= end_date_val:
|
||||||
date_key = current_date.strftime('%Y-%m-%d')
|
date_key = current_date.strftime('%Y-%m-%d')
|
||||||
day_records = daily_records.get(date_key, [])
|
|
||||||
|
|
||||||
daily_calc = DailyTimeCalculator()
|
# Calculate hours for each work type on this day
|
||||||
|
hours_by_type = {}
|
||||||
|
is_miss_punch_by_type = {}
|
||||||
|
records_count_by_type = {}
|
||||||
|
|
||||||
if day_records:
|
for work_type in ['regular', 'SP', 'PW', 'PT']:
|
||||||
# Build record pairs
|
day_records = daily_records_by_type[work_type].get(date_key, [])
|
||||||
pairs = RecordPairBuilder.build_pairs_from_records(day_records)
|
records_count_by_type[work_type] = len(day_records)
|
||||||
for pair in pairs:
|
|
||||||
daily_calc.add_record_pair(pair)
|
|
||||||
|
|
||||||
# Calculate daily totals
|
if day_records:
|
||||||
total_minutes = daily_calc.get_minutes_total_exclude_travel_time()
|
# Build pairs and calculate hours
|
||||||
|
daily_calc = DailyTimeCalculator()
|
||||||
|
pairs = RecordPairBuilder.build_pairs_from_records(day_records)
|
||||||
|
for pair in pairs:
|
||||||
|
daily_calc.add_record_pair(pair)
|
||||||
|
|
||||||
|
total_minutes = daily_calc.get_minutes_total_exclude_travel_time()
|
||||||
|
|
||||||
|
if total_minutes < 0:
|
||||||
|
hours_by_type[work_type] = 0.0
|
||||||
|
is_miss_punch_by_type[work_type] = True
|
||||||
|
else:
|
||||||
|
hours_by_type[work_type] = total_minutes / 60.0
|
||||||
|
is_miss_punch_by_type[work_type] = False
|
||||||
|
else:
|
||||||
|
hours_by_type[work_type] = 0.0
|
||||||
|
is_miss_punch_by_type[work_type] = False
|
||||||
|
|
||||||
|
# Store daily data with SP/PW/PT breakdown
|
||||||
|
total_day_hours = sum(hours_by_type.values())
|
||||||
|
total_records_count = sum(records_count_by_type.values())
|
||||||
|
|
||||||
daily_hours[date_key] = {
|
daily_hours[date_key] = {
|
||||||
'total_minutes': total_minutes,
|
'total_minutes': int(total_day_hours * 60),
|
||||||
'total_hours': total_minutes / 60.0 if total_minutes > 0 else 0,
|
'total_hours': total_day_hours,
|
||||||
'is_miss_punch': total_minutes < 0,
|
'regular_hours': hours_by_type['regular'],
|
||||||
'records_count': len(day_records)
|
'sp_hours': hours_by_type['SP'],
|
||||||
|
'pw_hours': hours_by_type['PW'],
|
||||||
|
'pt_hours': hours_by_type['PT'],
|
||||||
|
'is_miss_punch': any(is_miss_punch_by_type.values()),
|
||||||
|
'records_count': total_records_count,
|
||||||
|
'miss_punch_details': {
|
||||||
|
'regular': is_miss_punch_by_type['regular'],
|
||||||
|
'SP': is_miss_punch_by_type['SP'],
|
||||||
|
'PW': is_miss_punch_by_type['PW'],
|
||||||
|
'PT': is_miss_punch_by_type['PT']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add to weekly calculator (group by week)
|
# Accumulate weekly hours by type
|
||||||
if current_date.weekday() == 0: # Monday - start new week
|
for work_type in ['regular', 'SP', 'PW', 'PT']:
|
||||||
weekly_calc = WeeklyTimeCalculator()
|
current_week_hours[work_type] += hours_by_type[work_type]
|
||||||
weekly_calculators.append(weekly_calc)
|
|
||||||
|
|
||||||
if weekly_calculators:
|
# Check for end of week (Sunday) or end of period
|
||||||
weekly_calculators[-1].add_daily_calculator(daily_calc)
|
is_end_of_week = current_date.weekday() == 6
|
||||||
|
is_end_of_period = current_date >= end_date_val
|
||||||
|
|
||||||
|
if is_end_of_week or is_end_of_period:
|
||||||
|
# Calculate weekly totals
|
||||||
|
week_regular_total = current_week_hours['regular']
|
||||||
|
week_sp_total = current_week_hours['SP']
|
||||||
|
week_pw_total = current_week_hours['PW']
|
||||||
|
week_pt_total = current_week_hours['PT']
|
||||||
|
week_total = week_regular_total + week_sp_total + week_pw_total + week_pt_total
|
||||||
|
|
||||||
|
# Only regular hours count toward overtime (40 hour rule)
|
||||||
|
week_regular_hours = min(week_regular_total, 40.0)
|
||||||
|
week_overtime_hours = max(0, week_regular_total - 40.0)
|
||||||
|
|
||||||
|
weekly_hours.append({
|
||||||
|
'total_hours': round(week_total, 2),
|
||||||
|
'regular_hours': round(week_regular_hours, 2),
|
||||||
|
'overtime_hours': round(week_overtime_hours, 2),
|
||||||
|
'sp_hours': round(week_sp_total, 2),
|
||||||
|
'pw_hours': round(week_pw_total, 2),
|
||||||
|
'pt_hours': round(week_pt_total, 2),
|
||||||
|
'total_minutes': int(week_total * 60),
|
||||||
|
'regular_minutes': int(week_regular_hours * 60),
|
||||||
|
'overtime_minutes': int(week_overtime_hours * 60),
|
||||||
|
'sp_minutes': int(week_sp_total * 60),
|
||||||
|
'pw_minutes': int(week_pw_total * 60),
|
||||||
|
'pt_minutes': int(week_pt_total * 60)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Reset for next week
|
||||||
|
current_week_hours = {'regular': 0, 'SP': 0, 'PW': 0, 'PT': 0}
|
||||||
|
|
||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
# Calculate weekly totals
|
|
||||||
weekly_hours = []
|
|
||||||
for week_calc in weekly_calculators:
|
|
||||||
week_calc.calculate_time()
|
|
||||||
weekly_hours.append({
|
|
||||||
'total_hours': week_calc.total_hours,
|
|
||||||
'regular_hours': week_calc.regular_hours,
|
|
||||||
'overtime_hours': week_calc.overtime_hours,
|
|
||||||
'total_minutes': week_calc.total_minutes,
|
|
||||||
'regular_minutes': week_calc.regular_minutes,
|
|
||||||
'overtime_minutes': week_calc.overtime_minutes
|
|
||||||
})
|
|
||||||
|
|
||||||
# Calculate grand totals
|
# Calculate grand totals
|
||||||
grand_total_hours = sum(week['total_hours'] for week in weekly_hours)
|
grand_total_hours = sum(week['total_hours'] for week in weekly_hours)
|
||||||
grand_regular_hours = sum(week['regular_hours'] for week in weekly_hours)
|
grand_regular_hours = sum(week['regular_hours'] for week in weekly_hours)
|
||||||
grand_overtime_hours = sum(week['overtime_hours'] for week in weekly_hours)
|
grand_overtime_hours = sum(week['overtime_hours'] for week in weekly_hours)
|
||||||
|
grand_sp_hours = sum(week['sp_hours'] for week in weekly_hours)
|
||||||
|
grand_pw_hours = sum(week['pw_hours'] for week in weekly_hours)
|
||||||
|
grand_pt_hours = sum(week.get('pt_hours', 0) for week in weekly_hours)
|
||||||
|
|
||||||
|
print(f"✅ Employee {employee_id}: Total: {grand_total_hours:.2f}h (Regular: {grand_regular_hours:.2f}h, OT: {grand_overtime_hours:.2f}h, SP: {grand_sp_hours:.2f}h, PW: {grand_pw_hours:.2f}h, PT: {grand_pt_hours:.2f}h)")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'employee_id': employee_id,
|
'employee_id': employee_id,
|
||||||
'start_date': start_date.strftime('%Y-%m-%d'),
|
'base_employee_id': base_employee_id,
|
||||||
'end_date': end_date.strftime('%Y-%m-%d'),
|
'start_date': start_date.strftime('%Y-%m-%d') if hasattr(start_date, 'strftime') else str(start_date),
|
||||||
|
'end_date': end_date.strftime('%Y-%m-%d') if hasattr(end_date, 'strftime') else str(end_date),
|
||||||
'daily_hours': daily_hours,
|
'daily_hours': daily_hours,
|
||||||
'weekly_hours': weekly_hours,
|
'weekly_hours': weekly_hours,
|
||||||
'grand_totals': {
|
'grand_totals': {
|
||||||
'total_hours': grand_total_hours,
|
'total_hours': round(grand_total_hours, 2),
|
||||||
'regular_hours': grand_regular_hours,
|
'regular_hours': round(grand_regular_hours, 2),
|
||||||
'overtime_hours': grand_overtime_hours,
|
'overtime_hours': round(grand_overtime_hours, 2),
|
||||||
|
'sp_hours': round(grand_sp_hours, 2),
|
||||||
|
'pw_hours': round(grand_pw_hours, 2),
|
||||||
|
'pt_hours': round(grand_pt_hours, 2),
|
||||||
'total_minutes': int(grand_total_hours * 60),
|
'total_minutes': int(grand_total_hours * 60),
|
||||||
'regular_minutes': int(grand_regular_hours * 60),
|
'regular_minutes': int(grand_regular_hours * 60),
|
||||||
'overtime_minutes': int(grand_overtime_hours * 60)
|
'overtime_minutes': int(grand_overtime_hours * 60),
|
||||||
|
'sp_minutes': int(grand_sp_hours * 60),
|
||||||
|
'pw_minutes': int(grand_pw_hours * 60),
|
||||||
|
'pt_minutes': int(grand_pt_hours * 60)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error calculating working hours for employee {employee_id}: {e}")
|
print(f"❌ Error calculating working hours for employee {employee_id}: {e}")
|
||||||
|
import traceback
|
||||||
|
print(f"❌ Traceback: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def calculate_all_employees_hours(self, start_date: datetime, end_date: datetime,
|
def calculate_all_employees_hours(self, start_date: datetime, end_date: datetime,
|
||||||
attendance_records: List[Dict]) -> Dict[str, Any]:
|
attendance_records: List[Dict]) -> Dict[str, Any]:
|
||||||
"""Calculate working hours for all employees in the given period"""
|
"""
|
||||||
|
Calculate working hours for all employees in the given period.
|
||||||
|
|
||||||
|
This method consolidates employees by base ID, so records for 1234, 1234 SP,
|
||||||
|
1234 PW, 1234 PT will all be grouped under base employee 1234.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start_date: Start date for calculation
|
||||||
|
end_date: End date for calculation
|
||||||
|
attendance_records: List of attendance records from database
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing hours data for all employees with SP/PW/PT breakdown
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# Get unique employee IDs
|
print(f"🚀 Starting calculation for all employees with SP/PW/PT consolidation")
|
||||||
employee_ids = set()
|
|
||||||
|
# Get unique BASE employee IDs (consolidate SP/PW/PT variants)
|
||||||
|
base_employee_ids = set()
|
||||||
for record in attendance_records:
|
for record in attendance_records:
|
||||||
if hasattr(record, '__dict__'):
|
try:
|
||||||
employee_ids.add(str(record.employee_id))
|
if hasattr(record, '__dict__'):
|
||||||
else:
|
employee_id = str(getattr(record, 'employee_id', '')).strip()
|
||||||
employee_ids.add(str(record['employee_id']))
|
else:
|
||||||
|
employee_id = str(record.get('employee_id', '')).strip()
|
||||||
|
|
||||||
|
if employee_id:
|
||||||
|
base_id, _ = parse_employee_id_for_work_type(employee_id)
|
||||||
|
if base_id:
|
||||||
|
base_employee_ids.add(base_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error processing employee ID: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"👥 Found {len(base_employee_ids)} unique base employees (after consolidation)")
|
||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
for emp_id in employee_ids:
|
for base_emp_id in sorted(base_employee_ids):
|
||||||
results[emp_id] = self.calculate_employee_hours(emp_id, start_date, end_date, attendance_records)
|
try:
|
||||||
|
print(f"\n🔄 Processing base employee {base_emp_id}")
|
||||||
|
results[base_emp_id] = self.calculate_employee_hours(
|
||||||
|
base_emp_id, start_date, end_date, attendance_records
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error processing employee {base_emp_id}: {e}")
|
||||||
|
# Return empty result for this employee
|
||||||
|
results[base_emp_id] = {
|
||||||
|
'employee_id': base_emp_id,
|
||||||
|
'base_employee_id': base_emp_id,
|
||||||
|
'start_date': start_date.strftime('%Y-%m-%d') if hasattr(start_date, 'strftime') else str(start_date),
|
||||||
|
'end_date': end_date.strftime('%Y-%m-%d') if hasattr(end_date, 'strftime') else str(end_date),
|
||||||
|
'daily_hours': {},
|
||||||
|
'weekly_hours': [],
|
||||||
|
'grand_totals': {
|
||||||
|
'total_hours': 0.0,
|
||||||
|
'regular_hours': 0.0,
|
||||||
|
'overtime_hours': 0.0,
|
||||||
|
'sp_hours': 0.0,
|
||||||
|
'pw_hours': 0.0,
|
||||||
|
'pt_hours': 0.0,
|
||||||
|
'total_minutes': 0,
|
||||||
|
'regular_minutes': 0,
|
||||||
|
'overtime_minutes': 0,
|
||||||
|
'sp_minutes': 0,
|
||||||
|
'pw_minutes': 0,
|
||||||
|
'pt_minutes': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'calculation_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
'calculation_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
'period_start': start_date.strftime('%Y-%m-%d'),
|
'period_start': start_date.strftime('%Y-%m-%d') if hasattr(start_date, 'strftime') else str(start_date),
|
||||||
'period_end': end_date.strftime('%Y-%m-%d'),
|
'period_end': end_date.strftime('%Y-%m-%d') if hasattr(end_date, 'strftime') else str(end_date),
|
||||||
'employee_count': len(employee_ids),
|
'employee_count': len(base_employee_ids),
|
||||||
'employees': results
|
'employees': results
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error calculating hours for all employees: {e}")
|
print(f"❌ Error calculating hours for all employees: {e}")
|
||||||
|
import traceback
|
||||||
|
print(f"❌ Traceback: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
Reference in New Issue
Block a user