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: