Approval Process reminders


2022--> Might want to check the updated solution: https://lc169.blogspot.com/2022/05/approval-process-reminders.html

Following tool may be used for sending email reminders for pending approval in approval process in specific 

interval. The reminders will be keep sending until the related record be approved.
Note: the tool use Salesforce standard scheduling system and it scheduled to run every hour. 
This means that the reminders might not be accurate. E.g. If record was submitted for approval 
at 9:30 AM, and you setup reminders after 24 Hours, the reminder will be send at 10:00 AM (next day).
If you need it to be accurate you can expose it at service and call it with external tool.

Can install as manage package from here or download the code from git repository


Custom objects:

1. Approval Process Reminder:
Setup for sending reminders for specific approval process

Related Fields:

  • Approval Process Reminder Name- Name of the process
  • Related Approval Process- Related approval process Name
  • Related Object- Related object of the approval process (should be object API name)
  • Reminder After (H) - After how many hours send each reminder
  • Business Hours - Lookup for Business Hours in Salesforce. This will indicate which days/hours count for this process.
Additional recipients for the reminders, other then the approver.
  • Additional Recipient 1 - used to reference user field from the related record (e.g. owner).
  • Additional Recipient 2 -  used to reference user field from the related record.
  • Additional Recipient 3 -  Reference for specific user.
  • Additional Recipient 4 - Reference for specific user.
  • Additional Recipient 5 - Email Address.
  • Additional Recipient 6 - Email Address.
  • Alert Recipient X From Level - Those set of field indicate from which level of reminders send to each of the additional recipients. First alert is level 1, Second is level 2, and so on...

Schedule/Abort the process:
Go to the Approval Process Reminder tab and press the button Schedule Reminders Process, then click Schedule or Stop button

2. Approval Process Record
Relation for approver in specific approval. 
This record shouldn't be created manually, it will be created and maintain by the process.


Related Fields:

  • Approval Process Reminder - Lookup for the parent approval process reminder setup
  • Status- Status of this approval. Pending/Approved
  • Approver - Lookup to the approver user
  • Approver Name- Formula. Approver Name
  • Additional Recipient 1 - Lookups to the additional recipients 
  • Additional Recipient 2
  • Additional Recipient 3
  • Additional Recipient 4
  • Additional Recipient 5
  • Additional Recipient 6
  • Alerts Sent- Number alerts that was sent
  • Interval- Formula. After how many hours to send alert
  • Pending Hours- Number of hours the record is pending approval
  • ProcessInstance- Id of the processInstance of the approval in salesforce.
  • Record Id- Id of the record in approval process
  • Record Link- Link to the record in approval process
  • Record Name - Record Type + Name of the record in approval process (e.g.: Opportunity: Test Opportunity)


Main class:

