From 1a606419180b39b5475697a942ba3ab5de97e166 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 1 Jan 2025 08:12:18 -0300 Subject: [PATCH 1/4] Fixed montly goals logic --- backend/api/src/analytics/yearly_forecast.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/api/src/analytics/yearly_forecast.py b/backend/api/src/analytics/yearly_forecast.py index aa1682fd..ad605fae 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 From e2a0d67fa618071a6b8d88d70c0305f595ed4596 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 1 Jan 2025 08:29:58 -0300 Subject: [PATCH 2/4] Refactor Forecast Logic in Financial Page - Updated the logic for determining past months in the ForecastTable component by introducing a new `isMonthInPast` function, improving clarity and maintainability. - Replaced direct comparisons with the current month and year to utilize the new function, ensuring accurate calculations for actual values and totals. - Enhanced the readability of the code by consolidating date-related logic, making future modifications easier. These changes improve the accuracy of the financial forecasting calculations and enhance the overall code structure. --- frontend/src/app/financial/2025/page.tsx | 37 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/financial/2025/page.tsx b/frontend/src/app/financial/2025/page.tsx index 61c6641d..318fc77c 100644 --- a/frontend/src/app/financial/2025/page.tsx +++ b/frontend/src/app/financial/2025/page.tsx @@ -64,7 +64,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 +102,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 ( @@ -284,18 +293,17 @@ const ForecastTable = ({ months, forecast }: ForecastTableProps) => { {months.map((month, idx) => ( 0 ? (months[idx - 1].month <= currentMonth ? months[idx - 1].actual : 0) : undefined} - normalizedPreviousValue={idx > 0 ? (months[idx - 1].month <= currentMonth ? months[idx - 1].actual : 0) : undefined} + value={isMonthInPast(month.month) ? month.actual : 0} + normalizedValue={isMonthInPast(month.month) ? month.actual : 0} + previousValue={idx > 0 ? (isMonthInPast(months[idx - 1].month) ? months[idx - 1].actual : 0) : undefined} + normalizedPreviousValue={idx > 0 ? (isMonthInPast(months[idx - 1].month) ? months[idx - 1].actual : 0) : undefined} normalized={false} className={`border-x border-gray-400 ${month.month === currentMonth ? 'bg-blue-50' : ''}`} - /> ))} sum + (month.month <= currentMonth ? month.actual : 0), 0)} - normalizedValue={months.reduce((sum, month) => sum + (month.month <= currentMonth ? month.actual : 0), 0)} + value={months.reduce((sum, month) => sum + (isMonthInPast(month.month) ? month.actual : 0), 0)} + normalizedValue={months.reduce((sum, month) => sum + (isMonthInPast(month.month) ? month.actual : 0), 0)} normalized={false} className="border-x border-gray-400" /> @@ -314,8 +322,19 @@ export default function YearlyForecast2025() { if (error) return
Error loading data: {error.message}
; const forecast = data?.yearlyForecast; + const today = new Date(); + const currentMonth = today.getMonth() + 1; + const currentYear = today.getFullYear(); + + function isMonthInPast(month: number) { + if (month === 12) { + return currentYear > 2024 || (currentYear === 2024 && currentMonth >= 12); + } + return currentYear > 2025 || (currentYear === 2025 && currentMonth >= month); + } + const totalActual = forecast.byMonth.reduce((sum: number, month: any) => - sum + (month.month <= new Date().getMonth() + 1 ? month.actual : 0), 0 + sum + (isMonthInPast(month.month) ? month.actual : 0), 0 ); const remaining = forecast.goal - totalActual; From 6a49abeed66c43dcd422fce2bdfc8e3436350e92 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 1 Jan 2025 08:44:36 -0300 Subject: [PATCH 3/4] Enhance Yearly Forecast with Working Days Metrics - Added `workingDays` and `realizedWorkingDays` fields to the YearlyForecast GraphQL schema, improving the data model for financial forecasting. - Updated the `resolve_yearly_forecast` function to calculate total working days and realized working days based on the current date, enhancing the accuracy of the forecast. - Modified the frontend to display working days metrics in the Yearly Forecast 2025 component, providing users with better insights into their financial goals. These changes improve the financial forecasting capabilities and enhance user visibility into working day metrics. --- backend/api/src/analytics/schema.graphql | 2 ++ backend/api/src/analytics/yearly_forecast.py | 19 ++++++++++++++++++- frontend/src/app/financial/2025/page.tsx | 13 ++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) 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 ad605fae..2ad98f6a 100644 --- a/backend/api/src/analytics/yearly_forecast.py +++ b/backend/api/src/analytics/yearly_forecast.py @@ -52,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/frontend/src/app/financial/2025/page.tsx b/frontend/src/app/financial/2025/page.tsx index 318fc77c..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 @@ -347,7 +349,7 @@ export default function YearlyForecast2025() { Yearly Forecast {forecast.year} -
+
Annual Goal
{formatCurrency(forecast.goal)}
@@ -361,6 +363,15 @@ export default function YearlyForecast2025() {
+
+
Working Days
+
+ {forecast.realizedWorkingDays}/{forecast.workingDays} +
+ {((forecast.realizedWorkingDays / forecast.workingDays) * 100).toFixed(1)}% +
+
+
Remaining
From 06fdb665f259f696c106cfbd86a93b96bf8133f0 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 1 Jan 2025 09:01:06 -0300 Subject: [PATCH 4/4] Refactor Forecast Calculations and Enhance Consultant Summary Logic - Updated the forecast calculation logic in `forecast.py` to prevent division by zero by introducing a safe divisor for projected values, improving accuracy in financial forecasts. - Enhanced the `ConsultantSummary` class in `revenue_tracking.py` to safely check for the presence of `by_worker` in project data, ensuring robust data handling and preventing potential errors. - Consolidated consultant name collection logic to improve readability and maintainability. These changes enhance the reliability of forecast calculations and improve the overall data integrity in consultant summaries. --- .../src/omni_models/analytics/forecast.py | 6 ++-- .../omni_models/analytics/revenue_tracking.py | 28 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) 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):