0

I am creating a Google Script that fills a Google Doc template based on the fields of a Google Forms and sends the generated PDF via email to the user.

All of the steps that I followed are explained in detail here: Hacking it: Generate PDFs from Google Forms

The script (obtained from the article) is:

function onSubmit(e) {
  const rg = e.range;
  const sh = rg.getSheet();
  
  //Get all the form submitted data
  //Note: This data is dependent on the headers. If headers, are changed update these as well.
  const cName = e.namedValues['Client Name'][0];
  const cEmail = e.namedValues['Client Email'][0];
  const cAddress = e.namedValues['Client Address'][0];
  const cMobile = e.namedValues['Client Mobile'][0];
  const sendCopy = e.namedValues['Send client a copy?'][0];
  const paymentType = e.namedValues['What is your agreed upon payment schedule?'][0];
  const fixedCost = e.namedValues['What was your agreed upon cost for the project?'][0];
  const hourlyRate = e.namedValues['Hourly Rate'][0];
  const manHours = e.namedValues['Total man hours'][0];
  const services = e.namedValues['Select the services'][0];
  
  //Consequential Data
  const tax = 18.5
  var subtotal = 0;
  var taxAmt = 0;
  var payableAmt = 0;
      
  //if the user has selected hourly payment model
  //Note: Be careful that the responses match the elements on the actual form
  switch (paymentType ){
    case 'Hourly Rate':
      subtotal = hourlyRate*manHours;
      taxAmt = subtotal * (tax/100);
      payableAmt = +subtotal + +taxAmt;
      break;
    case 'Fixed Cost':
      subtotal = fixedCost;
      taxAmt = fixedCost * (tax/100)
      payableAmt = +fixedCost + +taxAmt;
      break;            
  }
  
  const invoiceID = 'IN' + Math.random().toString().substr(2, 9);
  var formattedDate = Utilities.formatDate(new Date(), "IST", "dd-MMM-yyyy");
  
  //Set the consequential data in the columns of the spreadsheet for record keeping
  //Note: These variable are dependent on the sheet's columns so if that changes, please update.
  const row = rg.getRow();
  
  const payableAmtCol = 2; //B
  const invoiceIDCol = 3; //C
  
  sh.getRange(row,payableAmtCol).setValue(payableAmt);
  sh.getRange(row,invoiceIDCol).setValue(invoiceID); 
  
  
  //Build a new invoice from the file
  //Folder and file IDs
  const invoiceFolderID = '<invoice-folder-id>';
  const invoiceFolder = DriveApp.getFolderById(invoiceFolderID);
  
  const templateFileID = '<template-id>';
  const newFilename = 'Invoice_' + invoiceID;
  
  //Make a copy of the template file
  const newInvoiceFileID = DriveApp.getFileById(templateFileID).makeCopy(newFilename, invoiceFolder).getId();;
  
  //Get the invoice body into a variable
  var document = DocumentApp.openById(newInvoiceFileID);
  var body = document.getBody();
  
  //Replace all the {{ }} text in the invoice body
  body.replaceText('{{Invoice num}}', invoiceID);
  body.replaceText('{{Date}}', formattedDate);
  body.replaceText('{{Client Name}}', cName);
  body.replaceText('{{Client Address}}', cAddress);
  body.replaceText('{{Client Mobile}}', cMobile);
  body.replaceText('{{Client Email}}', cEmail);
  body.replaceText('{{Services}}', services.split(', ').join('\n'));
  
  body.replaceText('{{Subtotal}}', subtotal);
  body.replaceText('{{Tax Value}}', taxAmt);
  body.replaceText('{{Total}}', payableAmt);
  
  //In the case of hourly rate payment type, let's add an additional message giving the rate and the man hours.
  if(paymentType.includes('Hourly Rate')){
     //It should look something like this on the invoice
     //Hourly Rate
     //Rate of Rs.1200/hour
     //Completed 50 man hours
     const message = paymentType + '\nRate of Rs.' + hourlyRate + '/hour\nCompleted ' + manHours + ' man hours';
     body.replaceText('{{Payment Type}}', message);
  } else {
    body.replaceText('{{Payment Type}}', paymentType);
  }
  
  document.saveAndClose();
  
    //send email with the file
  var attachment = DriveApp.getFileById(newInvoiceFileID);
    GmailApp.sendEmail(cEmail, '<subject>, 
                     '<body>', 
                     {attachments: [attachment.getAs(MimeType.PDF)]});
}

