From 276b8ad415ab4d911219138afd34c97133128f0e Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Mon, 6 Jan 2025 16:35:59 -0300 Subject: [PATCH 1/8] Refactor revenue forecasting logic and update global variables - Commented out unused revenue tracking calculations in `yearly_forecast.py` to streamline the forecasting process. - Replaced `compute_revenue_tracking` with `compute_forecast` for improved accuracy in revenue predictions. - Updated the handling of global variables in `globals.py` to ensure they are initialized only if not already defined, enhancing code robustness. - Removed unnecessary updates to global variables in `app.py` to simplify application startup. These changes enhance the clarity and efficiency of the revenue forecasting functionality. --- backend/api/src/analytics/yearly_forecast.py | 228 ++++++++++--------- backend/api/src/app.py | 2 +- backend/shared/src/omni_shared/globals.py | 16 +- 3 files changed, 134 insertions(+), 112 deletions(-) diff --git a/backend/api/src/analytics/yearly_forecast.py b/backend/api/src/analytics/yearly_forecast.py index 2ad98f6a..a93f4ec0 100644 --- a/backend/api/src/analytics/yearly_forecast.py +++ b/backend/api/src/analytics/yearly_forecast.py @@ -1,5 +1,5 @@ from omni_utils.helpers.dates import get_working_days_in_month -from omni_models.analytics.revenue_tracking import compute_revenue_tracking +from omni_models.analytics.forecast import compute_forecast from omni_utils.helpers.dates import get_last_day_of_month import calendar @@ -23,33 +23,37 @@ def resolve_yearly_forecast(_, info, year=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) + #expected_consulting_fee = get_expected_regular_consulting_revenue(y, m) + #expected_pre_contracted_revenue = get_expected_pre_contracted_revenue(y, m) + last_day_of_month = get_last_day_of_month(datetime(y, m, 1)) + date_of_interest = datetime(y, m, 15) 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): - 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"] + forecast = compute_forecast(current_date) + forecast_doi = forecast else: - revenue_tracking = None + forecast = compute_forecast(last_day_of_month) + forecast_doi = compute_forecast(date_of_interest) - actual += revenue_tracking["total"] if revenue_tracking else 0 + + if y < current_year or (y == current_year and m < current_month): + discount = forecast + #actual += revenue_tracking["total"] if (y != current_year or m != current_month) else 0 + by_month.append({ "month": m, "goal": main_goal / (13 - month), "working_days": len(get_working_days_in_month(y, m)), - "expected_consulting_fee": expected_consulting_fee, - "expected_squad_fee": expected_pre_contracted_revenue["squad"], - "expected_hands_on_fee": expected_pre_contracted_revenue["hands_on"], - "expected_consulting_pre_fee": expected_pre_contracted_revenue["consulting_pre"], + "expected_consulting_fee": forecast_doi["by_kind"]["consulting"]['totals'].expected, + "expected_squad_fee": forecast_doi["by_kind"]["squad"]['totals'].in_analysis, + "expected_hands_on_fee": forecast_doi["by_kind"]["hands_on"]['totals'].in_analysis, + "expected_consulting_pre_fee": forecast_doi["by_kind"]["consulting_pre"]['totals'].in_analysis, "actual": revenue_tracking["total"] if revenue_tracking else 0 }) - main_goal -= discount + #main_goal -= discount total_working_days = sum(len(get_working_days_in_month(y, m)) for m in range(1, 13)) @@ -76,123 +80,131 @@ def resolve_yearly_forecast(_, info, year=None): } -def get_expected_regular_consulting_revenue(year, month): +# def get_expected_regular_consulting_revenue(year, month): - result = 0 +# result = 0 - cases = [ - case - for case in globals.omni_models.cases.get_all().values() - if case.is_active - ] +# cases = [ +# case +# for case in globals.omni_models.cases.get_all().values() +# if case.is_active +# ] - for case in cases: - wah = case.weekly_approved_hours +# for case in cases: +# wah = case.weekly_approved_hours - if not wah: - continue +# if not wah: +# continue - project_ = None - for project_info in case.tracker_info: - if project_info.kind == 'consulting' and project_info.rate and project_info.rate.rate: - project_ = project_info - break +# project_ = None +# for project_info in case.tracker_info: +# if project_info.kind == 'consulting' and project_info.rate and project_info.rate.rate: +# project_ = project_info +# break - if not project_: - continue +# if not project_: +# continue - working_days_in_month = get_working_days_in_month(year, month) - days_in_month = calendar.monthrange(year, month)[1] +# working_days_in_month = get_working_days_in_month(year, month) +# days_in_month = calendar.monthrange(year, month)[1] - hours_in_month = 0 - daily_approved_hours = wah / 5 +# hours_in_month = 0 +# daily_approved_hours = wah / 5 - due_on = project_.due_on.date() if project_ and project_.due_on else case.end_of_contract +# due_on = project_.due_on.date() if project_ and project_.due_on else case.end_of_contract - for day in range(1, days_in_month + 1): - date = datetime(year, month, day) +# for day in range(1, days_in_month + 1): +# date = datetime(year, month, day) - if case.start_of_contract and date.date() < case.start_of_contract: - continue +# if case.start_of_contract and date.date() < case.start_of_contract: +# continue - if due_on and date.date() > due_on: - break +# if due_on and date.date() > due_on: +# break - if date in working_days_in_month: - hours_in_month += daily_approved_hours +# if date in working_days_in_month: +# hours_in_month += daily_approved_hours - result += hours_in_month * (project_.rate.rate / 100) +# result += hours_in_month * (project_.rate.rate / 100) - return result +# return result -def get_expected_pre_contracted_revenue(year, month): - - cases = [ - case - for case in globals.omni_models.cases.get_all().values() - if case.is_active - ] - - consulting_pre = 0 - hands_on = 0 - squad = 0 - - for case in cases: - start = case.start_of_contract # .replace(day=1) - if start is None: - start = datetime(year, month, 1) - else: - start = start.replace(day=1) +# def get_expected_pre_contracted_revenue(year, month): + +# cases = [ +# case +# for case in globals.omni_models.cases.get_all().values() +# if case.is_active +# ] + +# consulting_pre = 0 +# hands_on = 0 +# squad = 0 + +# for case in cases: +# start = case.start_of_contract # .replace(day=1) +# if start is None: +# start = datetime(year, month, 1) +# else: +# start = start.replace(day=1) - end = case.end_of_contract - if end is None: - end = datetime(year, month, calendar.monthrange(year, month)[1]) +# end = case.end_of_contract +# if end is None: +# end = datetime(year, month, calendar.monthrange(year, month)[1]) - in_contract = start.year <= year <= end.year - if in_contract and year == start.year: - in_contract = month >= start.month +# in_contract = start.year <= year <= end.year +# if in_contract and year == start.year: +# in_contract = month >= start.month - if in_contract and year == end.year: - in_contract = month <= end.month +# if in_contract and year == end.year: +# in_contract = month <= end.month - if not in_contract: - continue +# if not in_contract: +# continue - for project_info in case.tracker_info: - if project_info.billing and project_info.billing.fee and project_info.billing.fee != 0: - if project_info.budget and project_info.budget.period == 'general': - if start.year == end.year: - number_of_months = end.month - start.month + 1 - else: - months_on_start_year = 12 - start.month + 1 - months_on_end_year = end.month - if end.year - start.year > 1: - number_of_months = months_on_start_year + months_on_end_year + (end.year - start.year - 1) * 12 - else: - number_of_months = months_on_start_year + months_on_end_year +# for project_info in case.tracker_info: +# if project_info.billing and project_info.billing.fee and project_info.billing.fee != 0: +# if project_info.budget and project_info.budget.period == 'general': +# if start.year == end.year: +# number_of_months = end.month - start.month + 1 +# else: +# months_on_start_year = 12 - start.month + 1 +# months_on_end_year = end.month +# if end.year - start.year > 1: +# number_of_months = months_on_start_year + months_on_end_year + (end.year - start.year - 1) * 12 +# else: +# number_of_months = months_on_start_year + months_on_end_year - fee = project_info.billing.fee / 100 / number_of_months +# fee = project_info.billing.fee / 100 / number_of_months - if project_info.kind == 'consulting': - consulting_pre += fee - elif project_info.kind == 'squad': - squad += fee - else: # hands_on - hands_on += fee +# if project_info.kind == 'consulting': +# consulting_pre += fee +# elif project_info.kind == 'squad': +# squad += fee +# else: # hands_on +# hands_on += fee - else: - fee = project_info.billing.fee / 100 - if project_info.kind == 'consulting': - consulting_pre += fee - elif project_info.kind == 'squad': - squad += fee - else: # hands_on - hands_on += fee +# else: +# fee = project_info.billing.fee / 100 +# date_of_interest = datetime(year, month, 5) + +# if project_info.created_at > date_of_interest: +# fee = 0 + +# if project_info.due_on and (project_info.due_on.date() if hasattr(project_info.due_on, 'date') else project_info.due_on) < (date_of_interest.date() if hasattr(date_of_interest, 'date') else date_of_interest): +# fee = 0 + +# if project_info.kind == 'consulting': +# consulting_pre += fee +# elif project_info.kind == 'squad': +# squad += fee +# else: # hands_on +# hands_on += fee - return { - "consulting_pre": consulting_pre, - "hands_on": hands_on, - "squad": squad - } +# return { +# "consulting_pre": consulting_pre, +# "hands_on": hands_on, +# "squad": squad +# } diff --git a/backend/api/src/app.py b/backend/api/src/app.py index 18e92923..eac678c6 100644 --- a/backend/api/src/app.py +++ b/backend/api/src/app.py @@ -108,6 +108,6 @@ def graphql_server(): app.logger.setLevel(logging.INFO) app.logger.info("Starting the application") - globals.update() + # globals.update() app.run(debug=args.verbose,host="0.0.0.0",port=5001) diff --git a/backend/shared/src/omni_shared/globals.py b/backend/shared/src/omni_shared/globals.py index 32c9385d..e73a699a 100644 --- a/backend/shared/src/omni_shared/globals.py +++ b/backend/shared/src/omni_shared/globals.py @@ -2,10 +2,20 @@ from omni_models.omnidatasets import OmniDatasets from datetime import datetime -omni_models: OmniModels = OmniModels() -omni_datasets: OmniDatasets = OmniDatasets(omni_models) -last_update_time = None +try: + omni_models +except NameError: + omni_models = OmniModels() + +try: + omni_datasets +except NameError: + omni_datasets = OmniDatasets(omni_models) +try: + last_update_time +except NameError: + last_update_time = datetime.now() def update(): global omni_models From 2046e024a21d77169455e194f696b4bca15a7298 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Mon, 6 Jan 2025 17:02:47 -0300 Subject: [PATCH 2/8] Enhance TimesheetDataset date filtering and improve Pipedrive API error handling - Added a check to ensure the DataFrame is not empty before applying date filters in the TimesheetDataset class, preventing potential errors. - Implemented a retry mechanism for handling rate limit errors (HTTP 429) in the Pipedrive API client, allowing for up to three retries with a delay, improving robustness against temporary API restrictions. - Updated caching behavior for the fetch_people method to include a remember parameter, enhancing data retrieval efficiency. These changes improve data handling and API interaction reliability. --- .../datasets/timesheet_dataset/main.py | 5 ++-- .../omni_models/syntactic/pipedrive/client.py | 27 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/backend/models/src/omni_models/datasets/timesheet_dataset/main.py b/backend/models/src/omni_models/datasets/timesheet_dataset/main.py index 9105dd47..1e912913 100644 --- a/backend/models/src/omni_models/datasets/timesheet_dataset/main.py +++ b/backend/models/src/omni_models/datasets/timesheet_dataset/main.py @@ -50,8 +50,9 @@ def get(self, after: datetime, before: datetime) -> SummarizablePowerDataFrame: first_day_of_month = last_day_of_month + timedelta(days=1) - df = df[df['Date'] >= after.date()] - df = df[df['Date'] <= before.date()] + if len(df) > 0: + df = df[df['Date'] >= after.date()] + df = df[df['Date'] <= before.date()] return SummarizablePowerDataFrame(df) diff --git a/backend/models/src/omni_models/syntactic/pipedrive/client.py b/backend/models/src/omni_models/syntactic/pipedrive/client.py index db59473d..7d606d6b 100644 --- a/backend/models/src/omni_models/syntactic/pipedrive/client.py +++ b/backend/models/src/omni_models/syntactic/pipedrive/client.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import List import requests +import time from omni_utils.decorators.cache import cache from .models.activity import Activity @@ -27,9 +28,24 @@ def __fetch(self, entity, start=0, params=None): params['limit'] = 500 params['start'] = start - response = self.session.get(url, params=params) - response.raise_for_status() # Proper error handling - return response.json() + retries = 0 + max_retries = 3 + + while True: + try: + response = self.session.get(url, params=params) + response.raise_for_status() # Proper error handling + return response.json() + except requests.exceptions.HTTPError as e: + if e.response.status_code == 429: # Too Many Requests + if retries >= max_retries: + print(f"Maximum retry attempts ({max_retries}) reached for rate limit. Raising exception...") + raise + retries += 1 + print(f"Rate limit exceeded for Pipedrive API. Waiting 2 seconds before retry {retries}/{max_retries}...") + time.sleep(2) + continue + raise # Re-raise other HTTP errors @staticmethod def __has_next_page(data): @@ -51,6 +67,9 @@ def _fetch_all(self, entity, params=None): # Create DataFrame from all data at once return all_data + + @cache(remember=True) + @cache def fetch_active_deals_in_stage(self, stage_id, status ='open'): @@ -80,7 +99,7 @@ def fetch_activities(self, starting: datetime, ending: datetime): for activity in json ] - @cache + @cache(remember=True) def fetch_people(self): params = { 'fields': 'picture_id' From d910e31d8c267f9cf83d9967f246ce1b38876285 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Tue, 7 Jan 2025 06:07:01 -0300 Subject: [PATCH 3/8] Refactor revenue tracking logic to improve case handling and date comparisons - Enhanced the `_compute_revenue_tracking_base` function to ensure active cases are accurately determined based on contract dates, improving the reliability of revenue calculations. - Updated the `compute_pre_contracted_revenue_tracking` function to handle date comparisons more robustly, accommodating various date formats for project creation and due dates. - These changes enhance the accuracy of revenue tracking and ensure that only relevant cases are considered in calculations. --- .../omni_models/analytics/revenue_tracking.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/backend/models/src/omni_models/analytics/revenue_tracking.py b/backend/models/src/omni_models/analytics/revenue_tracking.py index 30f3d20f..53285bb3 100644 --- a/backend/models/src/omni_models/analytics/revenue_tracking.py +++ b/backend/models/src/omni_models/analytics/revenue_tracking.py @@ -67,8 +67,26 @@ def _compute_revenue_tracking_base(df: pd.DataFrame, date_of_interest: date, pro if len(df) == 0: return default_result, pro_rata_info - case_ids = df["CaseId"].unique() + case_ids = sorted(set(df["CaseId"].unique())) active_cases = [globals.omni_models.cases.get_by_id(case_id) for case_id in case_ids] + doi = date_of_interest.date() if hasattr(date_of_interest, 'date') else date_of_interest + active_cases = [ + case + for case in globals.omni_models.cases.get_all().values() + if ( + case.is_active + or ( + (not case.start_of_contract or case.start_of_contract <= doi) and + (not case.end_of_contract or case.end_of_contract >= doi) + ) + ) + ] + case_ids2 = sorted(set([case.id for case in active_cases])) + for case_id in case_ids: + if case_id not in case_ids2: + case = globals.omni_models.cases.get_by_id(case_id) + active_cases.append(case) + account_managers_names = sorted(set(_get_account_manager_name(case) for case in active_cases)) by_account_manager = [] @@ -330,10 +348,13 @@ def process_project(date_of_interest: date, case: Case, project, timesheet_df: p elif case.pre_contracted_value: fee = project.billing.fee / 100 - if project.created_at > date_of_interest: + created_at = project.created_at.date() if hasattr(project.created_at, 'date') else project.created_at + date_of_interest = date_of_interest.date() if hasattr(date_of_interest, 'date') else date_of_interest + if created_at > date_of_interest: fee = 0 - if project.due_on and (project.due_on.date() if hasattr(project.due_on, 'date') else project.due_on) < (date_of_interest.date() if hasattr(date_of_interest, 'date') else date_of_interest): + due_on = project.due_on.date() if hasattr(project.due_on, 'date') else project.due_on + if due_on and due_on < date_of_interest: fee = 0 should_do_pro_rata = ( From 67ed6dbe7c9046ade77af0710d76c1ef0050be0a Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Tue, 7 Jan 2025 06:26:13 -0300 Subject: [PATCH 4/8] Refactor revenue tracking logic to enhance date handling and fee calculations - Updated the `compute_pre_contracted_revenue_tracking` function to improve date comparisons for project creation and due dates, ensuring accurate fee calculations. - Removed redundant checks for empty project DataFrames and streamlined logic to handle cases where no timesheet entries exist. - These changes enhance the reliability of revenue tracking by ensuring only relevant projects are considered based on their dates. --- .../omni_models/analytics/revenue_tracking.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/backend/models/src/omni_models/analytics/revenue_tracking.py b/backend/models/src/omni_models/analytics/revenue_tracking.py index 53285bb3..35c964dd 100644 --- a/backend/models/src/omni_models/analytics/revenue_tracking.py +++ b/backend/models/src/omni_models/analytics/revenue_tracking.py @@ -297,6 +297,16 @@ def compute_pre_contracted_revenue_tracking( def process_project(date_of_interest: date, case: Case, project, timesheet_df: pd.DataFrame, pro_rata_info): project_df = timesheet_df[timesheet_df["ProjectId"] == project.id] result = None + + created_at = project.created_at.date() if hasattr(project.created_at, 'date') else project.created_at + date_of_interest = date_of_interest.date() if hasattr(date_of_interest, 'date') else date_of_interest + if created_at > date_of_interest: + return None + + due_on = project.due_on.date() if hasattr(project.due_on, 'date') else project.due_on + if due_on and due_on < date_of_interest: + return None + if project.billing and project.billing.fee and project.billing.fee != 0: if project.budget and project.budget.period == 'general': @@ -348,15 +358,6 @@ def process_project(date_of_interest: date, case: Case, project, timesheet_df: p elif case.pre_contracted_value: fee = project.billing.fee / 100 - created_at = project.created_at.date() if hasattr(project.created_at, 'date') else project.created_at - date_of_interest = date_of_interest.date() if hasattr(date_of_interest, 'date') else date_of_interest - if created_at > date_of_interest: - fee = 0 - - due_on = project.due_on.date() if hasattr(project.due_on, 'date') else project.due_on - if due_on and due_on < date_of_interest: - fee = 0 - should_do_pro_rata = ( case.start_of_contract and case.start_of_contract.year == date_of_interest.year @@ -384,8 +385,6 @@ def process_project(date_of_interest: date, case: Case, project, timesheet_df: p } else: project_df = timesheet_df[timesheet_df["ProjectId"] == project.id] - if len(project_df) == 0: - return None partial = False fee = project.billing.fee / 100 @@ -543,7 +542,7 @@ def process_project(date_of_interest: date, case: Case, project, timesheet_df: p "kind": project.kind, "name": project.name, "fee": partial_fee if partial else fee, - "hours": project_df["TimeInHs"].sum(), + "hours": project_df["TimeInHs"].sum() if len(project_df) > 0 else 0, "partial": partial, "fixed": True } From a30d614e2e4e948df358813dd989144c66a2b416 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Tue, 7 Jan 2025 07:39:30 -0300 Subject: [PATCH 5/8] Refactor date handling and case logic in forecast and revenue tracking modules - Updated the `compute_forecast` function to improve date handling by ensuring `due_on` is correctly assigned based on the presence of a date attribute. - Enhanced the `_compute_revenue_tracking_base` function to streamline contract date checks for active cases, improving the accuracy of revenue calculations. - Simplified the case handling logic in `CasesRepository` by removing redundant checks for project kinds when determining due dates for archived projects. These changes enhance the reliability and clarity of date management across the forecasting and revenue tracking functionalities. --- backend/models/src/omni_models/analytics/forecast.py | 4 +++- backend/models/src/omni_models/analytics/revenue_tracking.py | 4 ++-- backend/models/src/omni_models/domain/cases.py | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/models/src/omni_models/analytics/forecast.py b/backend/models/src/omni_models/analytics/forecast.py index e8afe222..96442e04 100644 --- a/backend/models/src/omni_models/analytics/forecast.py +++ b/backend/models/src/omni_models/analytics/forecast.py @@ -301,8 +301,10 @@ def filter_items(items): hours_in_month = 0 daily_approved_hours = wah / 5 + - due_on = project_.due_on.date() if project_.due_on else case_.end_of_contract + due_on = project_.due_on if project_.due_on else case_.end_of_contract + due_on = due_on.date() if hasattr(due_on, 'date') else due_on for day in range(1, days_in_month + 1): date = datetime(year, month, day) diff --git a/backend/models/src/omni_models/analytics/revenue_tracking.py b/backend/models/src/omni_models/analytics/revenue_tracking.py index 35c964dd..ecd04b89 100644 --- a/backend/models/src/omni_models/analytics/revenue_tracking.py +++ b/backend/models/src/omni_models/analytics/revenue_tracking.py @@ -76,8 +76,8 @@ def _compute_revenue_tracking_base(df: pd.DataFrame, date_of_interest: date, pro if ( case.is_active or ( - (not case.start_of_contract or case.start_of_contract <= doi) and - (not case.end_of_contract or case.end_of_contract >= doi) + (case.end_of_contract and case.end_of_contract >= doi) and + (not case.start_of_contract or case.start_of_contract <= doi) ) ) ] diff --git a/backend/models/src/omni_models/domain/cases.py b/backend/models/src/omni_models/domain/cases.py index 52639f4f..a66f8612 100644 --- a/backend/models/src/omni_models/domain/cases.py +++ b/backend/models/src/omni_models/domain/cases.py @@ -298,7 +298,7 @@ def __build_data(self) -> Dict[str, Case]: for case in cases_dict.values(): for tracker_project in case.tracker_info: - if tracker_project.status == 'archived' and tracker_project.kind != 'consulting': + if tracker_project.status == 'archived': if not tracker_project.due_on: due_on = None if tracker_project.name.endswith("- 2024"): @@ -306,7 +306,8 @@ def __build_data(self) -> Dict[str, Case]: elif case.end_of_contract: due_on = case.end_of_contract elif case.is_active: - due_on = self.tracker.find_project_due_on(tracker_project.id) + if tracker_project.kind != 'consulting': + due_on = self.tracker.find_project_due_on(tracker_project.id) if due_on: tracker_project.due_on = due_on From 52491664d601bc710cc234a89adb6ee6e06f8289 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Tue, 7 Jan 2025 10:50:57 -0300 Subject: [PATCH 6/8] Refactor yearly forecast and revenue tracking logic for improved calculations - Updated the `resolve_yearly_forecast` function to ensure accurate goal calculations by handling cases where the month is equal to the current month, setting the goal to zero when necessary. - Enhanced the `compute_pre_contracted_revenue_tracking` function to prevent errors by ensuring the project DataFrame is only processed if it contains entries, improving robustness in revenue tracking. These changes enhance the accuracy and reliability of forecasting and revenue calculations. --- backend/api/src/analytics/yearly_forecast.py | 5 +++-- backend/models/src/omni_models/analytics/revenue_tracking.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/api/src/analytics/yearly_forecast.py b/backend/api/src/analytics/yearly_forecast.py index a93f4ec0..383998d0 100644 --- a/backend/api/src/analytics/yearly_forecast.py +++ b/backend/api/src/analytics/yearly_forecast.py @@ -36,15 +36,16 @@ def resolve_yearly_forecast(_, info, year=None): forecast = compute_forecast(last_day_of_month) forecast_doi = compute_forecast(date_of_interest) - + goal = main_goal / (13 - month) if y < current_year or (y == current_year and m < current_month): + goal = 0 discount = forecast #actual += revenue_tracking["total"] if (y != current_year or m != current_month) else 0 by_month.append({ "month": m, - "goal": main_goal / (13 - month), + "goal": goal, "working_days": len(get_working_days_in_month(y, m)), "expected_consulting_fee": forecast_doi["by_kind"]["consulting"]['totals'].expected, "expected_squad_fee": forecast_doi["by_kind"]["squad"]['totals'].in_analysis, diff --git a/backend/models/src/omni_models/analytics/revenue_tracking.py b/backend/models/src/omni_models/analytics/revenue_tracking.py index ecd04b89..d2e7eb29 100644 --- a/backend/models/src/omni_models/analytics/revenue_tracking.py +++ b/backend/models/src/omni_models/analytics/revenue_tracking.py @@ -295,7 +295,7 @@ def compute_pre_contracted_revenue_tracking( account_manager_name_or_slug: str = None ): def process_project(date_of_interest: date, case: Case, project, timesheet_df: pd.DataFrame, pro_rata_info): - project_df = timesheet_df[timesheet_df["ProjectId"] == project.id] + project_df = timesheet_df[timesheet_df["ProjectId"] == project.id] if len(timesheet_df) > 0 else pd.DataFrame() result = None created_at = project.created_at.date() if hasattr(project.created_at, 'date') else project.created_at From 386166bdd7f515e6f2a24beef02ebe509faefc00 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Tue, 7 Jan 2025 11:21:43 -0300 Subject: [PATCH 7/8] Refactor revenue tracking logic to improve DataFrame handling and streamline calculations - Updated the `_compute_revenue_tracking_base`, `compute_regular_revenue_tracking`, and `compute_pre_contracted_revenue_tracking` functions to ensure DataFrames are only processed when they contain entries, preventing potential errors. - Simplified conditional checks for empty DataFrames, enhancing the robustness of revenue calculations. - These changes improve the reliability and accuracy of revenue tracking by ensuring only relevant data is considered in calculations. --- .../omni_models/analytics/revenue_tracking.py | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/backend/models/src/omni_models/analytics/revenue_tracking.py b/backend/models/src/omni_models/analytics/revenue_tracking.py index d2e7eb29..ca44e18e 100644 --- a/backend/models/src/omni_models/analytics/revenue_tracking.py +++ b/backend/models/src/omni_models/analytics/revenue_tracking.py @@ -34,40 +34,21 @@ def _compute_revenue_tracking_base(df: pd.DataFrame, date_of_interest: date, pro } current_day = current_day + timedelta(days=1) - - default_result = { - "monthly": { - "total": 0, - "total_consulting_fee": 0, - "total_consulting_fee_new": 0, - "total_consulting_pre_fee": 0, - "total_consulting_hours": 0, - "total_consulting_pre_hours": 0, - "total_hands_on_fee": 0, - "total_squad_fee": 0, - "by_account_manager": [] - }, - "daily": [] - } - pro_rata_info = { "by_kind": [] } - if df is None or len(df) == 0: - return default_result, pro_rata_info - df = df[df["Kind"] != INTERNAL_KIND] - if len(df) == 0: - return default_result, pro_rata_info + df = df[df["Kind"] != INTERNAL_KIND] if len(df) > 0 else df + # if len(df) == 0: + # return default_result, pro_rata_info - if account_manager_name_or_slug: + if account_manager_name_or_slug and len(df) > 0: df_ = df[df["AccountManagerName"] == account_manager_name_or_slug] if len(df_) == 0: df_ = df[df["AccountManagerSlug"] == account_manager_name_or_slug] df = df_ - if len(df) == 0: - return default_result, pro_rata_info + - case_ids = sorted(set(df["CaseId"].unique())) + case_ids = sorted(set(df["CaseId"].unique())) if len(df) > 0 else [] active_cases = [globals.omni_models.cases.get_by_id(case_id) for case_id in case_ids] doi = date_of_interest.date() if hasattr(date_of_interest, 'date') else date_of_interest active_cases = [ @@ -261,7 +242,7 @@ def compute_regular_revenue_tracking( ): def process_project(date_of_interest: date, _, project, timesheet_df, pro_rata_info): if project.rate and project.rate.rate: - project_df = timesheet_df[timesheet_df["ProjectId"] == project.id] + project_df = timesheet_df[timesheet_df["ProjectId"] == project.id] if len(timesheet_df) > 0 else pd.DataFrame() if len(project_df) > 0: by_worker = [] for worker_name in project_df["WorkerName"].unique(): @@ -384,7 +365,7 @@ def process_project(date_of_interest: date, case: Case, project, timesheet_df: p "fixed": True } else: - project_df = timesheet_df[timesheet_df["ProjectId"] == project.id] + project_df = timesheet_df[timesheet_df["ProjectId"] == project.id] if len(timesheet_df) > 0 else pd.DataFrame() partial = False fee = project.billing.fee / 100 @@ -402,7 +383,7 @@ def process_project(date_of_interest: date, case: Case, project, timesheet_df: p if is_last_day_of_month: - workers_hours = project_df.groupby("WorkerName")["TimeInHs"].sum().reset_index() + workers_hours = project_df.groupby("WorkerName")["TimeInHs"].sum().reset_index() if len(project_df) > 0 else pd.DataFrame() if len(workers_hours) == 0: return None From 18158f4c5a1a5483763e431bf65ecfabb5363a13 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Tue, 7 Jan 2025 13:14:26 -0300 Subject: [PATCH 8/8] Refactor yearly forecast calculations to improve accuracy and clarity - Updated the `resolve_yearly_forecast` function to correctly handle actual revenue tracking for the current month, ensuring accurate goal calculations. - Introduced a new variable `month_actual` to store realized revenue, enhancing the clarity of the calculations. - Adjusted the logic for discount calculations to reflect the actual revenue, improving the reliability of the forecasting process. These changes enhance the accuracy and reliability of yearly forecasting and revenue tracking. --- backend/api/src/analytics/yearly_forecast.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/api/src/analytics/yearly_forecast.py b/backend/api/src/analytics/yearly_forecast.py index 383998d0..ea3642f9 100644 --- a/backend/api/src/analytics/yearly_forecast.py +++ b/backend/api/src/analytics/yearly_forecast.py @@ -37,10 +37,17 @@ def resolve_yearly_forecast(_, info, year=None): forecast_doi = compute_forecast(date_of_interest) goal = main_goal / (13 - month) + month_actual = 0 if y < current_year or (y == current_year and m < current_month): goal = 0 - discount = forecast - #actual += revenue_tracking["total"] if (y != current_year or m != current_month) else 0 + + month_actual = forecast["summary"]["realized"] + discount = month_actual + actual += month_actual + elif y == current_year and m == current_month: + month_actual = forecast["summary"]["realized"] + # discount = month_actual + actual += month_actual by_month.append({ @@ -51,10 +58,10 @@ def resolve_yearly_forecast(_, info, year=None): "expected_squad_fee": forecast_doi["by_kind"]["squad"]['totals'].in_analysis, "expected_hands_on_fee": forecast_doi["by_kind"]["hands_on"]['totals'].in_analysis, "expected_consulting_pre_fee": forecast_doi["by_kind"]["consulting_pre"]['totals'].in_analysis, - "actual": revenue_tracking["total"] if revenue_tracking else 0 + "actual": month_actual }) - #main_goal -= discount + main_goal -= discount total_working_days = sum(len(get_working_days_in_month(y, m)) for m in range(1, 13))