Create PDF from Google Sheet Template - google-apps-script

I am fairly new to code and App Script, but I've managed to come up with this from research.
Form submitted, Sheet populated, take entry data, copy and append new file, save as pdf, email pdf
I've created examples of what I've been trying to do
Link to form - https://docs.google.com/forms/d/e/1FAIpQLSfjkSBkn3eQ1PbPoq0lmVbm-Dk2u2TP_F_U5lb45SddsTsgsA/viewform?usp=sf_link
link to spreadsheet - https://docs.google.com/spreadsheets/d/1kWQCbNuisZsgWLk3rh6_Iq107HoK7g-qG2Gln5pmYTE/edit?resourcekey#gid=1468928415
link to template - https://docs.google.com/spreadsheets/d/1Ye7DyJQOjA3J_EUOQteWcuASBCfqlA-_lzyNw0REjY8/edit?usp=sharing
However I receive the following error - Exception: Document is missing (perhaps it was deleted, or you don't have read access?)
at Create_PDF(Code:32:34)
at After_Submit(Code:13:21)
App Script Code as follows - If I use a google Doc as a template it works. However I would like to use a spreadsheet as a template, and have the result pdf content fit to page. Please let me know if you need any additional information for this to work.
function After_Submit(e, ){
var range = e.range;
var row = range.getRow(); //get the row of newly added form data
var sheet = range.getSheet(); //get the Sheet
var headers = sheet.getRange(1, 1, 1,5).getValues().flat(); //get the header names from A-O
var data = sheet.getRange(row, 1, 1, headers.length).getValues(); //get the values of newly added form data + formulated values
var values = {}; // create an object
for( var i = 0; i < headers.length; i++ ){
values[headers[i]] = data[0][i]; //add elements to values object and use headers as key
}
Logger.log(values);
const pdfFile = Create_PDF(values);
sendEmail(e.namedValues['Your Email'][0],pdfFile);
}
function sendEmail(email,pdfFile,){
GmailApp.sendEmail(email, "Subject", "Message", {
attachments: [pdfFile],
name: "From Someone"
});
}
function Create_PDF(values,) {
const PDF_folder = DriveApp.getFolderById("1t_BYHO8CqmKxVIucap_LlE0MhslpT7BO");
const TEMP_FOLDER = DriveApp.getFolderById("1TNeI1HaSwsloOI4KnIfybbWR4u753vVd");
const PDF_Template = DriveApp.getFileById('1Ye7DyJQOjA3J_EUOQteWcuASBCfqlA-_lzyNw0REjY8');
const newTempFile = PDF_Template.makeCopy(TEMP_FOLDER);
const OpenDoc = DocumentApp.openById(newTempFile.getId());
const body = OpenDoc.getBody();
for (const key in values) {
body.replaceText("{{"+key+"}}", values[key]);
}
OpenDoc.saveAndClose();
const BLOBPDF = newTempFile.getAs(MimeType.PDF);
const pdfFile = PDF_folder.createFile(BLOBPDF);
console.log("The file has been created ");
return pdfFile;
}

You get the error message with Google Sheets because you are using a Google Doc class to create the PDF, which is not compatible with Google Sheets.
DocumentApp can only be used with Google Docs. I will advise you to change
const OpenDoc = DocumentApp.openById(newTempFile.getId());
for
const openDoc = SpreadsheetApp.openById(newTempFile.getId());
const newOpenDoc = openDoc.getSheetByName("Sheet1");
And depending on the Google Sheet where the "Body" of the information is located. Replace:
const body = OpenDoc.getBody();
for an equivalent like getRange() or any Range class that helps you target the information you need. For example:
// This example is assuming that the information is on the cel A1.
const body = newOpenDoc.getRange(1,1).getValue();
The template for the PDF should be something like this:

Related

How do I get the image ID of an image IN THE CELL?

I'm currently working on a code in Google Apps Script that allows a user to fill out a spreadsheet and have the spreadsheet generate printouts for a job board. I'm trying to design this in a way where the user can simply insert a logo image into a row of my Google sheet and have it replace a placeholder in my doc template.
I have found lots of answers about how you can take an image and convert it to a blob and insert it from a url or an ID, however, I can't seem to find a way to get the ID or url from the image in the cell.
Here's my code currently:
//Creates menu option on spreadsheet
function onOpen() {
const ui = SpreadsheetApp.getUi();
const menu = ui.createMenu('AutoFill Docs');
menu.addItem('Create New Docs', 'createNewGoogleDocs');
menu.addToUi();
}
//Defines where to get template and info from
function createNewGoogleDocs() {
const googleDocTemplate = DriveApp.getFileById('14MJNd37pn6D-EmNKCQzXXvxJCcOAoB3KS-TlDgZuWMI');
const destinationFolder = DriveApp.getFolderById('120Sb_CJJlmz5NzJW8W3DB4TNuC4kdD3e');
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('JobBoard');
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row, index) {
if (index === 0) return;
if (row[9]) return;
const copy = googleDocTemplate.makeCopy(`${row[1]}, ${row[0]} Printout`, destinationFolder);
const doc = DocumentApp.openById(copy.getId())
const body = doc.getBody();
const friendlyDate = new Date(row[2]).toLocaleDateString();
//Replacing text
body.replaceText('{{Company}}', row[1]);
body.replaceText('{{jobTitle}}', row[0]);
body.replaceText('{{datePosted}}', friendlyDate);
body.replaceText('{{Description}}', row[3]);
body.replaceText('{{Qualifications}}', row[5]);
body.replaceText('{{Wage}}', row[4]);
body.replaceText('{{Apply}}', row[6]);
//A subfunction to handle replacing the image
function textToImage() {
var replaceTextToImage = function(body, searchText, image, width) {
var next = body.findText(searchText);
if (!next) return;
var r = next.getElement();
r.asText().setText("");
var img = r.getParent().asParagraph().insertInlineImage(0, image);
if (width && typeof width == 100) {
var w = img.getWidth();
var h = img.getHeight();
img.setWidth(width);
img.setHeight(width * h / w);
}
return next;
};
var documentId = doc;
var replaceText = "{{Upload Image}}";
var imageFileId = "### File ID of image ###"; //I don't know how to get this variable
var body = DocumentApp.openById(documentId).getBody();
var image = DriveApp.getFileById(imageFileId).getBlob();
do {
var next = replaceTextToImage(body, replaceText, image, 200);
} while (next);
}
//Close and saves new doc
doc.saveAndClose();
const url = doc.getUrl();
sheet.getRange(index + 1, 10).setValue(url)
})
}
I think what might be messing me up is that I have to loop through all my cells right now so that I can create multiple documents at once (meaning each row will have a different doc and different image ID). I'm just not sure how to work around that.
Here's the template and spreadsheet
https://docs.google.com/spreadsheets/d/1cySHogAxcUgzr0hsJoTyPZakKQkM6uIOtmyPzcMoJUM/edit?usp=sharing
https://docs.google.com/document/d/14MJNd37pn6D-EmNKCQzXXvxJCcOAoB3KS-TlDgZuWMI/edit?usp=sharing
There is a bit of an issue trying to get an image in a specific cell. There's even a Feature Request for that. This year Google released a few classes for image management but there seems to be issues when retrieving those using cellImage class.
I found a related answer (workaround) from user #Tanaike where images are retrieved from Google Sheets, converted to a Blob and inserted into a Google Doc.
Sample code provided was:
const spreadsheetId = "###"; // Google Spreadsheet ID
const res = DocsServiceApp.openBySpreadsheetId(spreadsheetId).getSheetByName("Sheet1").getImages();
console.log(res); // You can check the retrieved images at the log.
if (res.length == 0) return;
const blob = res[0].image.blob; // Here, 1st image of Sheet1 is retrieved. Of course, you can choose the image on the sheet.
let doc = DocumentApp.create("newDocName Goes_Here");
var body = doc.getBody();
var imgPDF = body.appendImage(blob);
Take into consideration that to make the above work you need to:
Install Google Apps Script library. (instructions here)
Enable Drive API.
I tested this and indeed, got the images from the given sheet and inserted them into the Google Doc specified. For some reason, running your code did not show me a newly created file from the template but you can tweak the above accordingly to your case.

