Files
TimeTracker/app/utils/overtime.py
Dries Peeters 90dde470da style: standardize code formatting and normalize line endings
- Normalize line endings from CRLF to LF across all files to match .editorconfig
- Standardize quote style from single quotes to double quotes
- Normalize whitespace and formatting throughout codebase
- Apply consistent code style across 372 files including:
  * Application code (models, routes, services, utils)
  * Test files
  * Configuration files
  * CI/CD workflows

This ensures consistency with the project's .editorconfig settings and
improves code maintainability.
2025-11-28 20:05:37 +01:00

295 lines
9.3 KiB
Python

"""
Overtime Calculation Utilities
Provides functions to calculate overtime hours based on user's standard working hours per day.
"""
from datetime import datetime, timedelta, date
from typing import Dict, List, Optional, Tuple
from sqlalchemy import func
def calculate_daily_overtime(total_hours: float, standard_hours: float) -> float:
"""
Calculate overtime hours for a single day.
Args:
total_hours: Total hours worked in a day
standard_hours: Standard working hours per day
Returns:
Overtime hours (0 if no overtime)
"""
if total_hours <= standard_hours:
return 0.0
return round(total_hours - standard_hours, 2)
def calculate_period_overtime(
user, start_date: date, end_date: date, include_weekends: bool = True
) -> Dict[str, float]:
"""
Calculate overtime for a specific period.
Args:
user: User object with standard_hours_per_day setting
start_date: Start date of the period
end_date: End date of the period
include_weekends: Whether to count weekend hours as overtime
Returns:
Dictionary with regular_hours, overtime_hours, and total_hours
"""
from app.models import TimeEntry
from app import db
# Get all time entries for the period
# Convert dates to datetime ranges to include full day
from datetime import datetime as dt
start_datetime = dt.combine(start_date, dt.min.time())
end_datetime = dt.combine(end_date, dt.max.time())
entries = TimeEntry.query.filter(
TimeEntry.user_id == user.id,
TimeEntry.end_time.isnot(None),
TimeEntry.start_time >= start_datetime,
TimeEntry.start_time <= end_datetime,
).all()
# Group entries by date
daily_hours = {}
for entry in entries:
entry_date = entry.start_time.date()
hours = entry.duration_hours
if entry_date not in daily_hours:
daily_hours[entry_date] = 0.0
daily_hours[entry_date] += hours
# Calculate overtime per day
standard_hours = user.standard_hours_per_day
total_regular = 0.0
total_overtime = 0.0
for day_date, hours in daily_hours.items():
# Check if weekend
if not include_weekends and day_date.weekday() >= 5: # Saturday=5, Sunday=6
# All weekend hours are overtime
total_overtime += hours
else:
# Calculate regular vs overtime
if hours <= standard_hours:
total_regular += hours
else:
total_regular += standard_hours
total_overtime += hours - standard_hours
return {
"regular_hours": round(total_regular, 2),
"overtime_hours": round(total_overtime, 2),
"total_hours": round(total_regular + total_overtime, 2),
"days_with_overtime": sum(1 for h in daily_hours.values() if h > standard_hours),
}
def get_daily_breakdown(user, start_date: date, end_date: date) -> List[Dict]:
"""
Get a daily breakdown of regular and overtime hours.
Args:
user: User object with standard_hours_per_day setting
start_date: Start date of the period
end_date: End date of the period
Returns:
List of dictionaries with daily breakdown
"""
from app.models import TimeEntry
from app import db
# Get all time entries for the period
# Convert dates to datetime ranges to include full day
from datetime import datetime as dt
start_datetime = dt.combine(start_date, dt.min.time())
end_datetime = dt.combine(end_date, dt.max.time())
entries = (
TimeEntry.query.filter(
TimeEntry.user_id == user.id,
TimeEntry.end_time.isnot(None),
TimeEntry.start_time >= start_datetime,
TimeEntry.start_time <= end_datetime,
)
.order_by(TimeEntry.start_time)
.all()
)
# Group entries by date
daily_data = {}
for entry in entries:
entry_date = entry.start_time.date()
if entry_date not in daily_data:
daily_data[entry_date] = {"date": entry_date, "total_hours": 0.0, "entries": []}
daily_data[entry_date]["total_hours"] += entry.duration_hours
daily_data[entry_date]["entries"].append(entry)
# Calculate overtime for each day
standard_hours = user.standard_hours_per_day
breakdown = []
for day_date in sorted(daily_data.keys()):
day_info = daily_data[day_date]
total_hours = day_info["total_hours"]
regular_hours = min(total_hours, standard_hours)
overtime_hours = max(0, total_hours - standard_hours)
breakdown.append(
{
"date": day_date,
"date_str": day_date.strftime("%Y-%m-%d"),
"weekday": day_date.strftime("%A"),
"total_hours": round(total_hours, 2),
"regular_hours": round(regular_hours, 2),
"overtime_hours": round(overtime_hours, 2),
"is_overtime": overtime_hours > 0,
"entries_count": len(day_info["entries"]),
}
)
return breakdown
def get_weekly_overtime_summary(user, weeks: int = 4) -> List[Dict]:
"""
Get a weekly summary of overtime for the last N weeks.
Args:
user: User object with standard_hours_per_day setting
weeks: Number of weeks to look back
Returns:
List of weekly summaries
"""
from app.models import TimeEntry
from app import db
end_date = datetime.now().date()
start_date = end_date - timedelta(weeks=weeks)
# Convert dates to datetime ranges to include full day
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
# Get all time entries
entries = TimeEntry.query.filter(
TimeEntry.user_id == user.id,
TimeEntry.end_time.isnot(None),
TimeEntry.start_time >= start_datetime,
TimeEntry.start_time <= end_datetime,
).all()
# Group by week
weekly_data = {}
for entry in entries:
entry_date = entry.start_time.date()
# Get Monday of that week
week_start = entry_date - timedelta(days=entry_date.weekday())
if week_start not in weekly_data:
weekly_data[week_start] = {}
if entry_date not in weekly_data[week_start]:
weekly_data[week_start][entry_date] = 0.0
weekly_data[week_start][entry_date] += entry.duration_hours
# Calculate overtime per week
standard_hours = user.standard_hours_per_day
weekly_summary = []
for week_start in sorted(weekly_data.keys()):
daily_hours = weekly_data[week_start]
week_regular = 0.0
week_overtime = 0.0
for day_date, hours in daily_hours.items():
if hours <= standard_hours:
week_regular += hours
else:
week_regular += standard_hours
week_overtime += hours - standard_hours
week_end = week_start + timedelta(days=6)
weekly_summary.append(
{
"week_start": week_start,
"week_end": week_end,
"week_label": f"{week_start.strftime('%b %d')} - {week_end.strftime('%b %d')}",
"regular_hours": round(week_regular, 2),
"overtime_hours": round(week_overtime, 2),
"total_hours": round(week_regular + week_overtime, 2),
"days_worked": len(daily_hours),
}
)
return weekly_summary
def get_overtime_statistics(user, start_date: date, end_date: date) -> Dict:
"""
Get comprehensive overtime statistics for a period.
Args:
user: User object
start_date: Start date
end_date: End date
Returns:
Dictionary with various overtime statistics
"""
period_data = calculate_period_overtime(user, start_date, end_date)
daily_breakdown = get_daily_breakdown(user, start_date, end_date)
# Calculate additional statistics
days_worked = len(daily_breakdown)
days_with_overtime = sum(1 for day in daily_breakdown if day["is_overtime"])
# Average hours per day
avg_hours_per_day = period_data["total_hours"] / days_worked if days_worked > 0 else 0
# Max overtime in a single day
max_overtime_day = max(
(day for day in daily_breakdown if day["is_overtime"]), key=lambda x: x["overtime_hours"], default=None
)
return {
"period": {
"start_date": start_date.strftime("%Y-%m-%d"),
"end_date": end_date.strftime("%Y-%m-%d"),
"days_in_period": (end_date - start_date).days + 1,
},
"hours": period_data,
"days_statistics": {
"days_worked": days_worked,
"days_with_overtime": days_with_overtime,
"percentage_overtime_days": (round(days_with_overtime / days_worked * 100, 1) if days_worked > 0 else 0),
},
"averages": {
"avg_hours_per_day": round(avg_hours_per_day, 2),
"avg_overtime_per_overtime_day": (
round(period_data["overtime_hours"] / days_with_overtime, 2) if days_with_overtime > 0 else 0
),
},
"max_overtime": {
"date": max_overtime_day["date_str"] if max_overtime_day else None,
"hours": max_overtime_day["overtime_hours"] if max_overtime_day else 0,
},
}