mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-04-26 06:29:25 -05:00
312 lines
9.6 KiB
Python
312 lines
9.6 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
|
|
}
|
|
}
|
|
|