Google Scripts not skipping rows

Pretty new to using google scripts, so forgive me if I'm doing something silly.
I am trying to write a program to take a large spreadsheet of data and fill an invoice template I have created. I have successfully done this in the past, but now when I tried to apply the app to a new spreadsheet of data, it is no longer functioning properly.
The problem is because the sheet is so large, the scripts times out, then when I go to restart the script, it starts again all the way at the beginning instead of starting where it left off. Originally the function was set to write the URL of the new invoice in the last column of spreadsheet, and the function would skip any rows with entries in that column. This was not happening, so to simplify I added another row that once the invoice is created, the word "DONE" is entered, and then I tried to set it up to skip any row with "DONE" in that column, this still is not working.
Any ideas on how I can get this to work?
function onOpen() {
const ui = SpreadsheetApp.getUi();
const menu = ui.createMenu('AutoFill Docs');
menu.addItem('Create New Docs', 'createNewGoogleDocs')
menu.addToUi();
}
function createNewGoogleDocs() {
//This value should be the id of your document template that we created in the last step
const googleDocTemplate = DriveApp.getFileById('1yXfcXTESCHqKsfMcgkhYJ9MdVwYoLYPhH1MRv4RyPk0');
//This value should be the id of the folder where you want your completed documents stored
const destinationFolder = DriveApp.getFolderById('1TXEumNJXfgFzPtKLBOAKJBXG-yNnjseQ')
//Here we store the sheet as a variable
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName('Export Worksheet')
//Now we get all of the values as a 2D array
const rows = sheet.getDataRange().getValues();
//Start processing each spreadsheet row
rows.forEach(function(row, index){
//Here we check if this row is the headers, if so we skip it
if (index === 0) return;
//Here we check if a document has already been generated by looking at 'Document Link', if so we skip it
if (row[31]=== 'DONE') return;
//Using the row data in a template literal, we make a copy of our template document in our destinationFolder
const copy = googleDocTemplate.makeCopy(`${row[4]}, ${row[0]} Invoice` , destinationFolder)
//Once we have the copy, we then open it using the DocumentApp
const doc = DocumentApp.openById(copy.getId())
//All of the content lives in the body, so we get that for editing
const body = doc.getBody();
//In this line we do some friendly date formatting, that may or may not work for you locale
const friendlyDate = new Date(row[3]).toLocaleDateString();
//In these lines, we replace our replacement tokens with values from our spreadsheet row
body.replaceText('{{Full Address}}', row[4]);
body.replaceText('{{unit}}', row[5]);
body.replaceText('{{Total}}', row[15]);
body.replaceText('{{Account Num}}', row[2]);
body.replaceText('{{Owner 1}}', row[6]);
body.replaceText('{{Owner 2}}', row[7]);
body.replaceText('{{CO Name}}', row[17]);
body.replaceText('{{St Address}}', row[20]);
body.replaceText('{{Address 1}}', row[18]);
body.replaceText('{{City}}', row[21]);
body.replaceText('{{State}}', row[22]);
body.replaceText('{{CO Zip}}', row[23]);
body.replaceText('{{invoice #}}', row[0]);
//We make our changes permanent by saving and closing the document
doc.saveAndClose();
//Store the url of our new document in a variable
const url = doc.getUrl();
//Write that value back to the 'Document Link' column in the spreadsheet.
sheet.getRange(index + 1, 30).setValue(url)
//Write that value back to the 'Document Link' column in the spreadsheet.
sheet.getRange(index + 1, 31).setValue("DONE")
})
}
Try this:
You were checking for DONE in the wrong location
function createNewGoogleDocs() {
const googleDocTemplate = DriveApp.getFileById('1yXfc...');
const destinationFolder = DriveApp.getFolderById('1TXE...');
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Export Worksheet');
const vs = sh.getRange(2, 1, sh.getLastRow() - 1, sh.getLastColumn()).getValues();
vs.forEach((r, i) => {
if (r[30] != 'DONE') {
const copy = googleDocTemplate.makeCopy(`${r[4]}, ${r[0]} Invoice`, destinationFolder);
const doc = DocumentApp.openById(copy.getId());
const body = doc.getBody();
body.replaceText('{{Full Address}}', r[4]);
body.replaceText('{{unit}}', r[5]);
body.replaceText('{{Total}}', r[15]);
body.replaceText('{{Account Num}}', r[2]);
body.replaceText('{{Owner 1}}', r[6]);
body.replaceText('{{Owner 2}}', r[7]);
body.replaceText('{{CO Name}}', r[17]);
body.replaceText('{{St Address}}', r[20]);
body.replaceText('{{Address 1}}', r[18]);
body.replaceText('{{City}}', r[21]);
body.replaceText('{{State}}', r[22]);
body.replaceText('{{CO Zip}}', r[23]);
body.replaceText('{{invoice #}}', r[0]);
doc.saveAndClose();
const url = doc.getUrl();
sh.getRange(i + 2, 30).setValue(url);
sh.getRange(i + 2, 31).setValue("DONE");
}
});
}

