Saturday, May 4, 2013

Territory Management in Apex


Recipe: Territory Management
Territory management is account sharing system that grants access to accounts based on the characteristics of the accounts. It enables your company to structure your Salesforce data and users the same way you structure your sales territories.
If you want to use territory management in Apex then you should read this document for limitations and territory functionality in Apex.

Relationships among Territory, Account, Opportunity and User objects:
a) A territory can have unlimited number of users, and a user can be assigned to an unlimited number of Territories.
b) A territory can have unlimited number of accounts, and an account can be assigned to an unlimited number of territories.
c) An Opportunity can only be assigned to a Territory.

Territories in Opportunity object:

Problem:
If you want to access the territory field of Opportunity in Apex you can access it easily. Because the field Territory on Opportunity object is exactly present in Opportunity object.

Solution with Example Code:
/*
Author: @Abrar Haq
Purpose:
    (a)To populate Parent Territory Name and Territory User Name of assigned Territory of Opportunity.
*/
trigger OpportunityTrg on Opportunity (before insert) {
    if(Trigger.isBefore){
        /* Declaration of collection data types */
        Set<Id> setOfTerritoryIds = new Set<Id>();

        for(Opportunity oppty : Trigger.New){
            if(Trigger.isInsert){
                if(oppty.TerritoryId != null){
                    setOfTerritoryIds.add(oppty.TerritoryId);
                }
            }
        }

        /*
            (b)To populate Parent Territory Name of assigned Territory of Opportunity.
        */    
        if(setOfTerritoryIds.size() > 0){
            Map<Id, Territory> mapOfTerritory = new Map<Id, Territory>([Select t.RestrictOpportunityTransfer, t.ParentTerritoryId, t.OpportunityAccessLevel, t.Name, t.MayForecastManagerShare, t.Id, t.ForecastUserId, t.Description, t.ContactAccessLevel, t.CaseAccessLevel, t.AccountAccessLevel From Territory t Where Id IN :setOfTerritoryIds]);
        }
    }
}


Territories in Account object:

Problem:
If you want to access to the Territories field on Account then you need to do extra work. Because Territories field on Account is not present in Account object. You have to query in different objects to get the assigned territory of Account.

Algorithm (Pseudo Code):
1) Query AccountShare object where AccountId = Trigger.New (Account Ids)
where RowCause = 'Territory' (Note: If Account has assigned via Territory Assignment Rules).
where RowCause = 'TerritoryManual' (Note: If Account has assigned via “Manually Assigned Accounts” related list on Territory detail page or AccountShare record has created in Apex Code).
2) Query Group object where Id = AccountShare.UserOrGroupId
3) Query Territory object where Id = Group.RelatedId
4) Query UserTerritory object where Id = Territory.UserId


Solution with Example Code:
/*
Author: @Abrar Haq
Purpose:
    (a)To populate Parent Territory Name and Territory User Name of assigned Territory of Account.
*/
trigger AccountTrg on Account (before update){
    if(Trigger.isBefore){
        /* Declaration of collection data types */
        Set<Id> setOfAccountIds = new Set<Id>();

        for(Account acct : Trigger.New){
            if(Trigger.isUpdate){
                if(acct.Id != null){
                    setOfAccountIds.add(acct.Id);
                }
            }
        }

        /*
            (b) To populate Parent Territory Name of assigned Territory of Opportunity.
        */    
        if(setOfAccountIds.size() > 0){

        /* Declaration of collection data types */
Map<Id, Id> mapOfAccountShare = new Map<Id, Id>();
       Map<Id, Id> mapOfGroup = new Map<Id, Id>();
       Map<Id, Territory> mapOfUserTerritory = new Map<Id, Territory>();


(1)

//Query in Account Share object
    /*
    Those Accounts which are assigned via Territory Assignment Rules.
You can query those Accounts by filtering RowCause = 'Territory' in AccountShare object query.
    */
    List<AccountShare> listOfAccountShare =
    [Select Id, UserOrGroupId, AccountId from AccountShare where RowCause = 'Territory' and AccountId IN :setOfAccountIds];

    //Query in Account Share object
    /*
    Those Accounts which are assigned via Manually Assigned Accounts related list on Territory detail page or create an AccountShare record in Apex code.
    You can query those Accounts by filtering RowCause = 'TerritoryManual' in AccountShare object query.
    */
    List<AccountShare> listOfAccountShare =
    [Select Id, UserOrGroupId, AccountId from AccountShare where RowCause = 'TerritoryManual' and AccountId IN :setOfAccountIds];

//Map of Account Share
    for(AccountShare acctShare : listOfAccountShare){
    mapOfAccountShare.put(acctShare.AccountId, acctShare.UserOrGroupId);        
    }      

(2)

    //Query in Group object            
    List<Group> listOfGroup = [Select Id, RelatedId from Group where Type='Territory' and Id IN :mapOfAccountShare.Values()];

//Map of Group object
    for(Group groupRecord : listOfGroup){
    mapOfGroup.put(groupRecord.Id, groupRecord.RelatedId);        
    }

(3)

    //Query in Territory object
    //Map<Id, Territory> mapOfTerritories =
    new Map<Id, Territory>([select id, name, ParentTerritoryId from Territory where Id IN:mapOfGroup.Values() ]);      

(4)

    //Query in User Territory object
    List<UserTerritory> listOfUserTerritory = [Select u.UserId, u.TerritoryId, u.IsActive, u.Id From UserTerritory u WHERE IsActive = true AND TerritoryId IN :mapOfTerritories.KeySet()];

//Map of User Territory object
    for(UserTerritory userTerritory : listOfUserTerritory){
    mapOfUserTerritory.put(userTerritory.TerritoryId, userTerritory);              
    }

}

}
}


Limitation(s):
1) You cannot edit / delete AccountShare record where ROWCAUSE = ‘Territory’ via Data Loader and API. If you do then you will get the following error.
“System.DmlException: Delete failed. First exception on row 0 with id 00rZ000001G3hDoIAJ; first error: INVALID_CROSS_REFERENCE_KEY, id does not exist: []”.