Apex class: ApprovalReminderUtils
This class receive as parameter list of approval process names and search for pending approval records.
It is invoked from the batch class (see full classes for more details.


global class ApprovalReminderUtils {
    
    
    public static void ApprovalProcessReminderMain(list<String> l_approvalNames){
        try{
        
            //Retrieve the relevant approval process reminder setup
            list<Approval_Process_Reminder__c> l_reminderSetup =    
                        [select id, Related_Object__c, Related_Approval_Process__c, Reminder_After__c, Business_Hours__c,
                            Additional_Recipient_1__c, Additional_Recipient_2__c,Additional_Recipient_3__c, Additional_Recipient_4__c, Additional_Recipient_5__c, Additional_Recipient_6__c,
                            Alert_Recipient_1_From_Level__c, Alert_Recipient_2_From_Level__c, Alert_Recipient_3_From_Level__c, Alert_Recipient_4_From_Level__c, Alert_Recipient_5_From_Level__c, Alert_Recipient_6_From_Level__c,
                            (select id, Status__c, Record_Id__c, ProcessInstance_Id__c, Alerts_Sent__c,Approver__c from Approval_Process_Records__r where status__c = 'Pending')
                        from Approval_Process_Reminder__c
                        where Related_Approval_Process__c In :l_approvalNames 
                            and Active__c = true];
            
            //Map - per each approval process, some related properties
            map<String, ApprovalProcessProperty> m_approvalProp = new map<String, ApprovalProcessProperty>();
            
            //list of APR recors for insert
            list<Approval_Process_Record__c> l_newAPR = new list<Approval_Process_Record__c>();    
            
            //list of APR record for update
            list<Approval_Process_Record__c> l_updAPR = new list<Approval_Process_Record__c>();    
            
            //Set of APR records that was increased by 1
            set<Id> s_aprId = new set<Id>();
            
            //set of users that exists as actor in the workItems 
            set<Id> s_userIds = new set<Id>();
            
            //per each key (record Id from approval + processInstanceId + approverId) its Approval Process Record (if exists)
                map<String, Approval_Process_Record__c> m_recId_APR = new map<String, Approval_Process_Record__c>();
                
            for(Approval_Process_Reminder__c reminderSetup : l_reminderSetup){
                
                System.debug('####' + reminderSetup.Related_Approval_Process__c);
                
                m_approvalProp.put(
                    reminderSetup.Related_Approval_Process__c, new ApprovalProcessProperty(reminderSetup));
                
                //list of records Id in approval process
                list<Id> l_TargetObjId = new list<Id>();
            
                for(Approval_Process_Record__c apr : reminderSetup.Approval_Process_Records__r){
                    m_recId_APR.put(apr.Record_ID__c + '_' + apr.ProcessInstance_Id__c + '_' + apr.Approver__c, apr);
                }
            }
            
            if(Test.isRunningTest()){
                //Create mock data
                Account acc = [select Id from Account limit 1];
                
                ProcessInstanceRecord pi = new ProcessInstanceRecord('testId', 'Pending', acc.Id, System.Now() - 1);
                
                ProcessInstanceItemRecord processInstanceItemRec = new ProcessInstanceItemRecord(UserInfo.getUserId(), 'Pending', System.Now()-3);
                        
                s_userIds.add(UserInfo.getUserId());
                        
                pi.addItem(processInstanceItemRec);
                
                m_approvalProp.get(l_reminderSetup.get(0).Related_Approval_Process__c).addRecordId(acc.Id);
                m_approvalProp.get(l_reminderSetup.get(0).Related_Approval_Process__c).addProcessInstance(pi);
            }
            else{
                System.debug('####' + m_approvalProp.keySet());
                
                for ( ProcessInstance pi:   [   SELECT Id, Status, TargetObjectId, LastModifiedDate, ProcessDefinition.DeveloperName, 
                                                    (SELECT Id, ActorId, StepStatus, CreatedDate FROM StepsAndWorkitems where StepStatus='Pending')
                                                FROM ProcessInstance 
                                                where Status IN ('Pending','Hold','Reassigned','NoResponse')
                                                    and ProcessDefinition.DeveloperName IN :m_approvalProp.keySet()]) {
                                                
                    ProcessInstanceRecord processInstanceRec = new ProcessInstanceRecord(pi.Id, pi.Status, pi.TargetObjectId, pi.LastModifiedDate);
                    
                    for (ProcessInstanceHistory pih : pi.StepsAndWorkItems) {
                        s_userIds.add(pih.ActorId);
                        
                        ProcessInstanceItemRecord processInstanceItemRec = new ProcessInstanceItemRecord(pih.ActorId, pih.StepStatus, pih.CreatedDate);
                        
                        processInstanceRec.addItem(processInstanceItemRec);
                    }
                    
                    m_approvalProp.get(pi.ProcessDefinition.DeveloperName).addRecordId(pi.TargetObjectId);
                    m_approvalProp.get(pi.ProcessDefinition.DeveloperName).addProcessInstance(processInstanceRec);
                }
            }
            
            if(! s_userIds.isEmpty()){
            
                //In each approval process properties initialaze the related object with users fields
                for(String approvalKey : m_approvalProp.keySet()){
                    m_approvalProp.get(approvalKey).queryRelatedObjects();
                    
                    //Loop over approval process instance
                    for(ProcessInstanceRecord pi : m_approvalProp.get(approvalKey).l_instanceRecords){
                    
                        //Per each pending approver
                        for (ProcessInstanceItemRecord pih : pi.items) {
                            
                            //Calculate the hours difference from time processinstance was created until now
                            Double pendingHours = (Double) BusinessHours.diff(
                                m_approvalProp.get(approvalKey).approvalProcessReminder.Business_Hours__c, pih.CreatedDate, system.now())/1000/60/60;
                                
                            system.debug('###pendingHours:' + pendingHours);
                            
                            //If target object id inside the map, means alert was already sent for this record before
                            if(m_recId_APR.containsKey(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId)){
                                //calculate if hours that approval is pending divded by the alerts that were sent is greater than current number of alerts
                                //If criteria aply - means it time to send another reminder
                                if(math.Floor(pendingHours/m_approvalProp.get(approvalKey).approvalProcessReminder.Reminder_After__c) > m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Alerts_Sent__c){
                                
                                    //Increase the Alert Send by 1, this will run the workflow rule
                                    m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Alerts_Sent__c ++;
                                
                                    m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Pending_Hours__c = pendingHours;
                                
                                    //Populate the new recipients if needed
                                    integer alertCounter = Integer.valueOf(m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Alerts_Sent__c);
                                    
                                    m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Additional_Recipient_1__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 1, alertCounter); 
                                    m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Additional_Recipient_2__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 2, alertCounter); 
                                    m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Additional_Recipient_3__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 3, alertCounter); 
                                    m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Additional_Recipient_4__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 4, alertCounter); 
                                    m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Additional_Recipient_5__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 5, alertCounter); 
                                    m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).Additional_Recipient_6__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 6, alertCounter); 
                                
                                    //This will be used later. Need to know which Approval Process Records was increased. Other
                                    //records will be updated with Status = 'Approved'
                                    s_aprId.add(m_recId_APR.get(pi.targetObjectId + '_' + pi.id + '_' + pih.ActorId).id);
                                }
                            }
                            else{   //No previous alert was sent for this record
                            
                                //Pending hour is more than the setup, should send first alert
                                if(pendingHours > m_approvalProp.get(approvalKey).approvalProcessReminder.Reminder_After__c){ 
                                    //Add new Approval Process Record        
                                    l_newAPR.add(new Approval_Process_Record__c(Approval_Process_Reminder__c = m_approvalProp.get(approvalKey).approvalProcessReminder.id,
                                                                                Status__c = 'Pending', 
                                                                                Record_Id__c = pi.targetObjectId, 
                                                                                ProcessInstance_Id__c = pi.id, 
                                                                                Alerts_Sent__c = 1,
                                                                                Pending_Hours__c = pendingHours,
                                                                                Approver__c = pih.ActorId,
                                                                                Additional_Recipient_1__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 1, 1),
                                                                                Additional_Recipient_2__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 2, 1),
                                                                                Additional_Recipient_3__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 3, 1),
                                                                                Additional_Recipient_4__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 4, 1),
                                                                                Additional_Recipient_5__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 5, 1),
                                                                                Additional_Recipient_6__c = m_approvalProp.get(approvalKey).getUser(pi.targetObjectId, 6, 1)));
                                }
                            }
                        }
                    }
                    
                    //Evaluate old APR records that are no longer in the approval process. Should update their status to 'Approved'
                    for(Approval_Process_Record__c apr : m_approvalProp.get(approvalKey).approvalProcessReminder.Approval_Process_Records__r){
                        //if not update earlier
                        if(!s_aprId.contains(apr.id)){
                            apr.Status__c = 'Approved';
                        }
                    }
                    
                    l_updAPR.addAll(m_approvalProp.get(approvalKey).approvalProcessReminder.Approval_Process_Records__r);
                }
                    
                //New Approval Process Records for insert
                if(! l_newAPR.isEmpty()){
                    insert l_newAPR;
                }
                
                //Update all existing Approval Process Records. This list contain 3 type of records:
                //1.Records that Alert_Sent was increased, and now another reminder will be send for them
                //2.Records that are no longer found in the pending ProcessInstances, therefore will be upadated to status 'Approved'
                //3.Records without any change in this run
                if(!l_updAPR.isEmpty()){
                    update l_updAPR; 
                }
            }
        }
        catch(Exception e){
            //Might want to sent email alert to admin user, and notify the issue
            system.debug('###' + e);
        }
    }
    

    public class ApprovalProcessProperty{
        public Approval_Process_Reminder__c approvalProcessReminder;
        public list<String> l_recordsIds;
        public list<ProcessInstanceRecord> l_instanceRecords;
        
        map<String, SObject> m_relatedRecords;
        
        public ApprovalProcessProperty(Approval_Process_Reminder__c pApprovalProcessReminder){
            approvalProcessReminder = pApprovalProcessReminder;
            
            l_recordsIds = new list<String>();
            l_instanceRecords = new list<ProcessInstanceRecord>();
            m_relatedRecords = new map<String, SObject>();
        }
        
        public void addRecordId(String recordId){
            l_recordsIds.add(recordId);
        }
        
        public void addProcessInstance(ProcessInstanceRecord processRecord){
            l_instanceRecords.add(processRecord);
        }
        
        public void queryRelatedObjects(){
            if(!l_recordsIds.isEmpty() 
               && (approvalProcessReminder.Additional_Recipient_1__c != null || approvalProcessReminder.Additional_Recipient_2__c != null)){
                
                //Should collect all relevant records Ids which currently in approval process, and query their related users (According to the setup in approval reminder)
                String sql = 'select id';
                
                if(approvalProcessReminder.Additional_Recipient_1__c != null
                    && approvalProcessReminder.Additional_Recipient_1__c != 'None'){
                    sql += ',' + approvalProcessReminder.Additional_Recipient_1__c;
                }
                if(approvalProcessReminder.Additional_Recipient_2__c != null
                    && approvalProcessReminder.Additional_Recipient_2__c != 'None'){
                    sql += ',' + approvalProcessReminder.Additional_Recipient_2__c;
                }
                
                for(sObject obj : Database.query(sql + ' from ' + approvalProcessReminder.Related_Object__c + ' where id in :l_recordsIds')){
                    m_relatedRecords.put(obj.id, obj);
                }
            }
        }
        
        public String getUser(String recordId, integer recipiantNum, integer approvalLevel){
            if(recipiantNum == 1 
                && approvalProcessReminder.Alert_Recipient_1_From_Level__c <= approvalLevel
                && approvalProcessReminder.Additional_Recipient_1__c != null
                && approvalProcessReminder.Additional_Recipient_1__c != 'None'){
                return (String) m_relatedRecords.get(recordId).get(approvalProcessReminder.Additional_Recipient_1__c);
            }
            else if(recipiantNum == 2 
                && approvalProcessReminder.Alert_Recipient_2_From_Level__c <= approvalLevel
                && approvalProcessReminder.Additional_Recipient_2__c != null
                && approvalProcessReminder.Additional_Recipient_2__c != 'None'){
                return (String) m_relatedRecords.get(recordId).get(approvalProcessReminder.Additional_Recipient_2__c);
            }
            else if(recipiantNum == 3 
                && approvalProcessReminder.Alert_Recipient_3_From_Level__c <= approvalLevel
                && approvalProcessReminder.Additional_Recipient_3__c != null){
                return approvalProcessReminder.Additional_Recipient_3__c;
            }
            else if(recipiantNum == 4 
                && approvalProcessReminder.Alert_Recipient_4_From_Level__c <= approvalLevel
                && approvalProcessReminder.Additional_Recipient_4__c != null){
                return approvalProcessReminder.Additional_Recipient_4__c;
            }
            else if(recipiantNum == 5 
                && approvalProcessReminder.Alert_Recipient_5_From_Level__c <= approvalLevel
                && approvalProcessReminder.Additional_Recipient_5__c != null){
                return approvalProcessReminder.Additional_Recipient_5__c;
            }
            else if(recipiantNum == 6 
                && approvalProcessReminder.Alert_Recipient_6_From_Level__c <= approvalLevel
                && approvalProcessReminder.Additional_Recipient_6__c != null){
                return approvalProcessReminder.Additional_Recipient_6__c;
            }
            
            return null;
        }
    }
    
    //Wrap the standard salesfore ProcessInstance/InstanceItems
    public class ProcessInstanceRecord{
        
        public String id;
        public String status;
        public String targetObjectId;
        public DateTime lastModifiedDate;
        
        public list<ProcessInstanceItemRecord> items;
        
        public ProcessInstanceRecord(String pId, String pStatus, String pTargetObjectId, DateTime pDateTime){
            id = pId;
            status = pStatus;
            targetObjectId = pTargetObjectId;
            lastModifiedDate = pDateTime;
            
            items = new list<ProcessInstanceItemRecord>();
        }
        
        public void addItem(ProcessInstanceItemRecord itemRecord){
            items.add(itemRecord);
        }
    }
    
    
    public class ProcessInstanceItemRecord{
        public String actorId;
        public String stepStatus;
        public DateTime createdDate;
        
        public ProcessInstanceItemRecord(String pActorId, String pStepStatus, Datetime pCreatedDate){
            actorId = pActorId;
            stepStatus = pStepStatus;
            createdDate = pCreatedDate;
        }
    }
    
    
    //###########################################
    //### Schedule job to run
    //###########################################   
    webservice static String scheduleProcess(){
        String retMsg;
        
        try{
            list<CronTrigger> l_cr = [SELECT id, CronExpression, TimesTriggered, CronJobDetail.Name FROM CronTrigger WHERE CronJobDetail.Name = 'ApprovalProcessReminders' limit 1];
        
            if(l_cr.isEmpty()){
                String jobID = system.schedule('ApprovalProcessReminders', '0 0 * ? * *', new ScheduleApprovalReminders());

                retMsg = 'Process was scheduled';
            }
            else{
                retMSg = 'Process is already schduled.';
            }
        }
        catch(Exception e){
            retMsg = 'Error: ' + e.getMessage();
        }
        
        return retMsg;
    }

    //###########################################
    //### Abord job that is running
    //###########################################   
    webservice static String abortProcess(){
        string retMsg;
        try{
            list<CronTrigger> l_cr = [SELECT id, CronExpression, TimesTriggered, CronJobDetail.Name FROM CronTrigger WHERE CronJobDetail.Name = 'ApprovalProcessReminders' limit 1];
        
            if(l_cr.isEmpty())
                retMsg = 'Process is not schedule';
            else{
                System.abortJob(l_cr.get(0).id);
            
                retMSg = 'Process was aborted';
            }
        }
        catch(Exception e){
            retMsg = 'Error: ' + e.getMessage();
        }
            
        return retMsg;
    }
}