Google Apps Script: Use getFolderById for multiple variables depending on value

I think I have a very simple issue with Google Apps Script, but I already tried to google the solution for 1.5hrs without success. I guess I search for the wrong terms.
Here my code:
function folderLocations(){
var folder = {
Michael: '1bz9wIBRcRN2V-xxxxxxxxxx',
Chris: '1AEKHiI8iZKjHs-xxxxxxxxxx',
Steve: '1TD8iwjcbR7K5dN-xxxxxxxxxx',
};
return folder;
}
function createNewGoogleDocs() {
//ID of Google Docs Template, what sheet to use + save all values as 2D array
const googleDocTemplate = DriveApp.getFileById('xxxxxxxxxx_XznDn-i0WVtIM');
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName('Current Overview');
const rows = sheet.getDataRange().getValues();
//Start processing each spreadsheet row
rows.forEach(function(row, index){
//Destination folder ID (can differ from each person)
const destinationFolder = DriveApp.getFolderById(folderLocations().Chris);
// Set custom file name and create file
const copy = googleDocTemplate.makeCopy(`${row[15]} - ${row[3]} Quarterly Review` , destinationFolder);
const doc = DocumentApp.openById(copy.getId());
const body = doc.getBody();
// Replace placeholders with real values
body.replaceText('%NAME%', row[3]);
body.replaceText('%QUARTER%', row[15]);
body.replaceText('%ANSWER_1%', row[16]);
body.replaceText('%ANSWER_2%', row[17]);
[...]
doc.saveAndClose();
})
}
All working fine! BUT: What I want is to "dynamically" change the folder, depending on the value of a cell. It's not always "Chris"...:
const destinationFolder = DriveApp.getFolderById(folderLocations().Chris);
E.g.: If row[4] == Michael, then use the folder ID of "Michael". Somehow I can't get it to work to be "dynamically". 😔
I already tried all this, none working:
const destinationFolder = DriveApp.getFolderById(folderLocations().row[4]);
const destinationFolder = DriveApp.getFolderById(folderLocations(row[4]));
const destinationFolder = DriveApp.getFolderById(folderLocations().`${row[4]}`);
const destinationFolder = DriveApp.getFolderById(folderLocations().toString(row[4]));
etc.
👆🏻 I know what I try to do here is embarrassing. But I am normally not a developer and nobody at my company is familiar with Google Apps Script. That's the last bit I am missing, rest I put together myself using Google.
Thank you SOO much! 🙏🏻
You don't even need a function. Just an object is enough:
const folderLocations = {
Michael: '1bz9wIBRcRN2V-xxxxxxxxxx',
Chris: '1AEKHiI8iZKjHs-xxxxxxxxxx',
Steve: '1TD8iwjcbR7K5dN-xxxxxxxxxx',
};
var id = folderLocations['Chris'];
console.log(id); // 1AEKHiI8iZKjHs-xxxxxxxxxx
const destinationFolder = DriveApp.getFolderById(folderLocations[row[4]]);
This did the trick :
function folderLocations(person){
var folder = {
Michael: '1bz9wIBRcRN2V-xxxxxxxxxx',
Chris: '1AEKHiI8iZKjHs-xxxxxxxxxx',
Steve: '1TD8iwjcbR7K5dN-xxxxxxxxxx',
};
return folder[person];
}
...further below:
const destinationFolder = DriveApp.getFolderById(folderLocations(row[4]));

