diff --git a/backend/api/src/analytics/schema.graphql b/backend/api/src/analytics/schema.graphql index f3c5e6ee..45c950da 100644 --- a/backend/api/src/analytics/schema.graphql +++ b/backend/api/src/analytics/schema.graphql @@ -1006,6 +1006,8 @@ type YearlyForecast { year: Int! goal: Float! byMonth: [YearlyForecastByMonth!]! + workingDays: Int! + realizedWorkingDays: Int! } type YearlyForecastByMonth { diff --git a/backend/api/src/analytics/yearly_forecast.py b/backend/api/src/analytics/yearly_forecast.py index aa1682fd..2ad98f6a 100644 --- a/backend/api/src/analytics/yearly_forecast.py +++ b/backend/api/src/analytics/yearly_forecast.py @@ -14,25 +14,27 @@ def resolve_yearly_forecast(_, info, year=None): main_goal = 30000000 actual = 0 + current_date = datetime.now() + current_year = current_date.year + current_month = current_date.month by_month = [] + revenue_tracking = None for month in range(1, 13): m = month - 1 if month > 1 else 12 y = year if month > 1 else year - 1 expected_consulting_fee = get_expected_regular_consulting_revenue(y, m) expected_pre_contracted_revenue = get_expected_pre_contracted_revenue(y, m) - current_date = datetime.now() - current_year = current_date.year - current_month = current_date.month - - revenue_tracking = None discount = main_goal / (13 - month) if (y == current_year and m == current_month): revenue_tracking = compute_revenue_tracking(current_date) elif y < current_year or (y == current_year and m < current_month): - discount = revenue_tracking["total"] - revenue_tracking = compute_revenue_tracking(get_last_day_of_month(datetime(y, m, 1))) + last_day_of_month = get_last_day_of_month(datetime(y, m, 1)) + revenue_tracking = compute_revenue_tracking(last_day_of_month) + discount = revenue_tracking["total"] + else: + revenue_tracking = None actual += revenue_tracking["total"] if revenue_tracking else 0 @@ -50,10 +52,27 @@ def resolve_yearly_forecast(_, info, year=None): main_goal -= discount + total_working_days = sum(len(get_working_days_in_month(y, m)) for m in range(1, 13)) + + realized_working_days = 0 + current_date = datetime.now() + + for month in range(1, 13): + y = year if month > 1 else year - 1 + m = month - 1 if month > 1 else 12 + + if y < current_date.year or (y == current_date.year and m < current_date.month): + realized_working_days += len(get_working_days_in_month(y, m)) + elif y == current_date.year and m == current_date.month: + working_days = get_working_days_in_month(y, m) + realized_working_days += sum(1 for day in working_days if day.date() <= current_date.date()) + return { "year": year, "goal": 30000000, - "by_month": by_month + "by_month": by_month, + "working_days": total_working_days, + "realized_working_days": realized_working_days } diff --git a/backend/models/src/omni_models/analytics/forecast.py b/backend/models/src/omni_models/analytics/forecast.py index 05b4b3b9..e8afe222 100644 --- a/backend/models/src/omni_models/analytics/forecast.py +++ b/backend/models/src/omni_models/analytics/forecast.py @@ -352,7 +352,8 @@ def filter_items(items): def adjust_entity(entity): if slug == 'consulting': - entity.projected = (entity.in_analysis / forecast_working_days.in_analysis_partial) * forecast_working_days.in_analysis + divisor = forecast_working_days.in_analysis_partial or 1 + entity.projected = (entity.in_analysis / divisor) * forecast_working_days.in_analysis previous_value = entity.one_month_ago two_months_ago_value = entity.two_months_ago @@ -368,7 +369,8 @@ def adjust_entity(entity): entity.expected_historical = previous_value * 0.6 + two_months_ago_value * 0.25 + three_months_ago_value * 0.15 elif slug == 'consulting_pre': - entity.projected = (entity.in_analysis / forecast_working_days.in_analysis_partial) * forecast_working_days.in_analysis + divisor = forecast_working_days.in_analysis_partial or 1 + entity.projected = (entity.in_analysis / divisor) * forecast_working_days.in_analysis for client in by_client: diff --git a/backend/models/src/omni_models/analytics/revenue_tracking.py b/backend/models/src/omni_models/analytics/revenue_tracking.py index 769f9a4a..7dcf691d 100644 --- a/backend/models/src/omni_models/analytics/revenue_tracking.py +++ b/backend/models/src/omni_models/analytics/revenue_tracking.py @@ -1273,6 +1273,7 @@ def build(consultant_name, pre_contracted, regular): for sponsor in client["by_sponsor"] for case in sponsor["by_case"] for project in case["by_project"] + if "by_worker" in project for worker in project["by_worker"] if worker["name"] == consultant_name ) @@ -1284,6 +1285,7 @@ def build(consultant_name, pre_contracted, regular): for sponsor in client["by_sponsor"] for case in sponsor["by_case"] for project in case["by_project"] + if "by_worker" in project for worker in project["by_worker"] if worker["name"] == consultant_name ) @@ -1295,8 +1297,9 @@ def build(consultant_name, pre_contracted, regular): for sponsor in client["by_sponsor"] for case in sponsor["by_case"] for project in case["by_project"] + if "by_worker" in project and project["kind"] == "consulting" for worker in project["by_worker"] - if worker["name"] == consultant_name and project["kind"] == "consulting" + if worker["name"] == consultant_name ) consultant = globals.omni_models.workers.get_by_name(consultant_name) @@ -1311,17 +1314,22 @@ def build(consultant_name, pre_contracted, regular): @staticmethod def build_list(pre_contracted, regular): + consultant_names = set() + + # Collect consultant names safely checking for by_worker + for data in [regular, pre_contracted]: + for account_manager in data["monthly"]["by_account_manager"]: + for client in account_manager["by_client"]: + for sponsor in client["by_sponsor"]: + for case in sponsor["by_case"]: + for project in case["by_project"]: + if "by_worker" in project: + for worker in project["by_worker"]: + consultant_names.add(worker["name"]) + return [ ConsultantSummary.build(consultant_name, pre_contracted, regular) - for consultant_name in set( - worker["name"] - for account_manager in regular["monthly"]["by_account_manager"] - for client in account_manager["by_client"] - for sponsor in client["by_sponsor"] - for case in sponsor["by_case"] - for project in case["by_project"] - for worker in project["by_worker"] - ) + for consultant_name in consultant_names ] def compute_summaries(pre_contracted, regular): diff --git a/frontend/src/app/financial/2025/page.tsx b/frontend/src/app/financial/2025/page.tsx index 61c6641d..1844b457 100644 --- a/frontend/src/app/financial/2025/page.tsx +++ b/frontend/src/app/financial/2025/page.tsx @@ -18,6 +18,8 @@ const YEARLY_FORECAST_QUERY = gql` yearlyForecast(year: $year) { year goal + workingDays + realizedWorkingDays byMonth { month goal @@ -64,7 +66,9 @@ interface ForecastTableProps { } const ForecastTable = ({ months, forecast }: ForecastTableProps) => { - const currentMonth = new Date().getMonth() + 1; + const today = new Date(); + const currentMonth = today.getMonth() + 1; + const currentYear = today.getFullYear(); const totalGoal = months.reduce((sum, month) => sum + month.goal, 0); const totalWorkingDays = months.reduce( @@ -100,6 +104,13 @@ const ForecastTable = ({ months, forecast }: ForecastTableProps) => { totalExpectedHandsOnFee + totalExpectedSquadFee; + function isMonthInPast(month: number) { + if (month === 12) { + return currentYear > 2024 || (currentYear === 2024 && currentMonth >= 12); + } + return currentYear > 2025 || (currentYear === 2025 && currentMonth >= month); + } + return (