Files
Server-scripts/work/compareTimesheets.py
T
2026-09-14 12:43:38 +02:00

172 lines
7.4 KiB
Python
Executable File

import pandas as pd
import pprint
from datetime import datetime
import json
# Create a PrettyPrinter with custom settings
pp = pprint.PrettyPrinter(
indent=2,
width=100000, # large line width to avoid wrapping
compact=False, # don't compress structures
depth=None, # don't limit nesting depth
sort_dicts=False
)
# load data
absences = pd.read_excel("Activity Timeoff 2025_01_01 - 2026_02_28.xlsx")
# Load Excel (or CSV) file
# For Excel: timesheet = pd.read_excel("timesheet.xlsx", engine="openpyxl")
timesheets = pd.read_excel("[ALL] Template_Time_Sheets_FFG - Unified.xlsx", sheet_name=None)
df_timesheet = timesheets["2025"]
## Sort RATES
df_rates = timesheets["Stundensätze"]
# Remove all non-numeric characters (keep digits and dot)
hourlyRateKey = "Hourly Rate for Application €/h"
df_rates = df_rates.rename(columns={hourlyRateKey: 'rate'})
hourlyRateKey = 'rate'
df_rates[hourlyRateKey] = df_rates[hourlyRateKey].astype(str).str.replace(r'[^0-9.]', '', regex=True)
# Ensure the Hourly Rate column is numeric
df_rates[hourlyRateKey] = pd.to_numeric(df_rates[hourlyRateKey], errors='coerce')
## FORMAT timesheet data frame
# Fill missing Activity for earlier rows if necessary
df_timesheet['Activity'] = df_timesheet['Activity'].ffill() # forward-fill the activity for blank cells
# Convert 'Date' to datetime
df_timesheet['Date'] = pd.to_datetime(df_timesheet['Date'], errors='coerce')
# Forward-fill missing dates and activities
df_timesheet['Date'] = df_timesheet['Date'].ffill()
df_timesheet['Activity'] = df_timesheet['Activity'].ffill()
# Filter out rows where 'Person' is missing
df_timesheet_filtered = df_timesheet.dropna(subset=['Person']).copy()
# #FILETER ABSECES
absences['Start date'] = pd.to_datetime(absences['Start date'], errors='coerce')
absences['End date'] = pd.to_datetime(absences['End date'], errors='coerce')
df_absences_filtered = absences
df_absences_by_year = {}
for year in df_absences_filtered['Start date'].dt.year.unique():
# Filter for the year
df_year = df_absences_filtered[df_absences_filtered['Start date'].dt.year == year].copy()
# Combine first and last name into a new column 'Person'
df_year['Person'] = df_year['Last name'].astype(str) + ", " + df_year['First name'].astype(str)
# Select and reorder columns
df_year = df_year[['Person', 'Start date', 'End date']]
# Reset index
df_year = df_year.reset_index(drop=True)
# Store in dictionary
df_absences_by_year[year] = df_year
for year, df in df_absences_by_year.items():
# Function to find matching full name(s) for a person
def get_short_name(person):
# Remove spaces from person
person_clean = str(person).replace(" ", "")
# Remove spaces from df_rates['Name'] and check for match
matches = df_rates[df_rates['Name'].str.replace(" ", "", regex=False)
.str.contains(person_clean, case=False, na=False)]
if not matches.empty:
# Return only the first match
return str(matches['Short'].iloc[0])
return "" # no match found
# Apply the function row-wise to create a new column
df['Short'] = df['Person'].apply(get_short_name)
# Keep only the row with the highest Hourly Rate per Name
df_rates = df_rates.loc[df_rates.groupby('Short')[hourlyRateKey].idxmax()].reset_index(drop=True)
# Sort By Year
# Create dictionary of DataFrames for each year
df_timesheet_by_year = {}
for year in df_timesheet_filtered['Date'].dt.year.unique():
# Filter for the year
df_year = df_timesheet_filtered[df_timesheet_filtered['Date'].dt.year == year].copy()
# Convert 'Workpackage' to integer (nullable Int64 to handle NaN safely)
df_year['Workpackage'] = pd.to_numeric(df_year['Workpackage'], errors='coerce').astype('Int64')
# Select and reorder columns
df_year = df_year[['Person', 'Date', 'Workpackage', 'Time (in hours)', 'Activity']]
# Reset index
df_year = df_year.reset_index(drop=True)
# Store in dictionary
df_timesheet_by_year[year] = df_year
for year, df in df_timesheet_by_year.items():
# Function to find matching full name(s) for a person
def get_full_name(person):
matches = df_rates[df_rates['Short'].str.contains(str(person), case=False, na=False)]
if not matches.empty:
return str(matches['Name'].iloc[0])
return "" # no match found
# Apply the function row-wise to create a new column
df['FullName'] = df['Person'].apply(get_full_name)
# Build nested dictionary
data = {}
for year in df_timesheet_by_year.keys():
for _, row in df_timesheet_by_year[year].iterrows():
person = row['Person']
date = row['Date']
date_str = date.strftime("%Y-%m-%d")
# Skip rows without a person
if pd.isna(person):
continue
data.setdefault(year, {}).setdefault(person, {}).setdefault("absences", [])
data.setdefault(year, {}).setdefault(person, {}).setdefault("workingdays", {})
data[year][person]["workingdays"][date_str] = {
"workpackage": row['Workpackage'],
"time": row['Time (in hours)'],
"activity": row['Activity']
}
# Group by Person and Workpackage and sum hours
summary = df_timesheet_by_year[year].groupby(['FullName', 'Workpackage'])['Time (in hours)'].sum().reset_index()
# Merge hourly rate into summary using the full name
summary = summary.merge(
df_rates[['Name', hourlyRateKey]],
left_on='FullName',
right_on='Name',
how='left'
)
# Compute costs
summary['costs'] = summary['Time (in hours)'] * summary[hourlyRateKey]
# Show all columns and rows
print(summary.to_string(index=False)) # index=False hides row numbers
for year in df_absences_by_year.keys():
for _, row in df_absences_by_year[year].iterrows():
person = row['Short']
start_date = row['Start date']
end_date = row['End date']
# Ensure nested structure exists
data.setdefault(year, {}).setdefault(person, {}).setdefault("absences", [])
data.setdefault(year, {}).setdefault(person, {}).setdefault("workingdays", {})
data[year][person]["absences"].append({"start": start_date, "end": end_date})
for year in data.keys():
for person in data[year].keys():
for absence in data[year][person]["absences"]:
# Generate all dates between start and end
all_dates = pd.date_range(start=absence["start"], end=absence["end"], freq='B') # 'B' = business days (Mon-Fri)
# Create a dict with dates as keys and empty dict as value
workdays_dict = set(data[year][person]['workingdays'].keys())
absence_dict = {date.strftime("%Y-%m-%d"): {} for date in all_dates}
# Get sets of dates
absence_dates = set(absence_dict)
working_dates = set(data[year][person]['workingdays'].keys())
# Find intersection
overlap = absence_dates & working_dates
sorted_overlap = sorted(overlap, key=lambda x: datetime.strptime(x, "%Y-%m-%d"))
if overlap and absence["start"] != absence["end"]:
print(f"Dates overlapping for {person}: {overlap} - Vac: {absence["start"].strftime("%Y-%m-%d")} - {absence["end"].strftime("%Y-%m-%d")}")
elif overlap:
print(f"Dates overlapping for {person}: {overlap} - Vac: {absence["start"].strftime("%Y-%m-%d")}")
print("fin")