How to Create Sales Quotes or Estimates from Google Sheets (2026)

A sales quote is just a row of numbers until a client needs a document they can approve. You can keep the pipeline in Google Sheets and still send a branded PDF estimate for each deal, without retyping names and totals into a fresh Doc every time.

This is the same “one document per row” idea as invoices, but the document is a quote: it has a validity date, often a numbered quote ID, and it should only be generated when a row is actually ready to send. For invoices and certificates specifically, see the invoices and certificates guide.

Watch the 2-minute walkthrough on YouTube. Short on time? Watch the 50-second version.

Step 1: a sheet that can become a quote

Use columns such as: Quote #, Client, Email, Item, Qty, Rate, Total, Valid until, Status, and empty Quote Doc / Quote PDF columns for links. Status values like Draft, Ready, Sent and Accepted keep unready rows out of a batch.

Start from a sample tracker (sample data only): Make a copy of the Sales Quote & Estimate Tracker.

Format currency and dates in the sheet, not only in the Doc. A display column such as =TEXT(G2,"$#,##0.00") for Total, and =TEXT(H2,"MMM d, yyyy") for Valid until, gives the template clean text. If you use those helper columns, point the placeholders at the helper headers (for example {{Total display}}).

Step 2: a Google Docs quote template

Create a Doc with your logo, a “Quote” or “Estimate” heading, bill-to lines, a simple items table, a total, payment or acceptance terms, and a validity sentence. Placeholders should match headers exactly, for example:

Or start from a matching Doc: Make a copy of the quote template.

This pattern handles one item (or one summary line) per row. If a quote has several line items, either keep one row per item and generate one page each, or put a pre-joined summary in a single cell (for example a list of items) and merge that cell into the Doc. Multi-row tables that grow with a variable number of lines need a more involved script.

Copy the template ID (between /d/ and /edit in the URL) and the ID of a Drive folder for finished files (after /folders/).

Step 3: generate PDFs only for Ready rows

In the sheet, open Extensions › Apps Script, paste the script, fill in the two IDs, save, reload the sheet, and use Merge › Create quotes for ready rows. Approve permissions the first time. Rows that already have a Quote PDF link are skipped, so you can safely run it again.

/**
 * Create a Google Doc + PDF quote for each row whose Status is "Ready"
 * and whose "Quote PDF" cell is still empty.
 * Template placeholders must match headers, e.g. {{Quote #}}, {{Client}}, {{Total}}.
 * Menu: after saving, reload the sheet and use Merge > Create quotes for ready rows.
 */
const TEMPLATE_ID = 'PASTE_TEMPLATE_DOC_ID';
const FOLDER_ID   = 'PASTE_OUTPUT_FOLDER_ID';

function onOpen() {
  SpreadsheetApp.getUi().createMenu('Merge')
    .addItem('Create quotes for ready rows', 'mergeReadyQuotes')
    .addToUi();
}

function mergeReadyQuotes() {
  const sh = SpreadsheetApp.getActiveSheet();
  const data = sh.getDataRange().getDisplayValues();
  const raw  = sh.getDataRange().getValues();
  const head = data[0];
  const statusCol = head.indexOf('Status');
  const pdfCol    = ensureCol_(sh, head, 'Quote PDF');
  const docCol    = ensureCol_(sh, head, 'Quote Doc');
  const folder    = DriveApp.getFolderById(FOLDER_ID);

  for (let r = 1; r < data.length; r++) {
    if (String(raw[r][statusCol] || '').toLowerCase() !== 'ready') continue;
    if (sh.getRange(r + 1, pdfCol).getValue()) continue;   // already generated
    const rec = {};
    head.forEach((h, i) => { if (h) rec[h] = data[r][i]; });
    const name = 'Quote-' + (rec['Quote #'] || rec['Client'] || ('row-' + (r + 1)));
    const copy = DriveApp.getFileById(TEMPLATE_ID).makeCopy(name, folder);
    const doc  = DocumentApp.openById(copy.getId());
    const body = doc.getBody();
    Object.keys(rec).forEach((h) => {
      const escaped = h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      body.replaceText('\\{\\{' + escaped + '\\}\\}', rec[h] || '');
    });
    doc.saveAndClose();
    const pdf = folder.createFile(copy.getAs(MimeType.PDF).setName(name + '.pdf'));
    sh.getRange(r + 1, docCol).setValue(copy.getUrl());
    sh.getRange(r + 1, pdfCol).setValue(pdf.getUrl());
  }
}

function ensureCol_(sh, head, name) {
  let i = head.indexOf(name);
  if (i === -1) {
    i = head.length;
    sh.getRange(1, i + 1).setValue(name);
    head.push(name);
  }
  return i + 1;
}

Tips

Want documents without maintaining a script? SheetWeave (disclosure: we make it) is a Google Sheets add-on that fills a Google Docs template from each row and saves a Doc, a PDF, or both in a Drive folder you choose, writing the links back to the row. It can run on selected rows, all rows, or only new ones. Sheet data stays inside your Google account and it uses narrow scopes. 14-day free trial, no credit card required.
See SheetWeave

Related Google Sheets guides

Instructions refer to Google Sheets, Google Docs, Google Forms, Google Drive and Apps Script as they worked on November 10, 2026. Not affiliated with Google or any other vendor named here. Google Sheets, Google Docs, Google Forms, Google Slides and Google Drive are trademarks of Google LLC. Features change, so verify in your own account. Questions or corrections: hello@loomwise.app.