NetSweet/netsuite

Wondering if there is a way to download or get PDF from Invoices

cavi21 opened this issue Β· 14 comments

Hi there! first of all thanks a lot for all the hard work putting into this gem! It's great! πŸŽ‰

I was wondering if anyone knows if there is a way to download or print a PDF for an invoice. Or maybe be able to email to someone via de SOAP API. I looked a lot here and also in Google but I couldn't find anything, that's why I'm asking here.

Or maybe there are another approaches for this type of situation.
Any insight or recommendation is welcome
Again, awesome work!
Cheers!

@cavi21 unfortunately, there's not! Best option would be to expose the generated PDF link on a custom field and then pull the PDF off of the custom field via SOAP. This would probably take some Suitescript to accomplish in combination with the SOAP API calls.

Check out the Slack groupβ€”some folks there may have some more ideas!

http://opensuite-slackin.herokuapp.com

Thanks @iloveitaly for the reply! I'll check that group and check the approach you propose!
Have a nice week!

Hey @cavi21 how did you resolve this in the end? Is there a write up anywhere that you could please share?

Hi @bmiles I ended up doing a Restlet custom endpoint with SuiteScript 2 and then calling from the outside on demand and storing the PDF on an S3

Here is the code for the Restlet endpoint:

define(['N/render', 'N/file', 'N/encode', 'N/record', 'N/log'], function (render, file, encode, record, log) {

    /**
     * Fetch an Invoice based on the ID and return a PDF encoded in Base64
     *
     * @author cavi21
     *
     * @NApiVersion 2.0
     * @NModuleScope SameAccount
     * @NScriptType RESTlet
     */
    var exports = {
        get: getInvoicePDF
    };

    function getInvoicePDF(requestParams){
        log.audit({title: "getInvoicePDF"});

        var invoiceId = requestParams.invoiceId;
        var invoice = fetchInvoice(invoiceId);

        if (!invoice) {
            return { error: "There is no invoice with id " + invoiceId };
        }

        var invoicePDFString = generateInvoicePDFString(invoice);

        var base64EncodedInvoicePdfString = encode.convert({
            string: invoicePDFString,
            inputEncoding: encode.Encoding.UTF_8,
            outputEncoding: encode.Encoding.BASE_64
        });

        return {
            data64: invoicePDFString
        }
    }

    function fetchInvoice(invoiceId){
        log.audit({title: "fetchInvoice"});

        try {
            log.debug({
                title: "fetchInvoice",
                details: "fetchInvoice with InvoiceId: "+invoiceId
            });
            return record.load({
                type: record.Type.INVOICE,
                id: invoiceId
            })
        } catch (e) {
            log.error({
                title: "Error fetching Invoice",
                details: e.message
            });
        }
    }

    function generateInvoicePDFString(invoice){
        log.audit({title: "generateInvoicePDFString"});

        var invoiceId = invoice.getValue({fieldId: 'id'});
        var invoiceRenderer = render.create();
        var xmlTemplateFile = file.load({
            id: '7938' // ID of Custom Template for the Invoices
        });
        invoiceRenderer.templateContent = xmlTemplateFile.getContents();

        invoiceRenderer.addRecord({
            templateName: 'record',
            record: invoice
        });

        var pdfFile = invoiceRenderer.renderAsPdf();

        return pdfFile.getContents();
    }

    return exports;
});

Hope this helps!
Cheers

Cool, thanks for sharing @cavi21!

Thank you @cavi21 πŸŽ‰

@cavi21 sorry to ask again, where are you getting the ID for the invoice template? Is that coming from the URI or somewhere else? Thanks

hey @bmiles sorry for the late reply! That ID that's assigned to the XML that works as the template in the File Cabinet, if you rollover the files you get the ID in the URL (maybe there is another way for this haha!), in the case of this template in my cabinet the URL is https://[ns-instance-id].app.netsuite.com/app/common/media/7938?folder=7938

Hope this helps!

thank you @cavi21

I'll share this in case it helps someone:

After a long period of debugging this error:

