1

We are selling products in subscription and would like to display to our customers price per year in our quote. To do this, I am trying to allocate QuoteLineItem Total Price over "contract year" by splitting the Total Price by year using an apex trigger.

For instance :

Product  | Start Date | End Date  | Total Price    
Product1 | 6/1/2019   | 5/31/2021 | 300,000.00

Year 1 = 150,000.00   
Year 2 = 150,000.00

Easy one. Now let's assume we have several products with second one starting later.

Product  | Start Date | End Date  | Total Price    
Product1 | 6/1/2019   | 5/31/2021 | 300,000.00    
Product2 | 6/1/2020   | 5/31/2021 | 300,000.00

Year 1 = (300,000 / 2) = 150,000.00    
Year 2 = (300,000 / 2) + 300,000 = 450,000.00

It could possibly not start the first of a month :

Product  | Start Date | End Date  | Total Price    
Product1 | 6/1/2019   | 5/31/2021 | 300,000.00    
Product2 | 3/15/2020   | 5/31/2021 | 363,000.00

Year 1 = (300,000 / 2) + 63,000 = 213,000.00
Year 2 = (300,000 / 2) + 300,000 = 450,000.00

Here is some coding attempt using a per days calculation but we have decimal problems and leap year problems :

// Find the earliest start date from QuoteLineItem
Date firstStartDate = null;
for(QuoteLineItem qli : qlis) {
    if(firstStartDate == null || qli.StartDate__c < firstStartDate) {
        firstStartDate = qli.StartDate__c;
    }
}
// Loop on all QuoteLineItem updated
for(QuoteLineItem qli : qlis) {
    // Create Total map if new Quote
    if(!totalByQuote.containsKey(qli.QuoteId)) {
        totalByQuote.put(qli.QuoteId, new Map<Integer, Decimal>());
    }
    // We can only caculate if we have both dates
    if(qli.StartDate__c != null && qli.EndDate__c != null) {
        Date startDateYear1 = firstStartDate;
        Date startDateYear2 = startDateYear1.addYears(1);
        Date startDateYear3 = startDateYear2.addYears(1);
        Date startDateYear4 = startDateYear3.addYears(1);
        Date startDateYear5 = startDateYear4.addYears(1);
        Date startDateYear6 = startDateYear5.addYears(1);
        Date startDateYear7 = startDateYear6.addYears(1);

        Integer nbDays = qli.StartDate__c.daysBetween(qli.EndDate__c);
        Decimal pricePerDays = (qli.TotalPrice / nbDays);

        Integer nbDaysInYear1 = qli.StartDate__c.daysBetween(startDateYear2);
        Decimal year1 = pricePerDays * nbDaysInYear1;

        Integer nbDaysInYear2 = startDateYear2.daysBetween(startDateYear3);
        Decimal year2 = pricePerDays * nbDaysInYear2;

        Integer nbDaysInYear3 = startDateYear3.daysBetween(startDateYear4);
        Decimal year3 = pricePerDays * nbDaysInYear3;

        Integer nbDaysInYear4 = startDateYear4.daysBetween(startDateYear5);
        Decimal year4 = pricePerDays * nbDaysInYear4;

        Integer nbDaysInYear5 = startDateYear5.daysBetween(startDateYear6);
        Decimal year5 = pricePerDays * nbDaysInYear5;

        Integer nbDaysInYear6 = startDateYear6.daysBetween(startDateYear7);
        Decimal year6 = pricePerDays * nbDaysInYear6;

        System.debug('nbDays - ' + nbDays);
        System.debug('pricePerDays - ' + pricePerDays);
        System.debug('Year 1 - ' + nbDaysInYear1 + ' - ' + year1);
        System.debug('Year 2 - ' + nbDaysInYear2 + ' - ' + year2);
        System.debug('Year 3 - ' + nbDaysInYear3 + ' - ' + year3);
        if(totalByQuote.get(qli.QuoteId).containsKey(1)) {
            totalByQuote.get(qli.QuoteId).put(1, totalByQuote.get(qli.QuoteId).get(1) + year1);
        } else {
            totalByQuote.get(qli.QuoteId).put(1, year1);
        }
        if(totalByQuote.get(qli.QuoteId).containsKey(2)) {
            totalByQuote.get(qli.QuoteId).put(2, totalByQuote.get(qli.QuoteId).get(2) + year2);
        } else {
            totalByQuote.get(qli.QuoteId).put(2, year2);
        }
        if(totalByQuote.get(qli.QuoteId).containsKey(3)) {
            totalByQuote.get(qli.QuoteId).put(3, totalByQuote.get(qli.QuoteId).get(3) + year3);
        } else {
            totalByQuote.get(qli.QuoteId).put(3, year3);
        }
    }
}

