Turn a form request into a PDF quote, emailed automatically
A customer fills in a form. A minute later a laid-out PDF quote with their name and their numbers on it arrives in their inbox. You never open a document and you never type a total.
What it does
A customer fills in a form on your website or a link you send them. A minute later a properly laid out PDF quote, with their name and their numbers on it, arrives in their inbox. You never open a document and you never type a total.
Everything here is free with any Google account. There is nothing to install and nothing to buy.
Before you start
- You need a Google account, and it is free. Everything here is Google Forms, Sheets, Docs, Drive and Gmail, and all of them come with any Google account. If you can open Gmail you already have one.
- You need a computer for this, not a phone. The script editor does not work properly on a phone browser. Once it is set up it runs on Google's servers, so your computer can be closed.
- About thirty minutes. There are four things to make and then one to switch on.
- Decide before you start what your quote should say. It is much easier to write the wording once, in the template, than to change it later.
The build, step by step
Make the form the customer fills in.
Go to forms.new. Add five questions, in this order: Name, Email, Service, Hours, Rate. The order matters, because the script reads the answers by position.You should see: five questions listed down the form, in that order.Send the answers to a spreadsheet.
At the top of the form click Responses, then the green spreadsheet icon, and choose to create a new spreadsheet.You should see: a new Google Sheet open, with a header row reading Timestamp, Name, Email, Service, Hours, Rate.If the columns are in a different order to that, the script will put the wrong answer in the wrong place. Go back and reorder the questions on the form until the spreadsheet header matches.Make the quote template.
Go to docs.new and write your quote exactly as you want it to look — your business name, your terms, the layout. Where a customer's detail should appear, type one of these instead of a real value:{{name}},{{service}},{{hours}},{{rate}},{{total}}.Those are placeholders. They are ordinary text that you type yourself; the script finds each one and swaps it for the real answer. The double curly brackets are only there so nothing else in your document looks like one by accident.
You should see: a finished-looking quote with those five pieces of text sitting where the customer's details will go.Copy the template's ID out of the address bar.
With the template open, look at the web address at the top of the browser. It reads likedocs.google.com/document/d/then a long string of letters and numbers, then/edit. Select that long string in the middle and copy it. That is the document's ID — the script needs it to find your template.Do not copy the whole address. Only the section between/d/and/edit.Open the script editor from the spreadsheet.
Go back to the spreadsheet from step 2. In its menu bar clickExtensions, thenApps Script.You should see: a new tab headed Apps Script, with a little code in it beginningfunction myFunction().It must be opened from the spreadsheet, not the form and not the template. The script has to live where the answers arrive, or the trigger in step 8 will not be offered to you.Delete everything in the editor and paste this in.
function makeQuote(e) { var TEMPLATE_ID = 'PASTE_YOUR_TEMPLATE_ID_HERE'; var name = e.values[1]; var email = e.values[2]; var service = e.values[3]; var hours = readNumber(e.values[4]); var rate = readNumber(e.values[5]); // If either number cannot be read, tell YOURSELF and send them nothing. // A quote reading NaN is worse than no quote at all. if (hours === null || rate === null) { GmailApp.sendEmail(Session.getEffectiveUser().getEmail(), 'Quote NOT sent - check the numbers', 'A request from ' + name + ' (' + email + ') could not be turned into a quote.' + '\n\nHours field: ' + e.values[4] + '\nRate field: ' + e.values[5] + '\n\nNothing was sent to them. Reply yourself, or ask them again.'); return; } var total = hours * rate; var copy = DriveApp.getFileById(TEMPLATE_ID).makeCopy('Quote for ' + name); var doc = DocumentApp.openById(copy.getId()); var body = doc.getBody(); body.replaceText('{{name}}', name); body.replaceText('{{service}}', service); body.replaceText('{{hours}}', String(hours)); body.replaceText('{{rate}}', String(rate)); body.replaceText('{{total}}', String(total)); doc.saveAndClose(); var pdf = DriveApp.getFileById(copy.getId()).getAs('application/pdf'); GmailApp.sendEmail(email, 'Your quote', 'Your quote is attached. Thank you.', { attachments: [pdf] }); } // People do not type bare numbers. They type $50, 10 hours, 12,50 and 50/hr. // This reads the number out of whatever they wrote, and returns null when it // genuinely cannot tell - "3 to 5" is refused rather than guessed at, because a // quote for 35 that looks correct is worse than one that never sends. // // A COMMA IS A DECIMAL POINT IN MOST OF EUROPE. Stripping commas turns 12,50 // into 1250 - a hundred times too much, printed confidently, with nothing to // warn you. That is worse than the NaN this replaced: NaN gets reported back to // you, a wrong price gets paid or lost. So commas are read, not deleted. // // WHAT CANNOT BE DECIDED IS REFUSED, NOT GUESSED. A grouping separator is // always followed by exactly three digits, so "12,50" can only be a decimal and // is read as 12.50. But "1,200" is either 1200 or 1.200 depending on the // country, and the string does not say which - so it goes to you, like "3 to 5" // does. Guessing a country would be right for half your customers, silently. function readNumber(v) { var s = String(v === null || v === undefined ? '' : v).trim(); if (s === '') return null; if (s.indexOf('-') !== -1) return null; // negatives and "50-60": refuse var m = s.match(/^[^0-9]*([0-9][0-9.,]*)\s*[a-zA-Z\/£$€%. ]*$/); if (!m) return null; var t = m[1]; if (/[.,]$/.test(t)) return null; var dots = (t.match(/\./g) || []).length; var commas = (t.match(/,/g) || []).length; var dec = null; // which character is the decimal point if (dots > 0 && commas > 0) { dec = t.lastIndexOf('.') > t.lastIndexOf(',') ? '.' : ','; } else if (dots + commas === 1) { var sep = dots ? '.' : ','; var i = t.indexOf(sep); // exactly three digits after it, and a valid group before it: ambiguous if (t.length - i - 1 === 3 && i <= 3 && t.charAt(0) !== '0') return null; dec = sep; } var grp = dec === null ? (commas ? ',' : '.') : (dec === '.' ? ',' : '.'); var whole = dec === null ? t : t.slice(0, t.lastIndexOf(dec)); var frac = dec === null ? '' : t.slice(t.lastIndexOf(dec) + 1); if (whole.indexOf(grp) !== -1) { var ok = grp === ',' ? /^[0-9]{1,3}(,[0-9]{3})+$/.test(whole) : /^[0-9]{1,3}(\.[0-9]{3})+$/.test(whole); if (!ok) return null; whole = whole.split(grp).join(''); } if (!/^[0-9]+$/.test(whole)) return null; if (frac !== '' && !/^[0-9]+$/.test(frac)) return null; var n = Number(whole + (frac ? '.' + frac : '')); return isNaN(n) ? null : n; }Paste your template ID into the first line and save.
Select the wordsPASTE_YOUR_TEMPLATE_ID_HERE, keeping the quote marks either side, and paste in the ID you copied earlier.Your clipboard almost certainly does not hold the ID any more. You copied the whole script two steps ago, which replaced it — a clipboard only holds one thing. Switch back to the template tab, copy the ID out of the address bar again, then come back and paste.Then click the save icon in the toolbar — it looks like a floppy disk.Keep the single quote marks. The ID has to sit inside them. If you delete them the script will not run.Switch on the trigger.
In the left-hand sidebar of the script editor click the clock icon. This is the Triggers page — a trigger is a rule that runs your script when something happens, without you being there. Click Add Trigger in the bottom right. Leave the function asmakeQuote. For the event source choose From spreadsheet, and for the event type choose On form submit. Click Save.A permissions screen appears at this point, and it looks alarming. It is not an error and you have not broken anything. Google is asking whether you allow your own script to make documents and send email as you. Click Review permissions, choose your account, and you will then see a warning that Google has not verified this app — that is because the app is you, written ten minutes ago. Click Advanced at the bottom left, then the link that goes to your project anyway, then Allow.You should see: your trigger listed on that page, namingmakeQuoteand On form submit.Get the link customers will use, and test it on yourself.
Go back to your form tab and click Send at the top right. In the box that opens, click the link icon (it looks like a chain) and click Copy. Keep that link — it is the thing you give customers. Open it in a new tab and you are looking at the form exactly as they will.The Send button does not send anything on its own. It is where Google keeps the link. Opening it emails nobody.Now fill the form in with your own email address, and use these on purpose rather than tidy numbers: put10 hoursin the hours field and$50in the rate. Submit it. Then do it a second time leaving both number fields blank.You should see, from the first submission: a PDF quote in your inbox within a minute or two, reading 10 hours, 50 and a total of 500. The word and the dollar sign are ignored and the sum is still right.
And from the second, blank one: an email to yourself headed Quote NOT sent, and nothing at all sent to the customer.Why those values and not "any numbers". An earlier version of this guide said to test with any numbers, so everyone tested with tidy ones and nobody found that$50produced a quote reading NaN — which the customer received and the business owner never saw, because you never open these documents. A test you cannot fail tells you nothing. Use the messy values.If nothing arrives, go back to the script editor and click Executions in the left sidebar. That page lists every time the script ran and whether it failed. A failure there names the line it stopped on, which is almost always the template ID in step 7.
What to watch for
- A placeholder that is spelled differently in the template will not be
replaced.
{{Name}}and{{name}}are not the same, and the quote will go out with the bracketed word still showing. Test on yourself first — that is what step 9 is for. - Every quote leaves a copy of the document in your Drive. That is useful as a record, but they build up. Make a folder for them when you get tired of seeing them.
- The total is hours multiplied by rate, and nothing else. No tax,
no discount, no minimum charge. If your quotes need those, work out what the sum
should be before you change the line that begins
var total. - The email is sent from your own Gmail address, so replies come straight back to you.
What it costs
Nothing. Google Forms, Docs, Sheets, Gmail and Apps Script are all free with an ordinary Google account. Free accounts can send around a hundred emails a day through a script, which is far more quotes than most small businesses issue.