Checking if a filename exists and updating the file it in Google Script

I currently have a script which merges data from a Google Sheet into a Google Doc template. For each row of the worksheet, a new document is created using the title data from the row. The script works fine, but isn't my work. It has been passed onto me and I'm not skilled enough at Google Script to figure out what I'd like to achieve.
Ideally I wanted to know if it was possible to check when the script is run whether the document file already exists. It would do this as each document that is created uses the title data from the worksheet. If the document does exist then the data could be updated in that sheet, rather than creating a new version of it.
The script is the following
function mergeDocSheet() {
const TEMPLATE_ID = '16YfyeDjGDp-88McAtLCQQyZ1xz4QX5z';// Google Doc template ID
const SS_ID = '1C5gtJCSzHMuSz-oVWEItl2EUVRDwF5iH_'; // Google Sheet ID
const SHEET_NAME = "data"; // Google Sheet Tab name
const MAPPED = mappedDocToSheet;
const FILE_NAME = ["Titre de la formation"] // Header IDs from sheet.
docMerge(TEMPLATE_ID,SS_ID,SHEET_NAME,MAPPED, FILE_NAME);
}
function docMerge(templateID,ssID, sheetName, mapped, fileNameData, rowLen = "auto"){
//Get the Spreadsheet and sheet tab
const ss = SpreadsheetApp.openById(ssID);
const sheet = ss.getSheetByName(sheetName);
//Get number of rows to process
rowLen = (rowLen = "auto") ? getRowLen() - 1 : rowLen;
//Gets the range of data in the sheet then grabs the values of the range
const range = sheet.getRange(1,1,rowLen,sheet.getDataRange().getNumColumns());
const matrix = range.getValues();
// Searches the file mapped object and finds the corresponding number returns the column number in an array.
const fileNameRows = getFileNameRows()
//Loops through each row of the sheet grabbing the data from each row and putting it into a new doc.
for(let i = 1; i < rowLen; i++){
let row = matrix[i];
//Get the title for the file.
let fileName = buildFileName(row)
let newDoc = DriveApp.getFileById(templateID).makeCopy(fileName);
updateFileData(row, newDoc.getId());
};
function updateFileData(rowArray, doc){
//Loops through the mapped object.
mapped.forEach(function(element){
let textID = `\{\{${element.doc}\}\}`
DocumentApp.openById(doc).getBody().replaceText(textID,
rowArray[element.col]);
});
};
function buildFileName(rowArry){
let fileNameArray = fileNameRows.map(ele => rowArry[ele]);
return fileNameArray.join("_");
};
function getFileNameRows(){
//Map the column indexes from fileNameData
let fileNameLocs = fileNameData
.flatMap(name => {
return mapped.filter(element => element.sheet === name)
.map(ele => ele.col);
});
return fileNameLocs;
};
function getRowLen(){
return sheet.getDataRange().getNumRows();
};
};
Would it be possible to set up some kind of conditional, perhaps around these lines?
let newDoc = DriveApp.getFileById(templateID).makeCopy(fileName);
updateFileData(row, newDoc.getId());
I'm hoping someone can point me in the right direction with this. Any advice is much appreciated.
You can consider using searchFiles(params) to search for a specific filename with Doc type in your drive based on the search query term guidelines. Once you found all the files having the same filename, you can delete each file using setTrashed(trashed) before creating a new file using the template document
Sample Code:
//Loops through each row of the sheet grabbing the data from each row and putting it into a new doc.
for(let i = 1; i < rowLen; i++){
let row = matrix[i];
//Get the title for the file.
let fileName = buildFileName(row);
//This query parameter will search for an exact match of the filename with Doc file type
let params = "title='"+fileName+"' and mimeType = 'application/vnd.google-apps.document'"
let files = DriveApp.searchFiles(params);
while (files.hasNext()) {
//Filename exist
var file = files.next();
///Delete file
file.setTrashed(true);
}
//Create a new file
let newDoc = DriveApp.getFileById(templateID).makeCopy(fileName);
updateFileData(row, newDoc.getId());
};
In this given sample code, we will loop all files that have the exact filename and delete each file before creating a new one.
Additional References:
Google Drive Mimetypes