1 Answer 1

0

After some work, I finally got a functional code for this. I post it, it will maybe be useful for some one else. And if you see any improvement, please share.

/**
*   Calculate Quote Total Year x fields from QuoteLineItem
*   @param quotes  List of Quote
*   @param qlis  List of QuoteLineItem
*   @return Map Quotes to update
*/
public static List<Quote> calculateTotalYears(List<Quote> quotes, List<QuoteLineItem> qlis) {
    Map<Id, Quote> quotesMap = new Map<Id, Quote>(quotes);
    Map<Id,Map<Integer,Decimal>> totalByQuote = new Map<Id,Map<Integer,Decimal>>();
    // Find the earliest SKU start date from QuoteLineItem
    Date firstStartDate = null;
    for(QuoteLineItem qli : qlis) {
        if(firstStartDate == null || qli.StartDate__c < firstStartDate) {
            firstStartDate = qli.StartDate__c;
        }
    }
    // Store start of the next 7 years after the earliest SKU start date
    Map<Integer, Date> startDatePerYear = new Map<Integer, Date>();
    if(firstStartDate != null) {
        startDatePerYear.put(1, firstStartDate);
        startDatePerYear.put(2, startDatePerYear.get(1).addYears(1));
        startDatePerYear.put(3, startDatePerYear.get(2).addYears(1));
        startDatePerYear.put(4, startDatePerYear.get(3).addYears(1));
        startDatePerYear.put(5, startDatePerYear.get(4).addYears(1));
        startDatePerYear.put(6, startDatePerYear.get(5).addYears(1));
        startDatePerYear.put(7, startDatePerYear.get(6).addYears(1));
    }

    // Loop on all QuoteLineItem updated
    for(QuoteLineItem qli : qlis) {
        // Create Total map if new Quote
        if(!totalByQuote.containsKey(qli.QuoteId)) {
            totalByQuote.put(qli.QuoteId, new Map<Integer, Decimal>());
        }
        // If Product is recurring, split in all years
        if(qli.PricebookEntry.Product2.IsRecurringRevenue__c && ((Quote) quotesMap.get(qli.QuoteId)).BillingFrequency__c == 'Annually') {
            // We can only caculate if we have both dates
            if(qli.StartDate__c != null && qli.EndDate__c != null) {
                Decimal currentAmount = 0, currentDays = 0, amountCompleteYear = 0;
                Integer lastActiveYear = 0;
                Map<Integer, Decimal> daysPerYear = new Map<Integer, Decimal>{1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0, 6 => 0};
                Map<Integer, Decimal> amountPerYear = new Map<Integer, Decimal>{1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0, 6 => 0};

                // Store number of days between dates to calculate amount per days (for leap year, consider as 365 days for exact calculation)
                Decimal totalDays = (qli.StartDate__c.daysBetween(qli.EndDate__c) == 366) ? 365 : (qli.StartDate__c.daysBetween(qli.EndDate__c) + 1 - qli.NumberLeapYears__c);
                // Calculate amount per days
                Decimal amountPerDays = (qli.TotalPrice / totalDays);

                // Calculate amount per year
                for (Integer i = 1; i <= 6; i++){
                    // If first year, store number of days between QuoteLineItem start date and start of next year (for leap year, consider as 365 days for exact calculation)
                    if(i==1) {
                        daysPerYear.put(i, (qli.StartDate__c.daysBetween(startDatePerYear.get(i+1)) == 366) ? 365 : qli.StartDate__c.daysBetween(startDatePerYear.get(i+1)));
                    }
                    // Else, store number of days between start of year and start of next year (for leap year, consider as 365 days for exact calculation)
                    else {
                        daysPerYear.put(i, (startDatePerYear.get(2).daysBetween(startDatePerYear.get(i+1)) == 366) ? 365 : startDatePerYear.get(i).daysBetween(startDatePerYear.get(i+1)));
                    }
                    // If we exceed the number of total days, set year days to remaining days
                    if(currentDays + daysPerYear.get(i) > totalDays) { daysPerYear.put(i, totalDays - currentDays); }
                    // Update number of days "used"
                    currentDays += daysPerYear.get(i);
                    // If complete year, year amount is total yearly price. Else, if not greater than total price, prorate with number of days in year
                    amountPerYear.put(i, (daysPerYear.get(i) == 365) ? qli.TotalYearlyPrice__c : (currentAmount >= qli.TotalPrice) ? 0 : amountPerDays * daysPerYear.get(i));
                    // Update the "used" amount
                    currentAmount += amountPerYear.get(i).setScale(2);
                    // Update amount of complete year (used later to have exact amount)
                    amountCompleteYear += (daysPerYear.get(i) == 365) ? amountPerYear.get(i) : 0;
                    // Set the last active year (used later to have exact amount)
                    lastActiveYear = (amountPerYear.get(i) == 0 && lastActiveYear == 0) ? i - 1 : lastActiveYear;
                }

                Decimal commitTermsYear1, commitTermsLastYear;
                // If first or last year are not complete, calculate it by difference in order to have exact amount
                if(daysPerYear.get(1) != 365 && daysPerYear.get(lastActiveYear) != 365) {
                    // Get number of month (with decimals) of first and last active year
                    commitTermsYear1 = DateUtils.commitTermsBetween(qli.StartDate__c, startDatePerYear.get(2));
                    commitTermsLastYear = DateUtils.commitTermsBetween(startDatePerYear.get(lastActiveYear), qli.EndDate__c);

                    // If both of them are integer (no decimals), just prorate total yearly price
                    if(commitTermsYear1.scale() == 0 && commitTermsLastYear.scale() == 0) {
                        amountPerYear.put(1, (qli.TotalYearlyPrice__c / 12) * commitTermsYear1);
                        amountPerYear.put(lastActiveYear, (qli.TotalYearlyPrice__c / 12) * commitTermsLastYear);
                    }
                    // If only first year is integer, prorate first year using total yearly price and calculate remaining amount for last year
                    else if(commitTermsYear1.scale() == 0 && commitTermsLastYear.scale() != 0) {
                        amountPerYear.put(1, (qli.TotalYearlyPrice__c / 12) * commitTermsYear1);
                        amountPerYear.put(lastActiveYear, qli.TotalPrice - amountCompleteYear - ((qli.TotalYearlyPrice__c / 12) * commitTermsYear1));
                    }
                    // If only last year is integer, prorate last year using total yearly price and calculate remaining amount for first year
                    else if(commitTermsYear1.scale() != 0 && commitTermsLastYear.scale() == 0) {
                        amountPerYear.put(lastActiveYear, (qli.TotalYearlyPrice__c / 12) * commitTermsLastYear);
                        amountPerYear.put(1, qli.TotalPrice - amountCompleteYear - ((qli.TotalYearlyPrice__c / 12) * commitTermsLastYear));
                    }
                    // If non of them are integer, prorate last year using total yearly price and calculate remaining amount for first year
                    else if(commitTermsYear1.scale() != 0 && commitTermsLastYear.scale() != 0) {
                        amountPerYear.put(lastActiveYear, (qli.TotalYearlyPrice__c / 12) * commitTermsLastYear);
                        amountPerYear.put(1, qli.TotalPrice - amountCompleteYear - ((qli.TotalYearlyPrice__c / 12) * commitTermsLastYear));
                    }
                }
                // If only last year is complete, calculate first year using the remaining amount
                else if(daysPerYear.get(1) != 365 && daysPerYear.get(lastActiveYear) == 365) {
                    amountPerYear.put(1, qli.TotalPrice - amountCompleteYear);
                }
                // If only first year is complete, calculate last year using the remaining amount
                else if(daysPerYear.get(1) == 365 && daysPerYear.get(lastActiveYear) != 365) {
                    amountPerYear.put(lastActiveYear, qli.TotalPrice - amountCompleteYear);
                }

                // If year was already calculated for other SKU, add it
                if(totalByQuote.get(qli.QuoteId).containsKey(1)) {
                    totalByQuote.get(qli.QuoteId).put(1, totalByQuote.get(qli.QuoteId).get(1) + amountPerYear.get(1).setScale(2));
                } else {
                    totalByQuote.get(qli.QuoteId).put(1, amountPerYear.get(1).setScale(2));
                }
                if(totalByQuote.get(qli.QuoteId).containsKey(2)) {
                    totalByQuote.get(qli.QuoteId).put(2, totalByQuote.get(qli.QuoteId).get(2) + amountPerYear.get(2).setScale(2));
                } else {
                    totalByQuote.get(qli.QuoteId).put(2, amountPerYear.get(2).setScale(2));
                }
                if(totalByQuote.get(qli.QuoteId).containsKey(3)) {
                    totalByQuote.get(qli.QuoteId).put(3, totalByQuote.get(qli.QuoteId).get(3) + amountPerYear.get(3).setScale(2));
                } else {
                    totalByQuote.get(qli.QuoteId).put(3, amountPerYear.get(3).setScale(2));
                }
                if(totalByQuote.get(qli.QuoteId).containsKey(4)) {
                    totalByQuote.get(qli.QuoteId).put(4, totalByQuote.get(qli.QuoteId).get(4) + amountPerYear.get(4).setScale(2));
                } else {
                    totalByQuote.get(qli.QuoteId).put(4, amountPerYear.get(4).setScale(2));
                }
                if(totalByQuote.get(qli.QuoteId).containsKey(5)) {
                    totalByQuote.get(qli.QuoteId).put(5, totalByQuote.get(qli.QuoteId).get(5) + amountPerYear.get(5).setScale(2));
                } else {
                    totalByQuote.get(qli.QuoteId).put(5, amountPerYear.get(5).setScale(2));
                }
                if(totalByQuote.get(qli.QuoteId).containsKey(6)) {
                    totalByQuote.get(qli.QuoteId).put(6, totalByQuote.get(qli.QuoteId).get(6) + amountPerYear.get(6).setScale(2));
                } else {
                    totalByQuote.get(qli.QuoteId).put(6, amountPerYear.get(6).setScale(2));
                }
            }
        }
        // If Product is not recurring, add entire TotalPrice to Year 1
        else {
            if(totalByQuote.get(qli.QuoteId).containsKey(1)) {
                totalByQuote.get(qli.QuoteId).put(1, totalByQuote.get(qli.QuoteId).get(1) + qli.TotalPrice);
            } else {
                totalByQuote.get(qli.QuoteId).put(1, qli.TotalPrice);
            }
        }
    }

    // Update total year fields to all quotes
    for(Quote q : quotes) {
        if(totalByQuote.containsKey(q.Id)) {
            q.TotalYear1__c = (totalByQuote.get(q.Id).get(1) != 0) ? totalByQuote.get(q.Id).get(1) : null;
            q.TotalYear2__c = (totalByQuote.get(q.Id).get(2) != 0) ? totalByQuote.get(q.Id).get(2) : null;
            q.TotalYear3__c = (totalByQuote.get(q.Id).get(3) != 0) ? totalByQuote.get(q.Id).get(3) : null;
            q.TotalYear4__c = (totalByQuote.get(q.Id).get(4) != 0) ? totalByQuote.get(q.Id).get(4) : null;
            q.TotalYear5__c = (totalByQuote.get(q.Id).get(5) != 0) ? totalByQuote.get(q.Id).get(5) : null;
            q.TotalYear6__c = (totalByQuote.get(q.Id).get(6) != 0) ? totalByQuote.get(q.Id).get(6) : null;
        }
    }
    return quotes;
}

