
renderAs="PDF" as an attribute to the <apex:page> tag.
The following sample scenario, based on the use case of generating a sales quote, highlights this capability. In this example you will see the two mechanisms by which this functionality can be leveraged:
The framework that will be used to describe this example mirrors the design pattern after which Visualforce has been modeled, MVC or Model-View-Controller.
While conversion of a Visualforce page to a PDF document is the primary subject of this example additional important concepts and capabilities are also highlighted by this example which include:
The model is the schema or data interface to the application. In this example the Model refers to SObjects (salesforce objects). The SObjects referenced in the model for this example include:
Standard:
Custom:
For your convenience, an unmanaged appexchange package has been created that contains the definitions of the custom objects as well as some additional metadata to help get started with this example. To install this package in your Developer Edition or Sandbox account click on this link.
The view layer is the presentation of your information to the user. In this example the view is comprised of three Visualforce pages QuoteNew, QuotePDF and QuoteAttach. The markup for each follows.
Note: Due to dependencies between pages and controllers, extensions and static resources you should follow the ordered instructions in the section named "Installing this sample" found below.
<apex:page standardController="Quote__c" showHeader="false" renderAs="pdf">
<body>
<apex:stylesheet value="{!URLFOR($Resource.pdfresources, 'styles.css')}"/>
<apex:image value="{!URLFOR($Resource.pdfresources,'logo.gif')}"/>
<apex:panelGrid columns="1" styleClass="companyTable" width="100%">
<apex:outputText value="{!$Organization.Name}" styleClass="companyName"/>
<apex:outputText value="{!$Organization.Street}"/>
<apex:outputText value="{!$Organization.City}, {!$Organization.State} {!$Organization.PostalCode}"/>
<apex:outputText value="{!$Organization.Phone}"/>
</apex:panelGrid>
<apex:outputPanel layout="block" styleClass="line"/>
<apex:panelGrid columns="1" styleClass="centered" width="100%">
<apex:panelGrid columns="2" width="100%" cellpadding="0" cellspacing="0" columnClasses="left,right">
<apex:outputText value="Quote# {!Quote__c.name}" styleClass="customerName"/>
<apex:outputField value="{!Quote__c.lastmodifieddate}" style="text-align:right"/>
</apex:panelGrid>
<apex:outputText value="{!Quote__c.opportunity__r.account.name}" styleClass="customerName"/>
<apex:outputText value="{!Quote__c.contact__r.name}" styleClass="contactName"/>
</apex:panelGrid>
<apex:panelGrid columns="1">
<apex:outputText value="{!Quote__c.opportunity__r.account.name}"/>
<apex:outputText value="{!Quote__c.contact__r.mailingStreet}"/>
<apex:panelGroup >
<apex:outputText value="{!Quote__c.contact__r.mailingCity}"/>
<apex:outputText value=", {!Quote__c.contact__r.mailingState}"/>
<apex:outputText value=" {!Quote__c.contact__r.mailingPostalCode}"/>
</apex:panelGroup>
<apex:outputText value="Phone: {!Quote__c.contact__r.phone}"/>
</apex:panelGrid>
<apex:outputPanel layout="block" styleClass="lineSmall"/>
<apex:repeat value="{!Quote__c.quote_items__r}" var="line">
<apex:panelGrid columns="2" columnClasses="left,right" width="100%">
<apex:panelGroup >
<apex:outputText value="{!line.name}" styleClass="productName"/>
<apex:outputPanel layout="block" styleClass="productDetail">
<apex:panelGrid columns="2" columnClasses="left,none">
<apex:outputText value="Units:" style="font-weight:bold"/>
<apex:outputField value="{!line.Quantity__c}"/>
<apex:outputText value="Unit Price:" style="font-weight:bold"/>
<apex:outputField value="{!line.Unit_Price__c}"/>
<apex:outputText value="Product Code:" style="font-weight:bold"/>
<apex:outputField value="{!line.product__r.productCode}"/>
<apex:outputText value="Description:" style="font-weight:bold"/>
<apex:outputField value="{!line.product__r.description}"/>
</apex:panelGrid>
</apex:outputPanel>
</apex:panelGroup>
<apex:outputField value="{!line.Total_Price__c}" styleClass="productName"/>
</apex:panelGrid>
</apex:repeat>
<apex:outputPanel layout="block" styleClass="lineSmall"/>
<apex:panelGrid columns="2" columnClasses="right" width="100%">
<apex:panelGrid columns="2" cellpadding="10" columnClasses="right totalLabel,right total" width="100%">
<apex:outputText value="Total"/>
<apex:outputField value="{!Quote__c.Total_Price__c}"/>
</apex:panelGrid>
</apex:panelGrid>
<apex:outputPanel layout="block" styleClass="line"/>
</body>
</apex:page>
<apex:page standardController="Quote__c" Extensions="quoteExt" action="{!attachQuote}">
{!Quote__c.Opportunity__c}{!Quote__c.name}
</apex:page>
<apex:page standardController="Quote__c" extensions="quoteExt">
<apex:sectionHeader title="Edit Quote" Subtitle="New Quote"/>
<apex:form >
<apex:inputHidden value="{!q.Opportunity__c}"/>
<apex:pageBlock title="Quote Information" mode="edit">
<apex:pageBlockButtons >
<apex:commandButton value="Save" action="{!save}"/>
<apex:commandButton value="Cancel" action="{!cancel}"/>
</apex:pageBlockButtons>
<apex:pageBlockSection title="Information" columns="1">
<apex:inputField value="{!Quote__c.Opportunity__c}"/>
<apex:inputField value="{!Quote__c.Contact__c}"/>
<apex:inputField value="{!Quote__c.Description__c}"/>
</apex:pageBlockSection>
<apex:pageBlockSection title="Address Information" columns="1">
<apex:outputField value="{!Quote__c.opportunity__r.account.name}"/>
<apex:inputField value="{!Quote__c.Street__c}"/>
<apex:inputField value="{!Quote__c.City__c}"/>
<apex:inputField value="{!Quote__c.State__c}"/>
<apex:inputField value="{!Quote__c.Zip_Code__c}"/>
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>
The controller is the layer that provides logic, data access and navigation work to your application. In this example the controller layer is comprised of the standard controller for the Quote__c custom object as well as the QuoteExt extension class written in Apex.
/*
* This class which extends the Quote__c standard controller is utilized by the QuoteNew
* Visualforce page to default values for the user from the related opportunity during
* the creation of a new quote. It also provides an action that generates the PDF and creates
* an attachment under the parent Quote record who's id is passed into the same Visualforce page
* that can be accessed in the UI to generate the same PDF.
*/
public class quoteExt {
/* The standard controller object which will be used later for navigation and to invoke
it's save action method to create the new Quote. */
ApexPages.StandardController controller;
/* The quote property which is used by the quoteNew and quotePDF Pages and this controller
to provide access to the relevant quote information. */
public Quote__c q {get;set;}
/* The constructor to this extension class which takes the standard controller as its argument
which allows this class to access the methods and information available in the instance for
the quote.*/
public QuoteExt(ApexPages.StandardController c) {
controller = c;
q = (Quote__c) c.getRecord();
/* Set the quote's lookup field to opportunity to the value of the oppid request parameter. */
q.opportunity__c = ApexPages.currentPage().getParameters().get('oppid');
/* If non-null, get the opportunity and contact role for appropriate defaulting of values. */
if(q.opportunity__c != null) {
/* Set the related opportunity with the result of the query that traverses to account for display of the name
and down to get the primary contact role. */
q.opportunity__r = [select name, account.name,
(select contact.name, contact.mailingStreet, contact.mailingcity, contact.mailingstate,
contact.mailingpostalcode
from opportunityContactRoles
where isPrimary = true)
from opportunity
where id = :q.opportunity__c];
if(q.opportunity__r.opportunityContactRoles.size() == 1) {
OpportunityContactRole r = q.opportunity__r.opportunityContactRoles[0];
q.contact__r = r.contact;
q.contact__c = r.contact.id;
q.street__c = r.contact.mailingstreet;
q.city__c = r.contact.mailingcity;
q.state__c = r.contact.mailingstate;
q.zip_code__c = r.contact.mailingpostalcode;
}
}
}
/* This save method will be called instead of the standard controller save method when bound
to an action component in an associated page, (apex:commandButton on the quoteNew
Visualforce page in this example) */
public PageReference save() {
/* Invoke the standard controller save method which returns the pageReference class
that will be used in the navigation, i.e. send the user to the expected location based on
standard navigation rules provided by salesforce.com */
PageReference p = controller.save();
/* The quote's record Id */
id qid = controller.getId();
/* The collection of quote_item__c records to be created based on the related opportunity's
opportunity line items (products), if any. */
Quote_Item__c[] items = new Quote_Item__c[]{};
/* Establish the quoteItem collection based on the opportunity's line items, if any */
for(OpportunityLineItem oli:[select quantity, unitprice, pricebookEntry.product2.name, pricebookEntry.product2id
from opportunitylineitem
where opportunityid = :q.opportunity__c]) {
items.add(new Quote_Item__c(quantity__c = oli.quantity, unit_price__c = oli.unitprice, quote__c = qid,
name = oli.pricebookentry.product2.name,product__c = oli.pricebookentry.product2id));
}
/* If any line items need to be inserted do so here.*/
if(items.size() > 0) insert items;
/* Return the page reference generated by the standard controller save, which usually drops the user
on the detail page for the newly created object. */
return p;
}
/* The action method that will generate a PDF document from the QuotePDF page and attach it to
the quote provided by the standard controller. Called by the action binding for the attachQuote
page, this will do the work and take the user back to the quote detail page. */
public PageReference attachQuote() {
/* Get the page definition */
PageReference pdfPage = Page.quotePDF;
/* set the quote id on the page definition */
pdfPage.getParameters().put('id',q.id);
/* generate the pdf blob */
Blob pdfBlob = pdfPage.getContent();
/* create the attachment against the quote */
Attachment a = new Attachment(parentId = q.id, name=q.name + '.pdf', body = pdfBlob);
/* insert the attachment */
insert a;
/* send the user back to the quote detail page */
return controller.view();
}
}
In order to consume this sample within your developer edition or sandbox account you should follow these steps (in order):
Creation of Apex Classes, Visualforce Pages and Static Resources can be performed under setup from the App Setup > Develop menu.
Alternatively you can also use development mode to create pages and edit them in place in the application. To enable development mode edit your user record under Setup > Personal Setup > My Personal Information > Personal Information, click edit and check the box for "Development Mode" on the right side of the page and save. Now when you navigate to a visualforce page per the instructions below you will see the footer at the bottom of the page where you can make changes to the page and instantly see them in the display area of the browser window.
In order to invoke the QuoteNew page you can either add the custom "New Quote" button in the appexchange package to the quote related list on the opportunity layout or simply call the page from within your salesforce.com account as such:
https://<instance>.salesforce.com/apex/quoteNew?oppid=<opportunityId>
Where <instance> is na1, na2, tapp0, etc. and <opportunityId> is the record ID of an opportunity record (ideally one with line items and a primary contact role established)
Complete the form and save it. Notice that the line items were copied over (if the opportunity had line items).
Now click on the "Generate Printable Format" button which will call the Visualforce page which will be converted to a PDF on the server based on the renderAs attribute value on the page component tag being "pdf". Change it to "html" to see it as a web page.
Click on the "Attach Printable Format" button to call the same page programmatically from apex and attach it to the same quote. When the page refreshes you should see an new attachment under the quote.
Given this sample includes apex, the following is an additional class that tests the methods in the standard controller extension above. This class is not required to install this sample into your sandbox or developer edition org but you can use the code below to create the respective Apex class for reference. If after doing so you'd like to execute the tests in this class you can do so by clicking on the "Run Test" button on the class detail page under setup to see what the test results look like.
Tests can also be executed from the Force.com IDE - see the documentation for installing and using the IDE for additional information.
/*
* The purpose of this class is to test the quoteExt class. The @IsTest annotation
* excludes this class from the system cache and as such it is not counted against the
* org code size limit. NOTE: this test and the sample ASSUMES the organization has
* opportunity products enabled and does NOT have multi-currency enabled.
*/
@IsTest private class quoteExtTests {
/* This is a basic test which simulates the primary positive case for the
save method in the quoteExt class. */
public static testmethod void basicSaveTest() {
Opportunity o = quoteExtTests.setupTestOpportunity();
/* Construct the standard controller for quote. */
ApexPages.StandardController con = new ApexPages.StandardController(new Quote__c());
/* Switch to runtime context */
Test.startTest();
/* Construct the quoteExt class */
QuoteExt ext = new QuoteExt(con);
/* Call save on the ext */
PageReference result = ext.save();
/* Switch back to test context */
Test.stopTest();
/* Verify the navigation outcome is as expected */
System.assertEquals(result.getUrl(), con.view().getUrl());
/* Verify the oppty amount is equivalent to the quote amount */
Decimal opportunityAmount = [select amount from opportunity where id = :o.id].amount;
Decimal quoteAmount = [select total_price__c from quote__c where id = :con.getId()].total_price__c;
System.assertEquals(opportunityAmount, quoteAmount);
}
/* This is a basic test which simulates the primary positive case for the
attachQuote method in the quoteExt class. */
public static testmethod void basicAttachTest() {
Opportunity o = quoteExtTests.setupTestOpportunity();
/* Construct the standard controller for quote. */
ApexPages.StandardController con = new ApexPages.StandardController(new Quote__c());
/* Construct the quoteExt class */
QuoteExt ext = new QuoteExt(con);
/* Call save on the ext */
ext.save();
/* Set the extension quote object using the id on the controller. */
ext.q = new quote__c(id = con.getId());
/* Switch to runtime context */
Test.startTest();
/* Simulate the button invocation of the attachQuote action method
on the extension. */
PageReference result = ext.attachQuote();
/* Switch back to test context */
Test.stopTest();
/* Verify the navigation outcome is as expected */
System.assertEquals(result.getUrl(), con.view().getUrl());
/* Verify the attachment was created. */
System.assert([select name from attachment where parentid = :con.getId()].name != null);
}
/* This setup method will create an opportunity with line items and a primary
contact role for use in various tests. */
private static Opportunity setupTestOpportunity() {
/* Create an account */
Account a = new Account();
a.name = 'TEST';
Database.insert(a);
/* Get the standard pricebook. There must be a standard pricebook already
in the target org. */
Pricebook2 pb = [select name, isactive from Pricebook2 where IsStandard = true limit 1];
if(!pb.isactive) {
pb.isactive = true;
Database.update(pb);
}
/* Get a valid stage name */
OpportunityStage stage = [select MasterLabel from OpportunityStage limit 1];
/* Setup a basic opportunity */
Opportunity o = new Opportunity();
o.Name = 'TEST';
o.AccountId = a.id;
o.CloseDate = Date.today();
o.StageName = stage.masterlabel;
o.Pricebook2Id = pb.id;
/* Create the opportunity */
Database.insert(o);
/* Create a contact */
Contact c = new Contact();
c.lastname = 'LASTNAME';
c.firstname = 'FIRSTNAME';
Database.insert(c);
/* Create the opportunity contact role */
OpportunityContactRole r = new OpportunityContactRole();
r.ContactId = c.id;
r.OpportunityId = o.id;
r.IsPrimary = true;
r.role = 'ROLE';
Database.insert(r);
/* Create a product2 */
Product2 p = new Product2();
p.Name = 'TEST';
Database.insert(p);
/* Create a pricebook entry. */
PricebookEntry pbe = new PricebookEntry();
pbe.Pricebook2Id = pb.id;
pbe.Product2Id = p.id;
pbe.IsActive = true;
pbe.UnitPrice = 1;
Database.insert(pbe);
/* Create a line item */
OpportunityLineItem i = new OpportunityLineItem();
i.opportunityId = o.id;
i.pricebookentryid = pbe.id;
i.quantity = 1;
i.unitprice = 1;
Database.insert(i);
/* Set up the opportunity with the related records */
r.Contact = c;
r.Opportunity = o;
o.Account = a;
i.Opportunity = o;
pbe.Product2 = p;
pbe.Pricebook2 = pb;
i.PricebookEntry = pbe;
/* Set the request parameter that the constructor for quoteExt is expecting */
PageReference pref = Page.quoteNew;
pref.getParameters().put('oppid',o.id);
Test.setCurrentPage(pref);
return o;
}
}