How to Create Purchase Orders from Google Sheets (One PDF per PO, 2026)

Plenty of small teams track purchasing in a spreadsheet: a row for each order, a vendor, a total, and someone who approved it. The part that takes time is turning that row into a clean purchase order the vendor can file. This guide shows how to keep orders and their line items in Google Sheets and generate a numbered Google Doc and PDF for every approved PO, with the links written back to the sheet.

It uses the same “one document per row” method as the quotes and estimates guide. The difference is line items: a PO usually has several, so they live on their own tab and are dropped into the document as a table.

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

Step 1: two tabs, orders and line items

Create a tab named POs with one row per purchase order. Useful columns: PO #, PO date, Vendor, Vendor address, Vendor email, Ship to, Needed by, Payment terms, Requested by, Approved by, Total, Status (Draft, Approved, Sent, Received), and empty PO Doc / PO PDF columns for links.

Create a second tab named Lines with one row per item: PO #, Item, Qty, Unit price, Amount. In Amount, use =C2*D2 and fill down. Format Unit price and Amount as currency (Format › Number › Currency), because the script copies values exactly as the sheet displays them.

PO #ItemQtyUnit priceAmount
PO-1001Copy paper, case10$42.00$420.00
PO-1001Toner cartridge2$89.50$179.00
PO-1002Safety gloves, box6$14.25$85.50

Back on the POs tab, total each order from its lines: =SUMIF(Lines!A:A,A2,Lines!E:E) in Total (assuming PO # is column A on both tabs), formatted as currency.

PO numbers. Each PO needs a unique number that never changes. A formula such as ="PO-"&TEXT(1000+ROW()-1,"0") is fine for a draft, but it shifts if rows are sorted or deleted, and the Lines tab would then point at the wrong order. Once a PO is approved, paste its number as a value (copy, then Paste special › Values only), or just type numbers in sequence.

Approvals. Add data validation (Data › Data validation, dropdown) on Status so it can only be Draft, Approved, Sent or Received. If approvals belong to one person, protect the Status and Approved by columns (Data › Protect sheets and ranges) so only that person can edit them.

Step 2: a Google Docs PO template

Create a Doc with your logo, company name, billing address and a “Purchase Order” heading. Use placeholders that match your POs headers exactly, including spaces and capitals. Put {{Line items}} alone on its own line where the item table should go; the script replaces that line with a table.

PURCHASE ORDER {{PO #}}
Date: {{PO date}}          Needed by: {{Needed by}}

Vendor:                    Ship to:
{{Vendor}}                 {{Ship to}}
{{Vendor address}}

{{Line items}}

Total: {{Total}}
Payment terms: {{Payment terms}}
Approved by: {{Approved by}}

Please include the PO number on your invoice and packing slip.

Keep at least one line of text after {{Line items}} (like the Total line above), and keep it in the main body, not inside an existing table, header or footer. Copy the template ID (between /d/ and /edit in its URL) and the ID of a Drive folder for finished POs (after /folders/).

Step 3: create a PDF for each approved PO

In the sheet, open Extensions › Apps Script, paste the script below, fill in the two IDs, save, then reload the sheet. A Purchase orders menu appears; choose Create POs for approved rows and approve the permissions the first time. Rows that already have a PO PDF link are skipped, so running it again never makes duplicates.

/**
 * Purchase orders from Google Sheets.
 * Tab "POs":   one row per purchase order (PO #, Vendor, ..., Status, PO Doc, PO PDF).
 * Tab "Lines": one row per line item (PO #, Item, Qty, Unit price, Amount).
 * Creates a Google Doc + PDF for every PO whose Status is "Approved"
 * and whose "PO PDF" cell is still empty.
 */
const TEMPLATE_ID = 'PASTE_TEMPLATE_DOC_ID';
const FOLDER_ID   = 'PASTE_OUTPUT_FOLDER_ID';
const LINE_HEADERS = ['Item', 'Qty', 'Unit price', 'Amount'];

function onOpen() {
  SpreadsheetApp.getUi().createMenu('Purchase orders')
    .addItem('Create POs for approved rows', 'createPurchaseOrders')
    .addToUi();
}

function createPurchaseOrders() {
  const ss    = SpreadsheetApp.getActive();
  const sh    = ss.getSheetByName('POs');
  const data  = sh.getDataRange().getDisplayValues();
  const head  = data[0];
  const lines = ss.getSheetByName('Lines').getDataRange().getDisplayValues();
  const lHead = lines[0];
  const lPo   = lHead.indexOf('PO #');
  const lCols = LINE_HEADERS.map((h) => lHead.indexOf(h));
  const statusCol = head.indexOf('Status');
  const pdfCol    = ensureCol_(sh, head, 'PO PDF');
  const docCol    = ensureCol_(sh, head, 'PO Doc');
  const folder    = DriveApp.getFolderById(FOLDER_ID);

  for (let r = 1; r < data.length; r++) {
    if (String(data[r][statusCol] || '').trim().toLowerCase() !== 'approved') continue;
    if (sh.getRange(r + 1, pdfCol).getValue()) continue;          // already created
    const rec = {};
    head.forEach((h, i) => { if (h) rec[h] = data[r][i]; });
    const po = rec['PO #'];
    if (!po) continue;
    const items = lines.slice(1)
      .filter((l) => String(l[lPo]).trim() === String(po).trim())
      .map((l) => lCols.map((c) => (c === -1 ? '' : l[c])));

    const name = po + '-' + (rec['Vendor'] || 'vendor');
    const copy = DriveApp.getFileById(TEMPLATE_ID).makeCopy(name, folder);
    const doc  = DocumentApp.openById(copy.getId());
    const body = doc.getBody();
    insertLineTable_(body, [LINE_HEADERS].concat(items));
    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());
  }
}

// Replaces the paragraph that contains {{Line items}} with a table of the PO's lines.
function insertLineTable_(body, cells) {
  const found = body.findText('\\{\\{Line items\\}\\}');
  if (!found) return;
  const para  = found.getElement().getParent();
  const index = body.getChildIndex(para);
  const table = body.insertTable(index, cells);
  table.getRow(0).editAsText().setBold(true);
  para.removeFromParent();
}

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;
}

Each PO lands in your folder as a Doc and a PDF named like PO-1001-Acme Paper, with both links in the row. The item table uses the four columns in LINE_HEADERS; change that list (and the matching Lines headers) if you need SKU or unit of measure columns too. Style the table once it exists in a generated Doc, or format the template text around it.

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. It fills placeholders from the columns of a row, so for a multi-item PO put the items in that row (for example a few Item and Qty columns) or keep using the script above for the line-item table. 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 11, 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.