/**
*   Calculate commitment between two dates in month
*   @param startDate  Start date to calculate commitment
*   @param endDate  End date to calculate commitment
*   @param startDate  Default commitment to use if no start or end date
*   @return Decimal Number of commitment months
*/
public static Decimal commitTermsBetween(Date startDate, Date endDate, Decimal defaultCommitTerms) {
    Decimal commitTerms;
    if(startDate != null && endDate != null) {
        Date currentDate = startDate;
        commitTerms = 0;
        do {
            Integer numberDays = Date.daysInMonth(currentDate.year(), currentDate.month());
            if (currentDate.year() == startDate.year() && currentDate.month() == startDate.month() && currentDate.day() != 1) {
                commitTerms += ((Decimal) (numberDays - startDate.day())).divide(numberDays, 2);
            } else if (currentDate.year() == endDate.year() && currentDate.month() == endDate.month() && endDate.day() != numberDays) {
                commitTerms += ((Decimal) endDate.addDays(1).day()).divide(numberDays, 2);
            } else {
                commitTerms += 1;
            }
            currentDate = currentDate.addMonths(1);
        } while (Integer.valueOf(String.valueOf(currentDate.year()) + String.valueOf(currentDate.month() + 100).right(2)) <= Integer.valueOf(String.valueOf(endDate.year()) + String.valueOf(endDate.month() + 100).right(2)));
    }
    else {
        commitTerms = defaultCommitTerms;
    }
    return commitTerms;
}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .