| # | Name | Country | Contact | Phone | Cur. | Notes |
|---|
No suppliers yet.
| # | SKU | Description | Specs | HS Code | Supplier | Unit Cost | Unit Price | UOM | Margin | Cur. |
|---|
No line items yet.
| Invoice # | Buyer | Dest. | Date | Status | Total | COGS | Gross Profit | Net Profit | Margin | Tax% | Dep. Rec. | Balance Due |
|---|
No invoices yet.
| CN # | Buyer | Type | Linked Invoice | Date | Credit Amount | Status |
|---|
No credit notes yet.
| PO # | Supplier | Linked Invoice | Date | Status | Total COGS | Dep. Paid | Balance Due |
|---|
No purchase orders yet.
| Ref | BL Number | Vessel | Carrier | Origin → Dest | ETD | ETA | Status | Container | DG | Docs | Linked Invoices |
|---|
No shipments yet.
| Quote # | Client | Date | Valid Until | Mode | Lines | Sell (USD) | Sell (GBP) | Status | PO |
|---|
No quotes yet. Click + New Quote to get started.
| # | Name | Company | Status | Source | Supplier | Last Contact | Enquiries |
|---|
No contacts yet. Click + New Contact to get started.
| # | Contact | Stage | Description | Open Actions | Realised Margin (GP) |
|---|
No order requests yet. Click + New Order Request to get started.
| # | Company | Contact | Currency | Payment Terms | Credit Limit | Outstanding (GBP) |
|---|
No buyers yet. Click + New Buyer to get started.
Reads directly from your master template in Google Drive. No export, no file, no formatting issues.
File: My Drive > FPM > FPM STACKD - OPS TEMPLATES > stackd-master-template-v1.0.4
Upload your supplier list first. Line items and POs reference suppliers by name — suppliers must exist before you import anything else.
Upload your product catalogue. Supplier Name in the CSV must match exactly what you entered in Step 1.
Upload your existing invoices. Line items are not required — invoices appear on the dashboard immediately with your provided totals. Enrich with line items later by editing each invoice. Status values are case-insensitive: paid, Paid, PAID all work.
Upload existing POs. Supplier Name must match your supplier list. Invoice # links the PO to an existing invoice — leave blank for standalone POs.
Upload structured purchase requirements collected from a webform or similar intake source. Rows sharing a Submission ID become one Order Request with multiple lines. Contacts are matched by email or created automatically.
Upload a list of buyer/prospect contacts. Matched by email if present, else by exact name. Existing contacts are updated, not duplicated — columns left blank on a re-upload keep their previously-saved value.
Switching language updates all interface labels instantly. Your data is not affected.
ⓘ When sync is enabled, supplier, invoice, PO, shipment, and payment data (including contact names and financial records) is transmitted to Google Sheets via your configured endpoint. This data is processed under your Google Workspace Data Processing Agreement. Sync is opt-in — no data is transmitted if no URL is configured.
ⓘ Buyers data — Buyer records (company name, contact name, email, phone, address) are stored locally in your browser (key: st_buy). Legal basis: UK GDPR Art. 6(1)(b) — contract performance (invoicing); Art. 6(1)(c) — legal obligation (HMRC 6-year financial record retention, Companies Act 2006 s.388, VAT Notice 700/21 s.19). Retention: no automated expiry; retain for the business relationship duration and minimum 6 years thereafter. Buyer data is included in JSON backup exports — secure exported files. No external transmission in this version (Sheets sync deferred to v3.0).
ⓘ Activity event log — Stackd Ops records operational events (contact created, status changes, quote conversions) in a local activity log (st_ev). Event records contain entity IDs and action descriptions only — no personal names. The log is capped at 2,000 entries (FIFO); oldest entries are dropped automatically. This data is local-only, included in your JSON backup, and never transmitted externally. Legal basis: Art.6(1)(f) legitimate interests in operating the trade portal.
Load a complete end-to-end demo scenario — supplier, contact, quote, PO, invoice, payment, and in-transit shipment — to showcase Stackd to prospects. Demo records are excluded from financial KPIs.
↻ Live Rates fetches current GBP→USD/CNY/BBD from the fawazahmed0 currency API (jsDelivr CDN, free, no key). Other rates set manually. Changes apply immediately after save.
ⓘ When a webhook URL is configured, shipment data (origin/destination ports, ETD, cargo description, forwarder contact name and email) is posted to that endpoint on each Forwarder Update Request. Only configure a URL you control or trust. Webhook is opt-in — no data is transmitted if no URL is configured.
Read-only reference. Gaps indicate data not available in Stackd that the platform expects.
| Xero Field | Stackd Source | Gap |
|---|---|---|
| ContactName | inv.buyer | — |
| EmailAddress | — | No buyer email in Stackd |
| POAddressLine1 | inv.buyerAddr | Partial (single field vs multi-line) |
| InvoiceNumber | inv.num | — |
| InvoiceDate | inv.date | — |
| DueDate | — | No due date; not reliably derivable from Payment Terms |
| Description | line.desc | — |
| Quantity | line.qty | — |
| UnitAmount | line.up | — |
| Discount | — | No discount field |
| AccountCode | — | No account codes in Stackd |
| TaxType | Derived: NONE (taxRate=0), TAX001 (taxRate>0) | Partial |
| TaxAmount | Calculated per line | — |
| Currency | inv.cur | — |
| TrackingName1/Option1 | — | No tracking categories |
| QB Field | Stackd Source | Gap |
|---|---|---|
| Customer | inv.buyer | — |
| Invoice Date | inv.date | — |
| Due Date | — | No due date |
| Item (Product/Service) | line.desc | No product/service code |
| Description | line.desc | — |
| Qty | line.qty | — |
| Rate | line.up | — |
| Amount | line.qty × line.up | — |
| Currency | inv.cur | — |
| FreeAgent Field | Stackd Source | Gap |
|---|---|---|
| Date | payment.date | — |
| Payee | inv.buyer via invId lookup | — |
| Description | payment.notes or payment.method | — |
| Net Amount | payment.amount | — |
| Tax Rate | — | No tax on payment records |
| Currency | inv.cur via invId lookup | — |
| Reference | payment.reference | — |
Owner-only overrides. Actions here are logged to the audit trail.
Invoices with status Sent, Partially Paid, Paid, or Cancelled are locked for HMRC compliance. Use this override only to correct a data entry error. The adjustment reason is logged permanently.
Reassigns all invoices from one buyer name to another — use to fix duplicates caused by inconsistent spelling (e.g. "Acme Ltd" vs "Acme Ltd."). Does not touch financial fields. No unlock required.
Export the VAT Return CSV and import into your MTD bridging software (e.g. ANNA Money, Sage, BTC Software) for HMRC submission. Boxes 2, 4, 7, 8, 9 are £0.00 — input VAT tracking is not yet supported (MTD-GAP-001).
| # | Description | UOM | Qty | Unit Cost | Unit Price | Margin | Total | Type |
|---|
cInv()/calcInv() actually compute. Also now catches a previously-missed case: a dangling library link (catalogue item since deleted) with no Unit Cost.status:'lead', source:'webform'). Re-importing an existing Submission ID only appends genuinely new lines — never overwrites lines already touched by an operator.approvedAt is captured only on the actual transition into Accepted, not on every re-save.pullAll() field-mapping and identity loss (SYNC-GAP-001) — the two-way Sheets Pull never translated Sheet column headers (e.g. "Description", "SKU") back to the app's internal field names, so every pulled Supplier/Line Item/Shipment/Invoice/etc. record read as blank under its real field, and its delete button silently failed (comparing against a permanently-undefined id). New unmapRec() reverses mapRec(); new mergePulledWithLocal() generically preserves any field not tracked in FIELD_MAPS (e.g. inv.type, li.priceHistory/invoiceRefs, co.enquiries, sup.ct, payments.invId) instead of silently dropping it on a matched pull.id column in Google Sheets at all; pullAll() now matches these by business key (SKU, Shipment Ref, Invoice/PO/CN/Quote #) instead of a permanently-absent id, preserving the local record's real identity on a matched pull.id — new claimOnceMatcher() ensures only the first legitimately claims it; the second gets its own fresh identity.isEmptyLI() (its only call sites were superseded by the generic merge above).renderBuyers() and the Buyer Summary panel were reading a non-existent currency field on invoices (always silently defaulting to USD) instead of the real cur field — corrected everywhere this feature touches.saveRates() would have silently reset the new display-currency preference back to GBP on every routine Rates & FX save — preference is now preserved across rate saves.lines[], separate from Quote lines, capturing what was actually asked for before pricing is possible: order volume (the stated ask, e.g. "1 container") kept distinct from base UOM/quantity (the costable unit, often unresolved at intake), a qtyStatus (Unknown/Estimated/Confirmed) set independently per line, and packing spec/source country/variant notes.update_order_line action) are always logged as pending and require an explicit operator confirm before the live field changes. 200-entry FIFO cap per line's update log, matching the existing event-log pattern.Unknown — informs, does not block the save.update_order_line has no matching AI read-tool). AI_SYSTEM_PROMPT updated. 13 new tests. Tests: 363/363 pass.qteToPoConvert() now creates one Purchase Order per distinct supplier on a Quote's lines, instead of a single PO misattributing every line except the first supplier's. Quote.linkedPOId (scalar) replaced by linkedPOIds (array), migrated automatically for existing Quotes. PO-number collision guard added against manually-typed PO numbers.New → Qualifying → Quoted → Converting → Processing → Fulfilled (with Declined/Lost exits), a dated action/follow-up list per request, a "Create Quote" handoff that links the request to its Quote, and a live-computed realised margin once Fulfilled (summed across every supplier PO/Invoice the Quote resolves to). Existing Quotes/Contacts are backfilled into Order Requests automatically. Stage changes outside the normal flow require a CONFIRM+reason admin override, permanently logged.AI_SYSTEM_PROMPT updated. 27 new tests. Tests: 349/349 pass.num (SUP-0001, LI-0001, BUY-0001, CON-0001), assigned once at creation, shown as the first list-table column. parseRefNum()/nextRefNum()/backfillRefNums() added; backfill runs on load, on restore, and immediately after every Sheets sync pull (fixes a would-be duplicate-number bug found by schema-migration-reviewer where a sync pull silently strips num since it isn't part of the Sheets field mapping). Cross-device num divergence remains an open, accepted limitation (DATA-GAP-003) of the localStorage-only architecture.num exists, cross-device identifier divergence. Documented in docs/reporting-pipeline-runbook.md.AI_SYSTEM_PROMPT updated with a Reference Numbers section. 19 new tests (backfill algorithm, malformed-input handling, two critical collision/stripping regression tests, per-entity save-path coverage). Tests: 322/322 pass.temperature: 0.2 added to AI assistant API calls (AI-GAP-007) — live testing found the AI inconsistently emitted action blocks for create requests (3 of 6 tested actions described manual steps instead), likely due to unset/default sampling temperature; lower temperature improves reliability of "always do X" instruction-followingcreate_supplier and create_buyer action blocks (AI-GAP-006, partial) — same review-and-confirm pattern as PO/Quote/Shipment/Contact; nothing auto-savessanitizeLogo() validates logo data URIs before <img src> interpolation in PDF headers and branding preview (SEC-GAP-015)expAll() now exports { url, auto, pol } only; bearer credential never in backup file (SEC-GAP-016)Content-Type: application/json header added to all Sheets sync requests (was missing)doPost() cannot read HTTP headers (Apps Script limitation); token-in-body is the correct approach for this architecture (SEC-GAP-007)AI_COMPLIANCE_PROMPT covering HMRC VAT 700/21, invoice locking, CN treatment, verified calc_ values, GDPR obligations, and MTD gap; compliance-specific quick-action chips; mode persisted to localStorageborder-radius: 6px on cards/modals, box-shadow depth layer, semantic left-border colour on KPI cards via :has(), refined table row hover, action button fade-in, modal slide-in animation, consistent border-radius: 4px on inputs/buttons/tags/toastsYYYY-MM-DD (ISO timestamp stripped); credit amounts displayed as negative on statement and PDFAccepted status only. Button hidden on Draft/Sent/Declined/Expired; appears automatically when status is set to Accepted. Hard guard added in qteToPoConvert(). "PO RAISED" badge shown in edit modal title once a PO is linked.sv() quota error upgraded from dismissible toast to blocking modal with one-click backup export. Logo upload now wrapped in try/catch; quota modal fires if logo write fails. Silent data-loss on storage-full condition eliminated.docs/dr-procedure.md covers backup schedule, restore steps, recovery scenarios, and known limitations.pullAll() double-fetch eliminated — was making 5 redundant API calls per pull (each entity fetched twice). Now one fetch per entity. Pull is faster and no longer wastes Apps Script quota.delEnt() now respects the auto-sync toggle — deletes no longer propagate to Sheets when auto-sync is off (was inconsistent with syncEnt).pullAll() now updates the "Last synced" timestamp after a successful pull (previously only syncAll set it).runFPMMigration(), flag st_fpm_repair_v1). repairCalcFields() is now generic and operator-safe. Settings → Repair invoice totals updated accordingly.repairCalcFields() now strips stray cnAmount from non-CN invoice records on every init — clears INV10031 data integrity warningconnect-src https:, object-src 'none', base-uri 'self'; defence-in-depth against XSS exfiltration (SEC-GAP-008)checkStorageQuota() — runs on app init; warns via toast at 75% and 90% of the 5 MB localStorage limit; prompts export before data loss (BACKUP-GAP-002)toGBP(amount, cur) helper — converts USD/RMB/CNY/BBD to GBP using stored QR rates; used by all dashboard financial aggregations and chartspullAll() crash — undefined variable scope in entity merge block; now correctly uses pulled/localOnly inside the loopsan() applied to dashboard/supplier/accounts charts, CN PDF bank field, payment table rowstestConn() sync token moved from URL query string to POST bodysyncAll() URL guard strengthened — requires https:// prefix before any sync attemptst_last_sync stored on successful sync; renderSyncStatus() shows relative time in Settings (green/amber/orange by age)cache_control: ephemeral; reduces input tokens ~90% on repeat queriestoggleAIMode() button in AI assistant panel; separate AI_COMPLIANCE_PROMPT focused on trade compliancesan() coverage on PDF blob builders (prevInvDoc/prevPODoc), AS.bank, colour CSS injection guardsaveCfgUrl/saveFwdWebhook — rejects non-https URLs at input.catch() chained on all syncEnt/delEnt callsvCN() validation rewritten using vErr/vOk/vClr helpers; linkedInv lookup cleaned upsaveCN updates calc_balanceDue on linked invoice; balance always live from cInvFIELD_MAPS, mapRec, syncEnt/delEnt/syncAll/pushAll; Cloudflare Worker CORS proxy; Apps Script bulk upsert/delete with audit logcalc_ fields used as source of truth; credit notes excluded from revenue, profit, and invoice count KPIsrepairCalcFields — COGS corrections for INV10028 and INV10031| # | Description / SKU | UOM | Qty | Unit Cost | Total |
|---|
Select items to add. Quantities editable after import.
Select invoices to include in the load. CBM is read from the Est. Cubic Weight field on each invoice. DG flag is raised if any linked library item has DG checked.