Hide Labels in Standard Layout

In some cases I wanted to hide the label in the standard layout.
For example, such requirement might seems logical if you are using image link and the information text is already in the image.



In such case, the labels redundant, but you can't hide them with the layout property, and no one want to build VF page just for hiding those labels.

The trick is in the label value. You cannot leave it empty, but you can add css properties (carefull, which attributes you adding!).
Before the label just add: <font style="font-size:0px">





Note that you might need to shortcut the previous text, but since you want to hide it, you won't mind.

The result:


Generic PDF Document

In many cases I was needed to create PDF document per requirement. Thought it will be great to have a generic code that can generate such document by setup.

I come up with such page that fill major parts of common requirements.
This post present this process and is mainly good for learning regarding generic code and it's advantages.

It contain the following components:
1.Apex Page: GenericDocument
2.Controller: vf_GenericDocument
3.3 Custom object:
Generic Document -Main detail regarding the document
Generic Document Table -(M/D with Generic Document) Objects that related to the document
Generic Document Element-(M/D with Generic Document) Element inside the document
4.Setup that define the relevant document.


The development allow the following functionalists:
1.Creating pdf document only by setup.
2.Adding footer/image/margin to the layout.
3.Use data from specific table + its child table(s).
4.Filter specific child records.
5.Add hard-coded section to the layout with rich text editor, which can contain also format text/images/so...
6.Positioning each component in the layout by setup.

