Updated time attendance time calculation methods
This commit is contained in:
@@ -5291,7 +5291,7 @@ def payroll_dashboard():
|
|||||||
employee_query = db.session.execute(text(f"""
|
employee_query = db.session.execute(text(f"""
|
||||||
SELECT
|
SELECT
|
||||||
ad.employee_id,
|
ad.employee_id,
|
||||||
CONCAT(e.firstName, ' ', e.lastName) as full_name
|
CONCAT(e.lastName, ',', e.firstName) as full_name
|
||||||
FROM attendance_data ad
|
FROM attendance_data ad
|
||||||
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
||||||
WHERE ad.employee_id IN ({placeholders})
|
WHERE ad.employee_id IN ({placeholders})
|
||||||
|
|||||||
+124
-17
@@ -37,6 +37,83 @@ def parse_employee_id_for_work_type(employee_id: str) -> Tuple[str, str]:
|
|||||||
# Default to regular work
|
# Default to regular work
|
||||||
return employee_id_clean, 'regular'
|
return employee_id_clean, 'regular'
|
||||||
|
|
||||||
|
def round_time_to_quarter_hour(minutes: float) -> float:
|
||||||
|
"""
|
||||||
|
Round time to nearest quarter hour based on 7.5-minute increments
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- 0:00-0:07 → 0:00
|
||||||
|
- 0:08-0:22 → 0:15
|
||||||
|
- 0:23-0:37 → 0:30
|
||||||
|
- 0:38-0:52 → 0:45
|
||||||
|
- 0:53-1:07 → 1:00
|
||||||
|
"""
|
||||||
|
if minutes < 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Get minutes within the hour
|
||||||
|
total_minutes = minutes
|
||||||
|
hours = int(total_minutes // 60)
|
||||||
|
minutes_in_hour = total_minutes % 60
|
||||||
|
|
||||||
|
# Determine rounding
|
||||||
|
if minutes_in_hour <= 7:
|
||||||
|
rounded_in_hour = 0
|
||||||
|
elif minutes_in_hour <= 22:
|
||||||
|
rounded_in_hour = 15
|
||||||
|
elif minutes_in_hour <= 37:
|
||||||
|
rounded_in_hour = 30
|
||||||
|
elif minutes_in_hour <= 52:
|
||||||
|
rounded_in_hour = 45
|
||||||
|
else: # 53-59
|
||||||
|
hours += 1
|
||||||
|
rounded_in_hour = 0
|
||||||
|
|
||||||
|
return hours * 60 + rounded_in_hour
|
||||||
|
|
||||||
|
|
||||||
|
def convert_minutes_to_base100(minutes: float) -> float:
|
||||||
|
"""Convert minutes to base-100 hours (each hour = 100 units)"""
|
||||||
|
if minutes < 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
decimal_hours = minutes / 60.0
|
||||||
|
whole_hours = int(decimal_hours)
|
||||||
|
fractional_hours = decimal_hours - whole_hours
|
||||||
|
base100_fraction = fractional_hours * 100
|
||||||
|
|
||||||
|
return whole_hours + (base100_fraction / 100)
|
||||||
|
|
||||||
|
|
||||||
|
def round_base100_hours(base100_hours: float) -> float:
|
||||||
|
"""
|
||||||
|
Round base-100 hours to nearest quarter using 12.5 thresholds
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- 4.12 → 4.00
|
||||||
|
- 4.18 → 4.25
|
||||||
|
- 8.02 → 8.00
|
||||||
|
"""
|
||||||
|
if base100_hours < 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
whole_hours = int(base100_hours)
|
||||||
|
fractional_part = (base100_hours - whole_hours) * 100
|
||||||
|
|
||||||
|
# Round to nearest quarter
|
||||||
|
if fractional_part < 12.5:
|
||||||
|
rounded_fraction = 0
|
||||||
|
elif fractional_part < 37.5:
|
||||||
|
rounded_fraction = 25
|
||||||
|
elif fractional_part < 62.5:
|
||||||
|
rounded_fraction = 50
|
||||||
|
elif fractional_part < 87.5:
|
||||||
|
rounded_fraction = 75
|
||||||
|
else:
|
||||||
|
whole_hours += 1
|
||||||
|
rounded_fraction = 0
|
||||||
|
|
||||||
|
return round(whole_hours + (rounded_fraction / 100), 2)
|
||||||
|
|
||||||
class SingleCheckInCalculator:
|
class SingleCheckInCalculator:
|
||||||
"""Enhanced calculator for single check-in attendance systems with SP/PW support"""
|
"""Enhanced calculator for single check-in attendance systems with SP/PW support"""
|
||||||
@@ -153,21 +230,31 @@ class SingleCheckInCalculator:
|
|||||||
week_sp_total = sum(current_week_hours['SP'])
|
week_sp_total = sum(current_week_hours['SP'])
|
||||||
week_pw_total = sum(current_week_hours['PW'])
|
week_pw_total = sum(current_week_hours['PW'])
|
||||||
|
|
||||||
|
# NEW: Round the weekly total
|
||||||
|
week_total_raw = week_regular_total + week_sp_total + week_pw_total
|
||||||
|
week_total_rounded = round_base100_hours(week_total_raw)
|
||||||
|
|
||||||
# Only regular hours count toward overtime
|
# Only regular hours count toward overtime
|
||||||
week_regular_hours = min(week_regular_total, 40.0)
|
week_regular_hours = min(week_regular_total, 40.0)
|
||||||
week_overtime_hours = max(0, week_regular_total - 40.0)
|
week_overtime_hours = max(0, week_regular_total - 40.0)
|
||||||
|
|
||||||
|
# NEW: Round regular and overtime hours
|
||||||
|
week_regular_hours_rounded = round_base100_hours(week_regular_hours)
|
||||||
|
week_overtime_hours_rounded = round_base100_hours(week_overtime_hours)
|
||||||
|
week_sp_hours_rounded = round_base100_hours(week_sp_total)
|
||||||
|
week_pw_hours_rounded = round_base100_hours(week_pw_total)
|
||||||
|
|
||||||
weekly_totals.append({
|
weekly_totals.append({
|
||||||
'total_hours': week_regular_total + week_sp_total + week_pw_total,
|
'total_hours': week_total_rounded, # CHANGED: Use rounded value
|
||||||
'regular_hours': week_regular_hours,
|
'regular_hours': week_regular_hours_rounded, # CHANGED: Use rounded value
|
||||||
'overtime_hours': week_overtime_hours,
|
'overtime_hours': week_overtime_hours_rounded, # CHANGED: Use rounded value
|
||||||
'sp_hours': week_sp_total,
|
'sp_hours': week_sp_hours_rounded, # CHANGED: Use rounded value
|
||||||
'pw_hours': week_pw_total,
|
'pw_hours': week_pw_hours_rounded, # CHANGED: Use rounded value
|
||||||
'total_minutes': int((week_regular_total + week_sp_total + week_pw_total) * 60),
|
'total_minutes': int(week_total_rounded * 60),
|
||||||
'regular_minutes': int(week_regular_hours * 60),
|
'regular_minutes': int(week_regular_hours_rounded * 60),
|
||||||
'overtime_minutes': int(week_overtime_hours * 60),
|
'overtime_minutes': int(week_overtime_hours_rounded * 60),
|
||||||
'sp_minutes': int(week_sp_total * 60),
|
'sp_minutes': int(week_sp_hours_rounded * 60),
|
||||||
'pw_minutes': int(week_pw_total * 60)
|
'pw_minutes': int(week_pw_hours_rounded * 60)
|
||||||
})
|
})
|
||||||
|
|
||||||
current_week_hours = {'regular': [], 'SP': [], 'PW': []}
|
current_week_hours = {'regular': [], 'SP': [], 'PW': []}
|
||||||
@@ -175,11 +262,18 @@ class SingleCheckInCalculator:
|
|||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
# Calculate grand totals
|
# Calculate grand totals
|
||||||
grand_total_hours = sum(week['total_hours'] for week in weekly_totals)
|
grand_total_raw = sum(week['total_hours'] for week in weekly_totals)
|
||||||
grand_regular_hours = sum(week['regular_hours'] for week in weekly_totals)
|
grand_regular_raw = sum(week['regular_hours'] for week in weekly_totals)
|
||||||
grand_overtime_hours = sum(week['overtime_hours'] for week in weekly_totals)
|
grand_overtime_raw = sum(week['overtime_hours'] for week in weekly_totals)
|
||||||
grand_sp_hours = sum(week['sp_hours'] for week in weekly_totals)
|
grand_sp_raw = sum(week['sp_hours'] for week in weekly_totals)
|
||||||
grand_pw_hours = sum(week['pw_hours'] for week in weekly_totals)
|
grand_pw_raw = sum(week['pw_hours'] for week in weekly_totals)
|
||||||
|
|
||||||
|
# Round grand totals
|
||||||
|
grand_total_hours = round_base100_hours(grand_total_raw)
|
||||||
|
grand_regular_hours = round_base100_hours(grand_regular_raw)
|
||||||
|
grand_overtime_hours = round_base100_hours(grand_overtime_raw)
|
||||||
|
grand_sp_hours = round_base100_hours(grand_sp_raw)
|
||||||
|
grand_pw_hours = round_base100_hours(grand_pw_raw)
|
||||||
|
|
||||||
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)")
|
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)")
|
||||||
|
|
||||||
@@ -247,23 +341,36 @@ class SingleCheckInCalculator:
|
|||||||
# Calculate complete pairs only
|
# Calculate complete pairs only
|
||||||
num_complete_pairs = len(sorted_records) // 2
|
num_complete_pairs = len(sorted_records) // 2
|
||||||
total_hours = 0.0
|
total_hours = 0.0
|
||||||
|
total_rounded_minutes = 0.0 # NEW: Track rounded minutes for Daily Total
|
||||||
|
|
||||||
for i in range(0, num_complete_pairs * 2, 2):
|
for i in range(0, num_complete_pairs * 2, 2):
|
||||||
try:
|
try:
|
||||||
start_time = sorted_records[i]['timestamp']
|
start_time = sorted_records[i]['timestamp']
|
||||||
end_time = sorted_records[i + 1]['timestamp']
|
end_time = sorted_records[i + 1]['timestamp']
|
||||||
|
|
||||||
|
# Calculate exact hours (for Hours/Building column)
|
||||||
duration = end_time - start_time
|
duration = end_time - start_time
|
||||||
hours = duration.total_seconds() / 3600.0
|
exact_minutes = duration.total_seconds() / 60.0
|
||||||
|
hours = exact_minutes / 60.0
|
||||||
total_hours += hours
|
total_hours += hours
|
||||||
|
|
||||||
|
# NEW: Round this pair's time and accumulate for Daily Total
|
||||||
|
pair_rounded_minutes = round_time_to_quarter_hour(exact_minutes)
|
||||||
|
total_rounded_minutes += pair_rounded_minutes
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error calculating pair hours: {e}")
|
print(f"⚠️ Error calculating pair hours: {e}")
|
||||||
return 0.0, True
|
return 0.0, True
|
||||||
|
|
||||||
|
# NEW: Convert rounded minutes to base-100 and round
|
||||||
|
base100_hours = convert_minutes_to_base100(total_rounded_minutes)
|
||||||
|
rounded_total_hours = round_base100_hours(base100_hours)
|
||||||
|
|
||||||
# Odd number of records = miss punch
|
# Odd number of records = miss punch
|
||||||
is_miss_punch = (len(sorted_records) % 2 != 0)
|
is_miss_punch = (len(sorted_records) % 2 != 0)
|
||||||
|
|
||||||
return total_hours, is_miss_punch
|
# NEW: Return rounded hours instead of exact hours for Daily Total
|
||||||
|
return rounded_total_hours, is_miss_punch
|
||||||
|
|
||||||
def _empty_result(self, employee_id: str, start_date: datetime, end_date: datetime) -> Dict[str, Any]:
|
def _empty_result(self, employee_id: str, start_date: datetime, end_date: datetime) -> Dict[str, Any]:
|
||||||
"""Return empty result structure with SP/PW support"""
|
"""Return empty result structure with SP/PW support"""
|
||||||
|
|||||||
Reference in New Issue
Block a user