0

Is there a way to have a selection of many transactions printed into a single PDF document? I only see two options which seem to have significant drawbacks:

1) Load individual records into each of their own nlobjTemplateRenderer objects, and then stitch them all together within tags before rendering to PDF. Has a limit of less than 50 transactions depending on other actions taken when used within a Suitelet.

2) Do a search based upon internals IDs of selected records and pass the search results into a nlobjTemplateRenderer object. This method, based upon existing documentation, does not lead me to believe that it will properly display records with line data as result columns completely within a single document.

It almost seems like my best option is #1, but to split the desired transaction up into groups of 5-10 records and repeatedly calling a Suitelet with the small groups in the hopes of meeting the 45-second timeout limit of nlapiRequestURL before stitching together all of the results and returning the final PDF document. I pretty much see a basic form of that as the following:

// initial called function that will return completed PDF document file
function buildPdfFromRecords() {
    var pdfBuilder = [];
    var selectedIDs = [];
    var chunks = chunkify(selectedIDs, 10);
    for (var c = 0; c < chunks.length; c++) {
        var param = { id_list : JSON.stringify(chunks[s]) };
        var result = nlapiRequestURL(url, param).getBody();
        pdfBuilder.push(result);
    }
    var finalXML = "<pdfset>" + pdfBuilder.join("") + "</pdfset>";
    var pdfDoc = nlapiXMLToPDF(finalXML);
}

// function in suitelet called by url to handle individual groups of record internal IDs
// to mitigate scripting governance limits
function handleRecordIdListRequest(request, reponse) {
    var idList = JSON.parse(request.getParameter("id_list"));
    var templateXML = nlapiLoadRecord("template.txt").getValue();
    var pdfBuilder = [];
    for (var i = 0; i < idList.length; i++) {
        var transRecord = nlapiLoadRecord("recordtype", idList[i]);
        var renderer = nlapiCreateTemplateRenderer();
        renderer.setTemplate(templateXML);
        renderer.addRecord("record", transRecord);
        pdfBuilder.push(renderer.renderToString());
    }
    response.write(pdfBuilder.join(""));
}

If this is really the best way, then so be it, but I'm hoping there's a more elegant solution out there that I'm just not seeing.

Michael McCauley
  • 853
  • 1
  • 12
  • 37

2 Answers2

1

There are a number of pieces you can stitch together to get this done.

  1. In the post handler of your Suitelet use the N/task library to schedule a map/reduce task. The task.submit method returns a taskId that you can use to monitor the progress of your job. Once your UI has a taskId it can periodically check to see if the task has completed. When complete you can show the generated .pdf. You could also let the user know that the pdf might take a few minutes to generate and offer to email it to them when done. Here's a snippet that schedules a scheduled script with parameters:

  const mrTask = task.create({
    taskType:task.TaskType.SCHEDULED_SCRIPT,
    scriptId:'customscript_knsi_batch_products',
    deploymentId: deploymentId,
    params: {
      custscript_knsi_batch_operator:user.id,
      custscript_knsi_batch_sourcing: sourcingId
    }
  });

  try{
    const taskId = mrTask.submit();
    context.response.setHeader({name:'content-type', value:'application/json'});
    context.response.write(JSON.stringify({
      success:true,
      message:'queued as task: '+ taskId
    }));
  }catch(e){
    log.error({
      title:'triggering '+ sourcingId +' for '+ user.email,
      details:(e.message || e.toString()) + (e.getStackTrace ? (' \n \n' + e.getStackTrace().join(' \n')) : '')
    });
    context.response.setHeader({name:'content-type', value:'application/json'});
    context.response.write(JSON.stringify({
      success:false,
      message:'An error occured scheduling this script\n'+e.message
    }));
  1. Use a Map/Reduce script where your map method generates and returns each transaction's pdf file url. You'll only have a single key so that the results of all map stages coalesce into a single reduce.
  2. In the reduce step you can generate open and close pdf files as necessary and put their references into your mapped array of pdfs.
  3. Use the pdfset to bind all your individual pdfs into a single pdf:

function renderSet(opts){
 var tpl = ['<?xml version="1.0"?>','<pdfset>'];

 opts.files.forEach(function(id, idx){
  const partFile = file.load({id:id});
  var pdf_fileURL = xml.escape({xmlText:partFile.url});
  tpl.push("<pdf src='" + pdf_fileURL + "'/>");
 });

 tpl.push("</pdfset>");

 log.debug({title:'bound template', details:xml.escape({xmlText:tpl.join('\n')})});

 return render.xmlToPdf({
  xmlString:  tpl.join('\n')
 });
}
bknights
  • 14,408
  • 2
  • 18
  • 31
  • I like this idea, and I've been testing it out. But now I'm getting weird errors from the XMLToPDF parser where it says "Error Parsing XML" and gives back the url of the first PDF added, but the URL always starts with "system.netsuite.com", regardless if I append "https://system.netsuite.com" to the beginning of the file URL or not. And I've verified that the ampersands are indeed escaped. – Michael McCauley Nov 20 '18 at 22:46
  • Oh, nevermind that last comment. I forgot to set the component PDF docs to "Is Online". Making that change solved the little issue there. – Michael McCauley Nov 20 '18 at 22:59
0

Why not use a Map Reduce script to generate the PDF? Does it need to be a Suitelet?

Rusty Shackles
  • 2,802
  • 10
  • 11
  • I'm starting with a Suitelet in order to generate an interface for users. Also, I'm unfamiliar with the map/reduce script type, I haven't had enough time to check it out. Are you able to pass it parameters and get a response back that allows you present a PDF to a user in the browser tab? Because that's why I'm using Suitelets. – Michael McCauley Nov 20 '18 at 15:45