What can be done to enhance it? Plenty....Ordering the data in the related child table, formatting this text, formatting each component in the document, roll-up summary fields, and so on...



The usage is by URL below, Where doid is the id of the generic document, recid is the id of the record you want to display.
/apex/GenericDocument?doid=a00w000000VqJY2&recid=001w0000019yccM


My example is with standard object of SF: Account, but it can be used to any custom object as well.

I defined GenericDocument record 'Customer Detail'.



1.Top data define general properties regarding the document: margins, style format, logo/footer images (should be added to Static Resources).

2.First related list - Generic Document Tables - define the objects that being used by the document.
In this example I have 2 objects: Account - main object, Contacts - child object.
Note, the field type: which make the difference.

3.Second related list - Generic Document Elements - define the components inside the document, which can be field from the relevant tables, hard-coded label, formatted text.
For example, first is 'Document Title', which is hard-coded text to be display in specific position.

Another example for Document Element is 'Customer Name', which show the name field from the Account.
As can be notice, in case the Type is 'Field', the fields 'Related To' and 'Source' take place, and mark the object and the field.


The code contain page and controller.

Note lines 13-17, get 5 optional parameters from URL. such parameters can be used to filter dynamically related child list (or they can used to other purposes as well).
For example, if I would like to query only contact where the field 'Level' is having specific value, I would assign in the URL:
/apex/GenericDocument?doid=a00w000000VqJY2&recid=001w0000019yccM&p1=<value>


