Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Advanced currency #725

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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