The code works fine. Now I need that the user can edit its response after he press "Send Form" on Google Forms. So I decided to check "Respondents can edit after submit". Then I need to send the document again via GmailApp with the edited fields. So I created a new trigger: Edit (from a Spreadsheet). The other trigger is Form submit.

However I have a problem. When the user edits a field and press, again, "Send Form", the trigger "Edit" is activated with the following error: Failed to send email: no recipient.

If I go to the Spreadsheet responses I can see the edited row (because the cell has a comment "The person who responded has updated this value"), and the column mail is not edited but it stills throwing the exception.

How can we solve this problem if cEmail was never edited?

Searchs

I could find some interesting answers:

They seem to describe that a blank row can be generated when the trigger "Edit" is activated. However I don't see why this could happen, and how I can solve it since the Spreadsheet Responses is automatically edited after a new user submit an answer.

manooooh
  • 113
  • 2
  • 7

1 Answers1

2

When a form response is edited the on form submit event object properties values and namedValues only include values for those questions that were edited.

To fix the error Failed to send email: no recipient, replace

 GmailApp.sendEmail(cEmail, '<subject>, 
                     '<body>', 
                     {attachments: [attachment.getAs(MimeType.PDF)]});

by

const recipientIdx = 1; // This is the 0 based index of the column having the recipient email address
const recipient = cEmail ? cEmail : e.range.getValues().flat()[recipientIdx]
 GmailApp.sendEmail(recipient , '<subject>', 
                     '<body>', 
                     {attachments: [attachment.getAs(MimeType.PDF)]});

P.S. Instead of hardcoding the value assigned to recipientIdx you might use some code to get it based on the column headers.

NOTE: The above only will prevent the error mentioned in the question. In order to make the script work you will have to apply the same idea for all the fields: Read the missing values from the spreadsheet by using the e.range.getValues().flat().

Related

Rubén
  • 34,714
  • 9
  • 70
  • 166
  • Oh I see. That makes sense. What would you change to the script so it works if the user error's is not in the email field? For example the Mobile field. – manooooh Apr 05 '21 at 01:19
  • Thanks! At least it does not throw the exception. But now some fields are not "considered" after the user edits the Form so they are not included in the final PDF. In the Spreadsheet, column 1 is "Time stamp", column 2 is "Email address", column 3 is "Surname" and column 4 is "First name". I get the first name but the Surname is empty. Perhaps we need to "use some code to get the `recipientIdx` based on the column headers"? How could we do that? – manooooh Apr 05 '21 at 01:33
  • Sorry I didn't meant "some fields" but **all** the fields that are not changed, now are blank. So I get a PDF only with the edited fields, not all the fields. The edited-by-user row in Spreadsheet is fine, the problem is that I get blank fields. – manooooh Apr 05 '21 at 01:37
  • You have to apply the same idea to all the "fields" – Rubén Apr 05 '21 at 01:49
  • What do you mean? The Email field is just one. – manooooh Apr 05 '21 at 03:01
  • So should I use `const cAddress = e.namedValues['Client Address'][0] ? e.namedValues['Client Address'][0] : e.range.getValues().flat()[2];` or what do I put on `.flat()[....]`? – manooooh Apr 05 '21 at 03:20
  • 1
    Something like that. In order to make your script efficient, use something like `const values = e.range.getValues().flat()` then `const cAddress = e.namedValues['Client Address'][0] ? e.namedValues['Client Address'][0] : values[2];` assuming that the Cliente Address value is in the column C. – Rubén Apr 05 '21 at 03:23
  • I understand. What about not hardcoding the column headers? Is that possible and how? Thanks again. – manooooh Apr 05 '21 at 04:05
  • Yes, that's possible. For details please post a new question. – Rubén Apr 05 '21 at 04:06
  • 1
    Thanks again. I have a problem related to saving a variable (day, month and year of a Date field). You can see these problem and the new question here: https://stackoverflow.com/q/66948874/11617040 – manooooh Apr 05 '21 at 06:05