In the setup:

Of course, the 'Where Filter' field can be also used to filter fixed value.



public class vf_GenericDocument {  

 public Generic_Document__c genericDoc {get; set;}  
 public list<CompObj> mainObjLst {get; set;}    //components related to main objects  
 public map<string, map<string, list<CompObj>>> msoc_compLst {get; set;}  //table --> recordId --> list of fields  
 public string imageLogo {get; set;}  
 public string imageFooter {get; set;}  

 public vf_GenericDocument(ApexPages.StandardController controller) {  
 
  string genericDocId = ApexPages.currentPage().getParameters().get('doid');  
  string objID = ApexPages.currentPage().getParameters().get('recid');  

  //upto 5 dynamic parameters from URL      
  string param1 = ApexPages.currentPage().getParameters().get('p1');  
  string param2 = ApexPages.currentPage().getParameters().get('p2');  
  string param3 = ApexPages.currentPage().getParameters().get('p3');  
  string param4 = ApexPages.currentPage().getParameters().get('p4');  
  string param5 = ApexPages.currentPage().getParameters().get('p5');              
  
  string mainObject;  
  map<string, sobject> mso_flatObj=new map<string, sobject>();  
  
  genericDoc = [ select id, Name, Logo__c, Logo_Height__c, Logo_Width__c, Footer__c, Footer_Height__c, Footer_Width__c, Add_Data_Top__c, Margins__c, Text_Format__c,  
      (select Name, Top__c, Left__c, Width__c, Height__c, Div_Name__c, Source__c, Type__c, Related_To__c, Rich_Text__c from Generic_Document_Elements__r),  
      (select name, API_Name__c, Type__c, Relation_Name__c, where_Filter__c from Generic_Document_Tables__r)  
      from Generic_Document__c where id =:genericDocId];  
      
  imageLogo = genericDoc.Logo__c!=null ? '/resource/' + genericDoc.Logo__c : '';  
  imageFooter = genericDoc.Footer__c!=null ? '/resource/' + genericDoc.Footer__c : '';  
  
  mainObjLst=new list<CompObj>();  
  map<string, string> mss_tableField=new map<string, string>();  
  map<string, Generic_Document_Table__c> mso_table=new map<string, Generic_Document_Table__c>();  
  
  for(Generic_Document_Table__c t : genericDoc.Generic_Document_Tables__r){  
   mss_tableField.put(t.name, 'select id,');  
   mso_table.put(t.name, t);  
  }  
  
  String sQuery;  
  for (Generic_Document_Element__c compRec : genericDoc.Generic_Document_Elements__r) {  
   if(compRec.Type__c=='Field') {
    mss_tableField.put(comprec.Related_To__c, mss_tableField.get(comprec.Related_To__c) + compRec.source__c + ',');  
   }
  }  

  for(Generic_Document_Table__c t : genericDoc.Generic_Document_Tables__r){  
   if(t.Type__c=='Master'){  
    sQuery = mss_tableField.get(t.name).subString(0, mss_tableField.get(t.name).length()-1);  
    mainObject=t.name;  
    for(Generic_Document_Table__c t2: mso_table.values()){  
     if(t2.Type__c=='Child') {
      sQuery += ',(' + mss_tableField.get(t2.name).subString(0, mss_tableField.get(t2.name).length()-1) + ' from ' + t2.Relation_Name__c + (t2.where_Filter__c != null ? ' where ' + t2.where_Filter__c : '') + ')';  
     }
    }  
    sQuery += ' from ' + t.API_Name__c + ' where ID = :objID';  
   }  
   else if(t.Type__c=='Flat'){  
    mso_flatObj.put(t.Name,   
     database.Query(mss_tableField.get(t.name).subString(0, mss_tableField.get(t.name).length()-1) + ' from ' + t.API_Name__c + ' where ' + t.Relation_Name__c + ' =:objID limit 1'));  
   }  
  }  
  system.debug('SOQL%%% ' + sQuery);  
  sobject mainObj = database.Query(sQuery);  
  
  for(Generic_Document_Element__c compRec : genericDoc.Generic_Document_Elements__r){  
   if(comprec.Related_To__c==mainObject)  {
    mainObjLst.add(new CompObj(compRec.Type__c=='Field' ? String.valueOf(mainObj.get(compRec.source__c)) : compRec.source__c, compRec));  
   }
   else if(mso_table.get(compRec.Related_To__c).Type__c=='Flat') {
    mainObjLst.add(new CompObj(String.valueOf(mso_flatObj.get(comprec.Related_To__c).get(compRec.source__c)), compRec));  
   }
  }  
  
  msoc_compLst=new map<string, map<string, list<CompObj>>>();   
  for(Generic_Document_Table__c t : genericDoc.Generic_Document_Tables__r){  
   if(t.type__c=='Child'){   
    msoc_compLst.put(t.Name, new map<string, list<CompObj>>());  
    //loop over all child records  
    for(sObject obj : (list<sObject>) mainObj.getSObjects(t.Relation_Name__c)){  
     msoc_compLst.get(t.Name).put(obj.id, new list<CompObj>());  
     for(Generic_Document_Element__c compRec : genericDoc.Generic_Document_Elements__r) {
      if(comprec.Related_To__c==t.name)  {
       msoc_compLst.get(t.Name).get(obj.id).add(new CompObj(compRec.Type__c=='Field' ? String.valueOf(obj.get(compRec.source__c)) : compRec.source__c, compRec));  
      }
     }
    }  
   }  
  }  
 }  

 public class CompObj {  
  public string value {get; set;}  
  public Generic_Document_Element__c comp {get; set;}  
  
  public CompObj(String vValue, Generic_Document_Element__c vComp) {  
   value=vValue;  
   comp=vComp;  
  }  
 }  
}  


The code in the page:

<apex:page standardController="Generic_Document__c" extensions="vf_GenericDocument" renderAs="PDF" applyBodyTag="false">  
 <head>     
  <style type="text/css">  
   body {  
    {!genericDoc.Text_Format__c}  
   }  
   @page {  
    size: A4;  
    margin: {!genericDoc.Margins__c}  
    position: relative;  
    top: 0mm;  
    left: 0mm;  
    width: 210mm;  
    height: 297mm;  
    page-break-after: avoid;  
    @top-left{  
     content : element(header);  
    }  
    @bottom-right{  
     content : element(footer);  
    }  
   }  
   div.header {  
    position : running(header);    
   }  
   div.footer{  
    position : running(footer);    
   }  
   /* First Page*/  
   div.page1_data {  
    position: absolute;  
    top: 0mm;  
    left: 2mm;  
    width: 210mm;  
    height: 190mm;  
   }   
   <apex:repeat value="{!mainObjLst}" var="param" id="cssRepeat">  
   div.{!param.comp.Div_Name__c}{  
    position: absolute;  
    top: {!param.comp.Top__c}  
    left: {!param.comp.Left__c}  
    width: {!param.comp.Width__c}  
    height: {!param.comp.Height__c}   
   }  
   </apex:repeat>  
   div.add_data{  
    position: absolute;  
    top: {!genericDoc.Add_Data_Top__c}  
    left: 2mm;  
    width: 210mn;  
   }  
  </style>    
 </head>  
 <body>  
  <div class="page">  
   <div class="page1_data">  
    <div class="header">  
     <apex:image url="{!imageLogo}" width="{!FLOOR(genericDoc.Logo_Width__c)}" height="{!FLOOR(genericDoc.Logo_Height__c)}" rendered="{!imageLogo!=''}"/>  
    </div>  
    <apex:repeat value="{!mainObjLst}" var="param" id="dataRepeat">  
     <div class="{!param.comp.Div_Name__c}">  
      <!--  <apex:outputText value="{!param.value}" rendered="{!not IsNull(param.value)}"/>-->  
      <apex:outputText value="{!SUBSTITUTE(JSENCODE(param.value), '\r\n', '<br/>')}" rendered="{!not IsNull(param.value)}" escape="false"/>  
      <apex:outputField value="{!param.comp.Rich_Text__c}" rendered="{!IsNull(param.value)}" />  
     </div>  
    </apex:repeat>  
    <div class="add_data">  
     <apex:repeat value="{!msoc_compLst}" var="tableO" id="tableReapet">  
      <br/><br/>  
      <u>{!tableO}</u>  
      <table width="700px">   
       <apex:repeat value="{!msoc_compLst[tableO]}" var="tableI" id="outer1">  
        <tr>  
         <apex:repeat value="{!msoc_compLst[tableO][tableI]}" var="I" id="outer2">  
          <td width="{!I.comp.width__c}"><apex:outputText escape="false">{!I.value} </apex:outputText></td>  
         </apex:repeat>  
        </tr>  
       </apex:repeat>  
      </table>  
     </apex:repeat>  
    </div>  
    <div class="footer">  
     <center><apex:image url="{!imageFooter }" height="{!FLOOR(genericDoc.Footer_Height__c)}" width="{!FLOOR(genericDoc.Footer_Width__c)}" rendered="{!imageFooter!=''}"/></center>  
    </div>   
   </div>  
  </div>  
 </body>   
</apex:page> 



And finally the output pdf of this example:





Retire of Permission on Profiles

If you are working as a Salesforce admin/developer you've probably heard somewhere that Salesforce is planning to make a significant cha...