{
    "error": {
        "code": "USER_ERROR",
        "message": "{\"type\":\"error.SuiteScriptError\",\"name\":\"USER_ERROR\",\"message\":\"Error Parsing XML: The reference to entity \\"c\\" must end with the \';\' delimiter.\",\"stack\":[\"createError(N/error)\",\"generateInvoicePDFString(/SuiteScripts/netsuitepdf.js:88)\",\"getInvoicePDF(/SuiteScripts/netsuitepdf.js:27)\"],\"cause\":{\"type\":\"internal error\",\"code\":\"USER_ERROR\",\"details\":\"Error Parsing XML: The reference to entity \\"c\\" must end with the \';\' delimiter.\",\"userEvent\":null,\"stackTrace\":[\"createError(N/error)\",\"generateInvoicePDFString(/SuiteScripts/netsuitepdf.js:88)\",\"getInvoicePDF(/SuiteScripts/netsuitepdf.js:27)\"],\"notifyOff\":false},\"id\":\"\",\"notifyOff\":false,\"userFacing\":false}"
    }
}

Using a default advanced PDF template for invoices, I determined that the issue was being caused by the following line in the template.xml for that template.

<img src="${companyInformation.logoUrl}" style="float: left; margin: 7px" />

The company logo url that was being pulled in by the template has & characters that aren't automatically escaped causing the XML to be invalid. To fix this, I got the URL of the corresponding logo file from the documents section of netsuite and used that in my template. I then changed the URL to replace & characters with &amp;.

Thank you, @cavi21. I have a similar requirement but I don't have a custom invoice template. Is there a way I can skip the xmlTemplateFile.getContents(); part? Couldn't find any documentation about a standard invoice pdf, or couldn't find one in the File Cabinet in NS.

Hi @robikovacs I did not have that requirement, and it was a long time ago since I review this, just doing a quick search I found this link that maybe helps, https://www.youtube.com/watch?v=mKugF5ikZhU, to create a custom invoice or to look for a base one. Sorry for not be able to help

@robikovacs also reading the docs for N/render seems that maybe there is a default way of doing it, here is the API docs for references https://docs.oracle.com/cloud/latest/netsuitecs_gs/NSAPI/NSAPI.pdf

I've replaced just this part:

function getInvoicePDF(requestParams) {
    log.audit({ title: "getInvoicePDF" });

    var invoiceId = parseInt(requestParams.invoiceId);
    var invoice = fetchInvoice(invoiceId);

    if (!invoice) {
      return { error: "There is no invoice with id " + invoiceId };
    }

    var invoicePDFString = generateInvoicePDFString(invoiceId);

    encode.convert({
      string: invoicePDFString,
      inputEncoding: encode.Encoding.UTF_8,
      outputEncoding: encode.Encoding.BASE_64,
    });

    return {
      data64: invoicePDFString,
    };
  }
....
function generateInvoicePDFString(invoiceId) {
    log.audit({ title: "generateInvoicePDFString" });

    var xmlTemplateFile = render.transaction({
      entityId: invoiceId,
      printMode: render.PrintMode.PDF,
    });

    return xmlTemplateFile.getContents();
  }

It works fine now, thank you @cavi21 !

Thanks @cavi21 and @robikovacs for the solutions, both work perfectly (even in 2023) πŸ‘Œ
I've ended up using the latter, here's the full restlet code:

define(['N/render', 'N/encode'], function (render, encode) {
  /**
   * @NApiVersion 2.1
   * @NModuleScope SameAccount
   * @NScriptType RESTlet
   */
  const exports = {
    get: getInvoicePDF
  };

  function getInvoicePDF(requestParams) {
    const pdfInvoice = render.transaction({
      entityId: parseInt(requestParams.invoiceId, 10),
      printMode: render.PrintMode.PDF
    });

    const invoicePDFString = pdfInvoice.getContents();

    encode.convert({
      string: invoicePDFString,
      inputEncoding: encode.Encoding.UTF_8,
      outputEncoding: encode.Encoding.BASE_64
    });

    return {
      data64: invoicePDFString
    };
  }

  return exports;
});

There is one caveat - using this solution will pick the "Preferred" template for invoices and render it (see: Customize > Forms > Advanced PDF/HTML Templates and look up the ones applied to "Invoice" record type).

However, if you need to fully customize template being used in rendering, use @cavi21 solution - you can export the template you want by going through Edit > Download Template and putting it in the File Cabinet and then referencing it in the following line:

const xmlTemplateFile = file.load({ id: 'path within file cabinet' })