172 lines
7.4 KiB
Python
Executable File
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") |