A Google Apps Script + HTML web app to capture Purchase Orders into Google Sheets with client/style autocomplete, SKU/Item lookups, duplicate protection, PO‑photo linkage via Google Forms, and lightweight logging.
-
Fast PO entry with multi‑style, multi‑product rows
-
Autocomplete
- Clients & Style IDs from
O2D_Styles - SKU & Item from
MasterItems(+ optional image preview)
- Clients & Style IDs from
-
Data integrity
- Duplicate guard on PO Number + Style ID + SKU + Material
- Requires PO photo uploaded via Google Form (checked before submit)
-
Dates
- PO Date stored as date‑only (
dd/MM/yyyy) - Target Date required, date‑only
- PO Date stored as date‑only (
-
Ops
- XLOOKUP auto‑fills photo URL in
Indent Details(col K) - Temp save / restore draft
- Execution & error logging (+ optional email summary)
- Time‑based triggers to maintain formulas / summaries
- XLOOKUP auto‑fills photo URL in
-
Google Apps Script (server):
Code.gs -
HTML/CSS/JS (client):
Index.html -
Google Sheets (data store):
Indent Details(main)Form_responses_1(Google Form responses for PO photo)O2D_Styles(Client ↔ Style lookup)MasterItems(SKU/Item/UOM lookup)Temp Form,Helper,Execution Log,Error Log,New_Entries
| Col | Field | Source |
|---|---|---|
| A | PO Date | dd/MM/yyyy |
| B | PO Number | Text, stored with leading ' |
| C | Style ID | From UI |
| D | Client | From UI |
| E | Supplier | From UI |
| F | SKU Code | From MasterItems / UI |
| G | Material Name | From MasterItems / UI |
| H | Quantity | Number |
| I | UOM | From MasterItems / UI |
| J | Target Date | dd/MM/yyyy |
| K | Photo URL | Set by extendXLOOKUP() |
| L | Timestamp | dd/MM/yyyy HH:mm:ss (submission time) |
Column K is maintained by a formula inserted by
extendXLOOKUP().
- B: PO Number
- C: Photo URL
The app checks this sheet to confirm a PO photo exists before accepting a submission.
- A: Client
- B: Style ID
- Must have headers including:
Client,SKU Code,Item,UOM,Image URL(optional)
Temp Form– draft storageHelper– usesA1as a progress marker forextendXLOOKUPExecution Log–[Timestamp, Level, Message]Error Log–[Timestamp, Level, Message](see “Notes” re: email summary)New_Entries– tracks new clients encountered during submit
-
User opens web app →
doGet()servesIndex.html. -
On load, client:
- Restores draft via
getLatestTempForm() - Loads master items via
getMasterItems()
- Restores draft via
-
Autocomplete
- Client → server:
getClients(query)→ returns up to 10 matches fromO2D_Styles!A - Client → server:
getStyleIDs(client, query)→ returns up to 10 matches fromO2D_Styles!Bfiltered by client - SKU/Item suggestions are client‑aware, with allow‑list override (
ALWAYS)
- Client → server:
-
Submit
-
Client builds payload
{ poNumber, poDate, client, supplier, targetDate, styles(JSON) } -
Server
submitIndent(form):- Validates required fields
- Blocks if no PO photo found in
Form_responses_1(isPhotoUploaded) - Parses & formats dates (PO Date as date‑only)
- For each product row: checks duplicates with
isDuplicatePOStyleSKUMat - Appends rows to
Indent Details - Logs execution
-
-
Post‑submit ops
extendXLOOKUP()(on open + hourly trigger) fills Photo URL (col K) via XLOOKUP fromForm_responses_1.
doGet()→ serves UI with title “PO Submission Form”
getClients(query)→ distinct client names fromO2D_Styles!AgetStyleIDs(client, query)→ style IDs fromO2D_Styles!(A:B)filtered by clientgetMasterItems()→ returns all rows fromMasterItemsas objects (keyed by header)
-
submitIndent(form)-
Validates fields (PO Number, Client, Supplier, PO Date, Target Date, at least one product row)
-
isPhotoUploaded(poNumber)ensures a photo exists inForm_responses_1(B: PO, C: URL) -
Duplicate guard:
isDuplicatePOStyleSKUMat(poNum, styleId, sku, material)-
Pure sheet scan (no cache) across
Indent Details:- PO → B, Style → C, SKU → F, Material → G
-
Normalizes with
norm(s)(strips leading', trims, uppercases)
-
-
Writes rows as per schema above
-
logExecution(msg)/logError(msg)
-
-
extendXLOOKUP()-
For each new row in
Indent Details, sets K to:XLOOKUP(PO, 'Form_responses_1'!B:B, 'Form_responses_1'!C:C, "No photo uploaded", 0, -1)- Tries both raw PO (with leading
') and stripped PO
-
Uses
Helper!A1to remember last processed row
-
saveTempForm(form)→ appends draft toTemp FormgetLatestTempForm()→ returns latest draft object
onOpen()→ callsextendXLOOKUP()createTriggers()→ hourlyextendXLOOKUP+ every‑3‑hourssendErrorSummarysendErrorSummary()→ emails a simple error digest and clearsError Logrows (A2:C)
norm(s)→ normalize string for comparisonsconvertAncientDate(input)→ helper for serial→date conversion (rarely used)logNewClient(client),logExecution(msg),logError(msg)
-
Layout: responsive container, header grid for PO meta, per‑style table with SKU/Item rows
-
Inputs (header):
poNumber(text)poDate(type="date"; saved asdd/MM/yyyy)client(autocomplete)supplier(text)targetDate(type="date"; required)
-
Per‑style table:
styleIdInput(autocomplete from server)skuInput&itemInput(suggest from MasterItems; optional image preview)quantity,uom
-
Actions:
- Add/Remove style & product rows
- Save draft (temp save)
- Submit PO
- Open Google Form (upload PO photo)
-
API calls (google.script.run):
getLatestTempForm()→ hydrate draftgetMasterItems()→ cache master items in browsergetClients(query)/getStyleIDs(client, query)saveTempForm(draft)submitIndent(payload)
Payload (submit):
{
"poNumber": "FTPL/0002",
"poDate": "2025-07-04",
"client": "Client A",
"supplier": "Supplier X",
"targetDate": "2025-07-18",
"styles": "[{ \"styleId\": \"STY-001\", \"products\": [{\"productCode\": \"SKU-123\", \"materialName\": \"Cotton\", \"quantity\": \"100\", \"uom\": \"M\"}]}]"
}-
Sheet prep: Ensure all sheets above exist and headers match.
-
Form link: Confirm your Google Form writes to
Form_responses_1with B=PO Number, C=Photo URL, and update the Upload Photo button link inIndex.htmlif needed. -
Apps Script
- Set script time zone (File → Project properties) to IST (or desired).
- Run
onOpen()once to initialize formulas. - Run
createTriggers()to install time‑based triggers.
-
Publish Web App
- Deploy → New deployment → type Web app
- Execute as Me, Who has access Anyone with the link (or per your policy)
- Copy the URL
- Client & Style‑ID autocompletes show expected values
- SKU/Item suggestions filter by client (except allow‑listed)
- PO photo workflow: Photo is uploaded to the Google Form first → then app allows submit
- Duplicate guard blocks on PO + Style + SKU + Material
- PO Date stored as dd/MM/yyyy (no time) in
Indent Details!A - Target Date stored as **dd/MM/yyyy
inIndent Details!J` - Column K fills with photo URL (or “No photo uploaded”) after
extendXLOOKUP -
Execution LogandError Logrecord events
-
Duplicate check scope: The guard intentionally does not use any time‑window cache; it purely scans what’s already in
Indent Details. -
Error summary email:
sendErrorSummary()currently summarizesError Logas if the columns are[Timestamp, PO Number, Error]. However,logError(msg)appends[Timestamp, "ERROR", msg].-
If you want PO Number in the email, either:
- Change
logError()to write[Timestamp, poNumber, msg], or - Adjust
sendErrorSummary()to use the actual column layout.
- Change
-
-
Leading apostrophes: PO Numbers are written with a leading
'to force text.extendXLOOKUP()handles both raw and stripped versions. -
ALWAYS client list: In the client‑side suggestions, certain clients (
['reliance','first cry','threads']) bypass strict client filtering for convenience. Adjust or remove as needed.
This project uses standard Apps Script scopes:
- Spreadsheet read/write
- Mail (for error summaries)
- Triggers (time‑based)
Keep the deployment access aligned with your organization’s policy.
- Keep
MasterItemstidy; large lists still work but increase client‑side filtering time. - Avoid volatile sheet formulas in
Indent Details. - If
Form_responses_1grows very large, consider indexing or moving to a lookup range (named range) for faster XLOOKUP.