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")