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 # | Item | Qty | Unit price | Amount |
|---|---|---|---|---|
| PO-1001 | Copy paper, case | 10 | $42.00 | $420.00 |
| PO-1001 | Toner cartridge | 2 | $89.50 | $179.00 |
| PO-1002 | Safety gloves, box | 6 | $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
- Changing an approved PO. Don't edit the PDF. Fix the row or its lines, delete the two links and the old files, and run the menu again. If the vendor already has the first version, issue it as a revision (for example PO-1001-R1) so both sides can tell them apart.
- Requests from a form. If staff request purchases through a Google Form, the responses already land in a sheet. Copy approved requests into the POs tab, or add Status and link columns next to the form columns; see generating a PDF from a Form response for the timing details.
- Sending. This script saves files and links; it doesn't email anyone. Attach the PDF from Drive when you write to the vendor, then set Status to Sent.
- Chasing deliveries. A Needed by date is a reminder waiting to happen. See automatic email reminders from a date column to get an email a few days before an order is due and skip rows marked Received.
- Matching the vendor's invoice. When the invoice arrives, compare it to the PO PDF and the Received quantities before paying. A Received qty column on the Lines tab makes partial deliveries easy to spot.
- Execution time. Apps Script runs stop after six minutes. A long backlog may need more than one run; the empty-link check lets it pick up where it left off.
See SheetWeave
Related Google Sheets guides
- How to generate a PDF or Google Doc for each row in Google Sheets
- Best Autocrat alternatives for mail merge to PDF and Docs
- Make invoices or certificates from Google Sheets rows
- Create sales quotes or estimates from Google Sheets
- Generate receipts from Google Sheets rows
- Make packing slips from Google Sheets orders
- Generate a PDF certificate from a Google Form response
- Issue numbered training completion certificates with expiry dates
- Make a PDF progress report for each student
- Generate offer letters from a Google Sheets hiring tracker
- Email each row’s PDF as an attachment
- Automatic email reminders from a date column
- Contract renewal and expiration alerts from Google Sheets
- Track certificate of insurance (COI) expirations in Google Sheets
- Track employee license and certification expirations
- Track equipment and vehicle maintenance with reminders
- Track warranty expiration dates with reminders
- Birthday and work anniversary reminders from Google Sheets
- Rent due and lease renewal reminders from Google Sheets
- Send invoice payment reminders from Google Sheets
- Highlight overdue tasks and send email reminders
- Follow-up reminders for leads, clients and candidates
- Membership renewal reminders for clubs and associations
- SheetWeave vs Autocrat
- SheetWeave vs Document Studio
- SheetWeave vs RowDocs
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.