This project was based on a request to generate a forecase of hours based off
a set of simple assumptions about how quickly hours would be used depending on
what point in the project's duration we are in.
There are a few other components to the project
related to importing data from a budget template,
unpivoting the data, and creating records that correspond
to the different staff on the project.
Then, the forecast class takes the project start
and end dates and the number of hours allocated to
each staff person and spreads them out across the
project in order to create a baseline forecast.
public class Forecast {
/**
* Gets the weighted percentage based on what percentage of the time the day falls on
*
* @param dayPercent The percentage of the span the day falls on
* @param dayHours The number of hours allocated
* @return The weighted number of hours
the projected hours amount is the based on a burn rate assumption
//First Q: Date <= 25% of total = +10%
//Second Q: Date <= 25% of total = -10%
//Third Q: Date <= 25% of total = -10%
//Fourth Q: Date <= 25% of total = +10%
*/
@TestVisible
private static Decimal getWeightedPercentage(Decimal dayPercent, Decimal dayHours) {
if (
dayPercent <= 0 ||
dayPercent >= 1
) {
return 0;
}
Decimal multiplier = 1.10;
if (
dayPercent >= 0.25 &&
dayPercent < 0.5
) {
multiplier = 0.90;
}
return dayHours * multiplier;
}
/**
* Gets all of the applicable budget detail records
*
* @param ids The ids to fetch
* @return The budget detail records
*/
@TestVisible
private static List getBudgetDetailsForId(Set ids) {
return [
select Budget__c,
Budget__r.Period_of_Performance_Start__c,
Budget__r.Period_of_Performance_End__c,
Role__c,
Total_per_role__c,
Total_Value__c
from Budget_Detail__c
where Id in :ids
];
}
/**
* Gets the key for the day map based on the date
*
* @param d The date to generate a key from
* @return The key
*/
@TestVisible
private static String getDayMapKey(Date d) {
Datetime dt = d;
return dt.format('yyyy-MM-dd');
}
/**
* Creates forcast records for a list of budget detail ids
*
* @param staffList The list of ids
*/
@future
public static void createForecast (List staffList) {
List forecastInsert = new List();
for (Budget_Detail__c bs : getBudgetDetailsForId(new Set(staffList))) {
Date startDate = bs.Budget__r.Period_of_Performance_Start__c;
Date offsetStart = startDate.toStartOfMonth();
Date endDate = bs.Budget__r.Period_of_Performance_End__c;
Date offsetEnd = endDate.addMonths(1).toStartOfMonth().addDays(-1);
Integer offsetDiff = offsetStart.daysBetween(startDate);
Integer budgetDays = startDate.daysBetween(endDate) + 1;
Integer days = offsetStart.daysBetween(offsetEnd) + 1;
Decimal dayHours = (bs.Total_per_role__c / budgetDays).setScale(3);
Map dayMap = new Map();
for (Integer i = 0; i <= days; i += 1) {
Decimal iDecimal = Decimal.valueOf(i);
Decimal ioffsetDiff = Decimal.valueOf(offsetDiff);
Decimal dayPercent = ((iDecimal - ioffsetDiff) / budgetDays).setScale(3);
String key = getDayMapKey(offsetStart.addDays(i));
dayMap.put(key, getWeightedPercentage(dayPercent, dayHours));
}
Integer months = offsetStart.monthsBetween(offsetEnd) + 1;
for (Integer m = 0; m < months; m += 1) {
Date forecastDate = offsetStart.addMonths(m);
Decimal projectedHours = 0;
Integer monthNumber = forecastDate.month();
Integer yearNumber = forecastDate.year();
Integer monthDays = Date.daysInMonth(yearNumber, monthNumber);
for (Integer a = 0; a < monthDays; a += 1) {
String key = getDayMapKey(forecastDate.addDays(a));
projectedHours += dayMap.get(key);
}
forecastInsert.add(new Forecast__c(
Budget_Detail__c = bs.Id,
Date__c = forecastDate,
Projected_Hours__c = projectedHours
});
}
}
insert forecastInsert;
}
}