Creating PDFs from Spreadsheet Need PDF URL to be returned to Spreadsheet

I have a spreadsheet that contains data, a doc that is acting as my template and the start of a script that creates a PDF using the spreadsheet & doc for each line of the data. This works great!
My next step and this is where I am struggling, is I need to put the URL/identifier (from google drive) of each created PDF (each PDF goes to the same folder) into my spreadsheet so that I can then create another script to email the recipient their document.
I've seen this done when using a form and triggers but as I am not actively collecting the data, I cannot figure out this next part.
Thanks,
Nate
/*This function takes the data from the Charitable Tax Receipt summary and passes one row at a time
to the createPDF function.
*/
function createTaxRcptPDFs(){
const DOCTEMPLATE = DriveApp.getFileById("ref");
const TEMPFOLDER = DriveApp.getFolderById("ref1");
const PDFFOLDER = DriveApp.getFolderById("ref2");
const CURRENTSHEET = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TEST");
const DATA = CURRENTSHEET.getRange(5,1,3,20).getDisplayValues(); //change 3 to formula when testing done
// this code block creates a data array to populate each PDF per (row)
DATA.forEach(row => {
const ACTIVECELL = row[20];
createPDF(row[2],row[4],row[5],row[6],row[7],row[1],row[14],row[15],row[16],new Date(),row[1] + "_" + row[2],DOCTEMPLATE,TEMPFOLDER,PDFFOLDER);
});
}
/* This function creates the PDF based on a Doc template and saves it to a specific folder
for future use.
*/
function createPDF(fullName,street,city,state,postalCode,receiptNumber,donation,advantage,eligible,rcptDate,pdfName,DOCTEMPLATE,TEMPFOLDER,PDFFOLDER){
// constants/variables to use
const TEMPFILE = DOCTEMPLATE.makeCopy(TEMPFOLDER);
const TEMPDOCFILE = DocumentApp.openById(TEMPFILE.getId());
// array of values to create body from data
const BODY = TEMPDOCFILE.getBody();
BODY.replaceText("{fullName}", fullName);
BODY.replaceText("{street}", street);
BODY.replaceText("{city}", city);
BODY.replaceText("{state}", state);
BODY.replaceText("{postalCode}", postalCode);
BODY.replaceText("{receiptNumber}", receiptNumber);
BODY.replaceText("{donation}", donation);
BODY.replaceText("{advantage}", advantage);
BODY.replaceText("{eligible}", eligible);
BODY.replaceText("{rcptDate}", rcptDate);
TEMPDOCFILE.saveAndClose();
// create pdf and delete temp file
const PDFBLOB = TEMPFILE.getAs(MimeType.PDF);
PDFFOLDER.createFile(PDFBLOB).setName(pdfName);
TEMPFOLDER.removeFile(TEMPFILE);
}
Solution:
setName(pdfName) returns a File object for chaining, so you can use getDownloadUrl() right after.
var fileUrl = PDFFOLDER.createFile(PDFBLOB).setName(pdfName).getDownloadUrl();
Reference:
getDownloadUrl()