function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Dennis H PalmerDennis H Palmer 

QuoteDocumentController.generateDocument used to work... It doesn't now. Any update on how to use this APEX method?

The below code used to work.  Now it doesn't.  Anyone know why?
List<id> documentIds = new List<id>();
id templateId = 'template id here';
id quoteId = 'quote id here';

id jobId = SBQQ.QuoteDocumentController.generateDocument('en_US', quoteId, templateId, 'Proposal.pdf', 'PDF', documentIds);

documentIds.add(quoteId);

jobId = SBQQ.QuoteDocumentController.saveProposal('en_US', quoteId, templateId, 'Proposal.pdf', 'PDF', documentIds);
Dennis H PalmerDennis H Palmer
It used to submit an APEX job that generates an outgoing PDF document. Now it doesn't do this and returns null for both job ids. Anyone know why?

I've already checked everywhere for documentation on this API and there is none.
krishna eranki 15krishna eranki 15
Hey Dennis,

Try adding papersize parameter(Default/Legal/ Letter/ A4) after outputformat.

Best
Krish
Dennis H PalmerDennis H Palmer
Hey thanks for the reply. Yes this is the answer. But those methods are declared as private so they’re not accessible.
SaravaSarava
Hey Dennis

The code posted by you to Generate Document works fine and I am able to create a document under quote. I have four templates and I want to generate the document for all four templates(4 documents) when a status changes. I have the generate document and save proposal class inside the template loop but only one document is getting saved. 
Please find the handler class below.
public static void Quote_GenerateDocument(list<SBQQ__Quote__c> QuoteLst){
        List<id> documentIds = new List<id>();
list<SBQQ__QuoteTemplate__c> templateLst = [SELECT Name, id from SBQQ__QuoteTemplate__c];
        system.debug('Quote List : '+QuoteLst.size()); //ANS : 4
        for (SBQQ__Quote__c QuoteRec : QuoteLst){
                 
            for (SBQQ__QuoteTemplate__c TemplateRec :templateLst ){

                   id jobId = SBQQ.QuoteDocumentController.generateDocument ('en_US', QuoteRec.Id, TemplateRec.id, 'Template Name.pdf', 'PDF', documentIds);

                   documentIds.add(QuoteRec.Id);

                   jobId = SBQQ.QuoteDocumentController.saveProposal ('en_US', QuoteRec.Id, TemplateRec.id, 'Template Name.pdf', 'PDF', documentIds);
                   System.debug('Job id :'+ jobId);
            }
                   
        }

Thanks
Dennis H PalmerDennis H Palmer
Hey Sarava!

Thanks for reaching out!  The only thing I can see here is that the documentIds List might need to be declared inside the Quote Template loop nested inside the Quote loop.
SaravaSarava
Thanks for your reply. I tried either way but no luck. Really stuck on this and couldn't figure out why all four documents are not saved.
Dennis H PalmerDennis H Palmer
It could be because CPQ adds these to the job list. Go into APEX Jobs in Setup and see if all four are added to the Queue.
SaravaSarava
Dennnis

Below is the error I am getting in the Apex job for generating the second document. Not sure how to pass a different value for documentID
Insert failed. First exception on row 0; first error: DUPLICATE_VALUE, duplicate value found: SBQQ__Key__c duplicates value on record with id: XXXXXXXXXXXXX: []

Let me know if you have any idea on this.

Thanks!
 
SaravaSarava
*Not sure how to pass a different value for SBQQ__Key__c
 
Dennis H PalmerDennis H Palmer
Hey Sarava,

The document ids need to be empty before each generate document call.  That is why I suggested to put the inside the template loop just before the call to generate document.  The list should be empty for the generate document call and contain one value for the save proposal call.
SaravaSarava
I did so but same error. 
The value of SBQQ__Key__c field on QuoteDocument object is assigned as Quoteid+'_1/_2/_3' based on the order of document. I guess it assigns the same value(eg: QuoteId_1) to all documents before the first job gets completed.
SaravaSarava
list<SBQQ__QuoteTemplate__c> templateLst = [SELECT Name, id 
                                                    from SBQQ__QuoteTemplate__c     ];                                          
        
        for (SBQQ__Quote__c QuoteRec : QuoteLst)
        {
            for (SBQQ__QuoteTemplate__c TemplateRec :templateLst )
            {
                List<id> documentIds 	= new List<id>(); 
                id jobId = SBQQ.QuoteDocumentController.generateDocument ('en_US', 
                                                                          QuoteRec.Id, 
                                                                          TemplateRec.id, 
                                                                          'DocumentName', 
                                                                          'PDF', 
                                                                          documentIds);
                system.debug('Doc Id size: '+documentIds.size()); //Size 0 for all 4 loops
                documentIds.add(QuoteRec.Id);
                system.debug('Doc Id size: '+documentIds.size()); //Size 1 for all 4 loops
                
                jobId = SBQQ.QuoteDocumentController.saveProposal ('en_US', QuoteRec.Id, TemplateRec.id, DocName, 'PDF', documentIds);
                system.debug('Quote ID :'+QuoteRec.Id+' Document name : '+DocName);
            }
        }
New code. 
 
Dennis H PalmerDennis H Palmer
That makes sense. Which version of CPQ are you on?
SaravaSarava
27
SaravaSarava
It is working now. I have created a beforeInsert trigger on 'quote document' and manually passing a unique value for SBQQ__key__c and then Inserting the document. So the duplication of the key is avoided.
Thanks for the help Dennis!
Dennis H PalmerDennis H Palmer
Wow that's great news! Are you able to post your full solution here? I think it would be great for others to find!
SaravaSarava
Please find the code below. 
Before Insert trigger
List<SBQQ__QuoteDocument__c> QuoteDocLst = new List<SBQQ__QuoteDocument__c>();
        for (SBQQ__QuoteDocument__c quoteDocRec : trigger.new){
            Datetime x = Datetime.now();
            Integer ms = x.millisecond();
            quoteDocRec.SBQQ__Key__c = quoteDocRec.SBQQ__Quote__c+'_'+ms;
            QuoteDocLst.add(quoteDocRec);
        }
Dennis H PalmerDennis H Palmer
Great! Thanks a lot Saradha!
elegalielegali

Hi Dennis,

I'm experiencing the same issue on a Org I'm working on, below the code snippet:

List<String> documentIds = new List<String>();

Id jobId = SBQQ.QuoteDocumentController.generateDocument('en_US', quoteId, templateId, 'Quote.pdf', 'PDF', documentIds);
System.debug('Job ID 1: ' + jobId);

documentIds.add(quoteId);

jobId = SBQQ.QuoteDocumentController.saveProposal('en_US', quoteId, templateId, 'Quote.pdf', 'PDF', documentIds);
System.debug('Job ID 2: ' + jobId);

Both jobId return null.

CPQ version installed: 212.10

Could you please help me find out what I'm doing wrong? Thanks!

Dennis H PalmerDennis H Palmer
Yes. This functionality has been disabled by Steelbrick/Salesforce. It is no longer supported. You must revert CPQ version back to before they disabled it.
elegalielegali
Hi Dennis,

Thank you for your quick answer!
That's unfortunate... I'll see if I can obtain the result I need without Apex, I would rather resort to downgrading only if strictly necessary :/

Thanks anyway!
Regards.
Dennis H PalmerDennis H Palmer
Hey Eleonora, You're welcome. I've wanted this functionality for a while and was super disappointed it was disabled. They have a new similar method that includes paper size but it's not a public method. I haven't checked in a while so maybe they've made the method accessible. Check for the same method, but with paper size.
John Lehrkind 10John Lehrkind 10
Hi Dennis,

Following up on this post. I was able to use the code below, adding in the paper size parameter. Both generateDocument and saveProposal both are running and I do see separate IDs being created for each method call.

The issue I am facing is: The ID that I get back from saveProposal gives me an error on the record indicating that "URL No Longer Exists". I checked permissions and everything seems in my scope. Is the record actualy being saved?
 
@RestResource(urlMapping='/saveDocument/*')
global class DocumentSaver {

      @HttpPost
      webservice static String saveDocument() {
         
               //add logic here......

               List<id> documentIds = new List<id>();
               id templateId = 'a0of200000J20pfAAB';
               id quoteId = 'a0qf200000DLb9SAAT';
               id jobId = SBQQ.QuoteDocumentController.generateDocument('en_US', quoteId, templateId, 'Proposal', 'PDF', 'Default', documentIds);
               System.debug('Job ID 1: ' + jobId);
               documentIds.add(quoteId);
               jobId = SBQQ.QuoteDocumentController.saveProposal('en_US', quoteId, templateId, 'Proposal', 'PDF', 'Default', documentIds);
               System.debug('Job ID 2: ' + jobId);

               return jobId;
       }

}

Any help here would be greatly appreciated! Thanks in advance!
John

 
Dennis H PalmerDennis H Palmer
Hey John, The ID that you get back from saveProposal is a job ID. If you go into apex jobs in setup, you should see your job and be able to see its status. I am glad to hear that the methods are working again! This is huge for a big pile of my clients. :D Are you able to see a generated document attached to your quote (a0qf200000DLb9SAAT) after running this code?
John Lehrkind 10John Lehrkind 10
Dennis!

Thank you for the quick reply! That makes so much sense!

I checked the Apex Jobs queue and it seems it is failing on the generateDocument call. Receiving "Unrecognized base64 character: {" now. Any thoughts on this?

Thanks,
John
SaravaSarava
Hey Dennis, I have been using Generate document class and I never faced any issues.
Dennis H PalmerDennis H Palmer
Hey John, Yes! This might have to do with your template. Debug Content: Go through your template and disable various sections until it doesn't fail. Then start enabling things until it fails. Then you know which content is the culprit. Then blank that content out and add html into it until it fails again. We're just narrowing down until we find the actual place where this error is caused. You can debug other things this same way. Blank out various fields, etc in your template, columns, sections and content until it starts working. Then add them back in to see which thing makes it fail. Otherwise, it's a problem in their methods and might need more internal attention on the CPQ end. Saradha, This is heavily dependent on the version of CPQ that is being used. Some versions of CPQ has this code working and some do not.
John Lehrkind 10John Lehrkind 10
Hmmm. I have actually identified it as the saveProposal method that is breaking on the base 64 character. 

I am using the most simple Quote Template. Removed Header, Footer and all sections. 

Sarava, are you using the saveProposal method too?

Thanks for all your help everyone!
John
John Lehrkind 10John Lehrkind 10
I tried saveProposal, saveAndEmailProposal, saveAndSendProposalForESignature, & saveProposal. All result in the Base 64 character issue. Seems like something is not being encoded internally. Can anyone else confirm? I am running Salesforce CPQ version 214.10.

Thanks!
John
Dennis H PalmerDennis H Palmer
Perhaps it could be an internal error. Have you confirmed the generateDocument is producing a document?
SaravaSarava
Hi John, yes i am using save proposal class too. With the following code i am generating the document on Quote for each template. 
for (SBQQ__QuoteTemplate__c TemplateRec :templateLst )
            {
                String DocName = OppName.Name+' - '+TemplateRec.Name;
                List<id> documentIds 	= new List<id>(); 
                id jobId = SBQQ.QuoteDocumentController.generateDocument ('en_US', QuoteRec.Id, TemplateRec.id, DocName, 'PDF', documentIds);
                documentIds.add(QuoteRec.Id);
                if (!test.isRunningTest()){
                jobId = SBQQ.QuoteDocumentController.saveProposal ('en_US', QuoteRec.Id, TemplateRec.id, DocName, 'PDF', documentIds);
                }

Thanks!
John Lehrkind 10John Lehrkind 10
Dennis!

It was a template issue! I am successfully generating and saving the document. Here is my final code:
@RestResource(urlMapping='/saveDocument/*')
global class DocumentSaver {

      @HttpPost
      webservice static String saveDocument() {
         
               //add logic here......
               
               
               List<id> documentIds = new List<id>();
               id templateId = 'a0of200000J20pf';
               id quoteId = 'a0qf200000DLb9S';
               id jobId = SBQQ.QuoteDocumentController.generateDocument('en_US', quoteId, templateId, 'Proposal', 'PDF', 'Default', documentIds);
               System.debug('Job ID 1: ' + jobId);
               documentIds.add(quoteId);
               id jobId2 = SBQQ.QuoteDocumentController.saveProposal('en_US', quoteId, templateId, 'Proposal', 'PDF', 'Default', documentIds);
               System.debug('Job ID 2: ' + jobId2);
               
               

               return jobId2;
       }

}

 
Dennis H PalmerDennis H Palmer
Awesome John! Good work figuring it out!
John Lehrkind 10John Lehrkind 10
My next question is: How do I get a hold of the document ID?

In this code segment, I receive the Job ID back. From here, I can query the AsyncApexJob object to get info about that job, but I do not see any reference to the QuoteDocument or the Document. I am missing the connection here back to the newly created record.

As you may have seen above, I am using a webservice apex class to initiate this document generation. From there, I'd like to grab the documentId so I could present the document to the user via a FileDownload link (i.e. https://c.na53.content.force.com/servlet/servlet.FileDownload?file=00Pf2000012NdfREAS).

Where is the data connection between the Job and the created record? Can't seem to find this in the docs.

Thanks!
John
Dennis H PalmerDennis H Palmer
What I would do in this case is: have the list of Quote Documents associated to this Quote ID on the screen with links, etc. Then when you get a status of completed from your apex job status check call, reload your list of documents. Then the user can click a link to view (or you can just auto load the latest document in the list).