Skip to content

Commit

Permalink
Changes from PR 591 to support multi-currency
Browse files Browse the repository at this point in the history
  • Loading branch information
afawcett committed Apr 15, 2018
1 parent 0beba31 commit 28e1ddd
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ before_install:
- sfdx force:auth:jwt:grant --clientid $CONSUMERKEY --jwtkeyfile assets/server.key
--username $USERNAME --setdefaultdevhubusername -a HubOrg
script:
- sfdx force:org:create -v HubOrg -s -f config/project-scratch-def.json -a ciorg -w 12
- sfdx force:org:create -v HubOrg -s -f config/packaging-org-scratch-def.json -a ciorg -w 12
- sfdx force:source:push -u ciorg
- sfdx force:apex:test:run -u ciorg -c -r human
after_script:
Expand Down
8 changes: 8 additions & 0 deletions config/multicurrency-org-scratch-def.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"orgName": "andrewfawcett Company",
"edition": "Developer",
"features": ["MultiCurrency", "AuthorApex"],
"orgPreferences" : {
"enabled": ["S1DesktopEnabled"]
}
}
File renamed without changes.
100 changes: 89 additions & 11 deletions force-app/libs/lrengine/classes/LREngine.cls
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ public class LREngine {
2 : Optional WHERE clause filter to add
3 : Group By field name
4 : ALL ROWS or empty string
5 : Group By additional fields
*/
static String SOQL_AGGREGATE_TEMPLATE = 'SELECT {0} FROM {1} WHERE {3} in :masterIds {2} GROUP BY {3} {4}';
static String SOQL_AGGREGATE_TEMPLATE = 'SELECT {0} FROM {1} WHERE {3} in :masterIds {2} GROUP BY {3}{5} {4}';

/*
Tempalte tokens
Expand Down Expand Up @@ -142,7 +143,7 @@ public class LREngine {

// k: detail field name, v: master field name
Integer exprIdx = 0;
Boolean needsCurrency = false;
Boolean needsCurrency = false, needsAdvancedCurrency = false;
Boolean builtAggregateQuery = false;
Set<String> selectedFields = new Set<String>();
Map<String, RollupSummaryField> rsfByAlais = new Map<String, RollupSummaryField>();
Expand Down Expand Up @@ -170,6 +171,9 @@ public class LREngine {
soqlProjection += ', ' + selectField;
selectedFields.add(selectField);
}
if(IsMultiCurrencyOrg() == true && rsf.operation == RollupOperation.SumAdvancedCurrency && needsAdvancedCurrency == false && rsf.isMasterTypeCurrency){
needsAdvancedCurrency = true;
}
}
}

Expand All @@ -179,19 +183,45 @@ public class LREngine {
soqlProjection += ', ' + RollupOperation.Max + '(' + lookupRelationshipName +
'.' + CURRENCYISOCODENAME + ') ' + MASTERCURRENCYALIAS;
}
else if(IsMultiCurrencyOrg() == true && needsAdvancedCurrency == true){
soqlProjection += ', ' + ctx.lookupField.getRelationshipName() + '.' + CURRENCYISOCODENAME + ', ' + CURRENCYISOCODENAME;
if(ctx.dateField != null && ctx.dateLookupField != null) {
soqlProjection += ', ' + ctx.dateLookupField.getRelationshipName() + '.' + ctx.dateField.getName();
}
else {
soqlProjection += ', ' + ctx.dateField.getName();
}
}

// Adding additional fields to query. If it is an aggregated query, add alias
exprIdx = 0;
Map<String, String> afByAlias = new Map<String, String>();
for (Schema.Describefieldresult af : ctx.additionalFields) {
String alias = 'are'+exprIdx++;
soqlProjection += ', ' + ctx.lookupField.getRelationshipName() + '.' + af.getName() + (builtAggregateQuery ? ' ' + alias : '');
afByAlias.put(af.getName(), alias);
}

// #1 token for SOQL_TEMPLATE
String detailTblName = ctx.detail.getDescribe().getName();

// #2 Where clause
String whereClause = '';
if (ctx.detailWhereClause != null && ctx.detailWhereClause.trim().length() > 0) {
whereClause = 'AND (' + ctx.detailWhereClause +')';
whereClause = 'AND ' + ctx.detailWhereClause ;
}

// #3 Group by field
String grpByFld = ctx.lookupField.getName();

// #5 Group by additional fields
String additionalGrpByFld = '';
if(builtAggregateQuery) {
for (Schema.Describefieldresult af : ctx.additionalFields) {
additionalGrpByFld += ', ' + ctx.lookupField.getRelationshipName() + '.' + af.getName();
}
}

// #4 Order by clause fields
// i.e. Amount ASC NULLS FIRST, Name DESC NULL LAST
String orderByClause = ctx.lookupField.getName() + (String.isBlank(ctx.detailOrderByClause) ? '' : (',' + ctx.detailOrderByClause));
Expand All @@ -208,7 +238,8 @@ public class LREngine {
detailTblName,
whereClause,
grpByFld,
allRows}) :
allRows,
additionalGrpByFld}) :
String.format(SOQL_QUERY_TEMPLATE,
new String[]{
soqlProjection,
Expand Down Expand Up @@ -248,6 +279,11 @@ public class LREngine {
continue;
}

// Adding additional fields to master object using alias
for(Schema.Describefieldresult af : ctx.additionalFields) {
masterObj.put(af.getName(), res.get(afByAlias.get(af.getName())));
}

for (String alias : rsfByAlais.keySet()) {
RollupSummaryField rsf = rsfByAlais.get(alias);
Object aggregatedDetailVal = res.get(alias);
Expand Down Expand Up @@ -280,6 +316,9 @@ public class LREngine {
currentDetailRecords = new List<SObject>();
detailRecordsByMasterId.put(masterId, currentDetailRecords);
}
for(Schema.Describefieldresult af : ctx.additionalFields) {
masterRecordsMap.get(masterId).put(af.getName(), detailRecord.getSObject(ctx.lookupField.getRelationshipName()).get(af.getName()));
}
currentDetailRecords.add(detailRecord);
lastMasterId = masterId;
}
Expand Down Expand Up @@ -323,6 +362,23 @@ public class LREngine {
masterRecordsMap.get(masterId).put(
rsf.master.getName(),
childDetailRecords[index].get(rsf.detail.getName()));
} else if(rsf.operation == RollupOperation.SumAdvancedCurrency) {
Decimal sum = 0;
for(SObject childDetailRecord : childDetailRecords) {
Decimal amountInMasterCurrency = 0;
String masterCurrencyIsoCode = (String) childDetailRecord.getSObject(ctx.lookupField.getRelationshipName()).get(CURRENCYISOCODENAME);
String childCurrencyIsoCode = (String) childDetailRecord.get(CURRENCYISOCODENAME);
if(childCurrencyIsoCode != masterCurrencyIsoCode) {
Date currencyDate = (ctx.dateLookupField != null) ? (Date) childDetailRecord.getSObject(ctx.dateLookupField.getRelationshipName()).get(ctx.dateField.getName()) : (Date) childDetailRecord.get(ctx.dateField.getName());
Decimal amountInCorporateCurrency = CurrencyManagement.convertToCorporateCurrency(childCurrencyIsoCode, (Decimal) childDetailRecord.get(rsf.detail.getName()), currencyDate);
amountInMasterCurrency = CurrencyManagement.convertFromCorporateCurrency(masterCurrencyIsoCode, amountInCorporateCurrency, currencyDate);
}
else {
amountInMasterCurrency = (Decimal) childDetailRecord.get(rsf.detail.getName());
}
sum += amountInMasterCurrency;
}
masterRecordsMap.get(masterId).put(rsf.master.getName(), sum);
}
// Remove master Id record as its been processed
masterIds.remove(masterId);
Expand All @@ -331,7 +387,7 @@ public class LREngine {
}

// Zero rollups for unprocessed master records (those with no longer any child relationships)
for(Id masterRecId : masterIds)
for(Id masterRecId : masterIds) {
for (RollupSummaryField rsf : ctx.fieldsToRoll) {
Object nullValue = null;
if(rsf.isMasterTypeNumber) {
Expand All @@ -341,6 +397,7 @@ public class LREngine {
}
masterRecordsMap.get(masterRecId).put(rsf.master.getName(), nullValue);
}
}

return masterRecordsMap.values();
}
Expand Down Expand Up @@ -395,7 +452,7 @@ public class LREngine {
Which rollup operation you want to perform
*/
public enum RollupOperation {
Sum, Max, Min, Avg, Count, Count_Distinct, Concatenate, Concatenate_Distinct, First, Last
Sum, Max, Min, Avg, Count, Count_Distinct, Concatenate, Concatenate_Distinct, First, Last, SumAdvancedCurrency
}

/**
Expand Down Expand Up @@ -563,7 +620,8 @@ public class LREngine {
return operation == RollupOperation.Concatenate ||
operation == RollupOperation.Concatenate_Distinct ||
operation == RollupOperation.First ||
operation == RollupOperation.Last;
operation == RollupOperation.Last ||
operation == RollupOperation.SumAdvancedCurrency ;
}

/**
Expand All @@ -579,8 +637,14 @@ public class LREngine {
public Schema.Sobjecttype detail;
// Lookup field on Child/Detail Sobject
public Schema.Describefieldresult lookupField;
// Date lookup field used for Dated Exchange Rate
public Schema.Describefieldresult dateLookupField;
// Date field used for Dated Exchange Rate
public Schema.Describefieldresult dateField;
// various fields to rollup on
public List<RollupSummaryField> fieldsToRoll;
// various additional fields
public List<Schema.Describefieldresult> additionalFields;
// include all rows
public Boolean allRows;
// what type of rollups does this context contain
Expand Down Expand Up @@ -626,10 +690,27 @@ public class LREngine {
this.detailWhereClause = detailWhereClause;
this.detailOrderByClause = detailOrderByClause;
this.fieldsToRoll = new List<RollupSummaryField>();
this.additionalFields = new List<Schema.Describefieldresult>();
this.sharingMode = sharingMode;
this.allRows = allRows;
}

public void setDateField(Schema.Describefieldresult df) {
this.dateField = df;
}

public void setDateLookupField(Schema.Describefieldresult dlf) {
this.dateLookupField = dlf;
}

public void addAdditionalField(Schema.Describefieldresult af) {
if(this.fieldsToRoll.isEmpty())
throw new BadRollUpSummaryStateException('Add atleast one RollupSummaryField before adding additional fields');
if(isAggregateBased)
System.debug(LoggingLevel.WARN,'On aggregate query, be careful to only add additional fields from parent object');
this.additionalFields.add(af);
}

/**
Adds new rollup summary fields to the context
*/
Expand All @@ -645,7 +726,7 @@ public class LREngine {
// A context cannot support summary fields with operations that mix the use of underlying query types
if(isAggregateBased && !fld.isAggregateBasedRollup() ||
isQueryBased && !fld.isQueryBasedRollup())
throw new BadRollUpSummaryStateException('Cannot mix Sum, Max, Min, Avg, Count, Count_Distinct operations with Concatenate, Concatenate_Distinct, First, Last operations');
throw new BadRollUpSummaryStateException('Cannot mix Sum, Max, Min, Avg, Count, Count_Distinct operations with Concatenate, Concatenate_Distinct, First, Last, SumAdvancedCurrency operations');

this.fieldsToRoll.add(fld);
}
Expand All @@ -656,9 +737,6 @@ public class LREngine {
**/
public abstract class QueryExecutor {
public virtual List<SObject> query(String query, Set<Id> masterIds) {
if ( masterIds == null || masterIds.isEmpty() ) {
return new List<SObject>();
}
return Database.query(query);
}
}
Expand Down
86 changes: 83 additions & 3 deletions force-app/libs/lrengine/classes/TestLREngine.cls
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
any dependency on custom objects and keep the code base simple and easy to deploy in new orgs.
*/
@isTest
@isTest(isParallel=true)
private class TestLREngine {
// common master records for the test case
static Account acc1, acc2, acc3, acc4;
Expand Down Expand Up @@ -73,6 +73,13 @@ private class TestLREngine {
return m_HasMultiCurrency;
}

private static Boolean m_HasMultiDatedCurrency = null;
public static Boolean hasMultiDatedCurrency() {
if(m_HasMultiDatedCurrency!=null) return m_HasMultiDatedCurrency;
m_HasMultiDatedCurrency = (Database.countQuery('select count() from DatedConversionRate WHERE ConversionRate != 1') > 0);
return m_HasMultiDatedCurrency;
}

/*
creates the common seed data using Opportunity and Account objects.
*/
Expand Down Expand Up @@ -286,6 +293,13 @@ private class TestLREngine {
LREngine.RollupOperation.Count
));

try {
ctx.addAdditionalField(Schema.SObjectType.Account.fields.Name);
}
catch (Exception e) {
System.assertEquals('Cannot add additional fields on aggregate query', e.getMessage());
}

Sobject[] masters = LREngine.rollUp(ctx, detailRecords);
// 2 masters should be back
System.assertEquals(2, masters.size());
Expand Down Expand Up @@ -650,6 +664,72 @@ private class TestLREngine {
System.assertEquals(acct2Val, (Decimal)reloadedAcc2.get('AnnualRevenue'));
}

/*
Test Advanced Multi-Currency (dated exchange rate) installations
*/
static testMethod void testAdvancedCurrencyConversionFields() {

// is org multi-currency?
// org has at least one non-corporate, not equivalent, currency installed.
if(IsMultiCurrencyOrg() == false || hasMultiDatedCurrency() == false)
return;

// create seed data
prepareData();

// change the currency of one of the master records to force currency conversion
sObject ct = Database.query('select IsoCode, ConversionRate from CurrencyType where IsActive = true AND IsCorporate = false AND ConversionRate != 1 limit 1');
acc1.put(CURRENCYISOCODENAME, ct.get('IsoCode'));
update acc1;

//change currency of one account
LREngine.Context ctx = new LREngine.Context(Account.SobjectType,
Opportunity.SobjectType,
Schema.SObjectType.Opportunity.fields.AccountId
);
ctx.setDateField(Schema.SObjectType.Opportunity.fields.CloseDate);
try {
ctx.addAdditionalField(Schema.SObjectType.Account.fields.Name);
}
catch (Exception e) {
System.debug(e);
System.assertEquals('Add atleast one RollupSummaryField before adding additional fields', e.getMessage());
}

ctx.add(
new LREngine.RollupSummaryField(
Schema.SObjectType.Account.fields.AnnualRevenue,
Schema.SObjectType.Opportunity.fields.Amount,
LREngine.RollupOperation.SumAdvancedCurrency
));
ctx.addAdditionalField(Schema.SObjectType.Account.fields.Name);

Sobject[] masters = LREngine.rollUp(ctx, detailRecords);

Decimal acct1Val = 0.0;
Decimal acct2Val = 0.0;
for(Sobject so : detailRecords){
if(so.get('AccountId') == acc1.Id) acct1Val += (Decimal)so.get('Amount');
if(so.get('AccountId') == acc2.Id) acct2Val += (Decimal)so.get('Amount');
}
System.Debug('Conversion Rate:'+ct.get('ConversionRate'));
System.Debug('Acct1 Val:'+acct1Val);

acct1Val = acct1Val * (Decimal)ct.get('ConversionRate');

System.Debug('Acct1 Conv Val:'+acct1Val);

Account reloadedAcc1, reloadedAcc2;
for (Sobject so : masters) {
if (so.Id == acc1.id) reloadedAcc1 = (Account)so;
if (so.Id == acc2.id) reloadedAcc2 = (Account)so;
}

//Test amount values for conversion accuracy
System.assertEquals(acct1Val, (Decimal)reloadedAcc1.get('AnnualRevenue'));
System.assertEquals(acct2Val, (Decimal)reloadedAcc2.get('AnnualRevenue'));
}

static testMethod void testRollupSummaryFieldValidation() {

LREngine.Context ctx = new LREngine.Context(Account.SobjectType,
Expand Down Expand Up @@ -697,7 +777,7 @@ private class TestLREngine {
LREngine.RollupOperation.Sum));
System.assert(false, 'Expecting an exception');
} catch (Exception e) {
System.assertEquals('Cannot mix Sum, Max, Min, Avg, Count, Count_Distinct operations with Concatenate, Concatenate_Distinct, First, Last operations', e.getMessage());
System.assertEquals('Cannot mix Sum, Max, Min, Avg, Count, Count_Distinct operations with Concatenate, Concatenate_Distinct, First, Last, SumAdvancedCurrency operations', e.getMessage());
}
try {
LREngine.Context ctx =
Expand All @@ -714,7 +794,7 @@ private class TestLREngine {
LREngine.RollupOperation.Concatenate));
System.assert(false, 'Expecting an exception');
} catch (Exception e) {
System.assertEquals('Cannot mix Sum, Max, Min, Avg, Count, Count_Distinct operations with Concatenate, Concatenate_Distinct, First, Last operations', e.getMessage());
System.assertEquals('Cannot mix Sum, Max, Min, Avg, Count, Count_Distinct operations with Concatenate, Concatenate_Distinct, First, Last, SumAdvancedCurrency operations', e.getMessage());
}
}

Expand Down
Loading

0 comments on commit 28e1ddd

Please sign in to comment.