| # | 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.
ⓘ When configured, Suppliers and Buyers are stored in a shared Supabase database instead of only this browser's local storage, so a colleague on another device sees the same records. Requires a separate Supabase sign-in the first time a session loads with this configured. Every other deal-pipeline and master-data entity (Line Item, Contact, Order Request, Quote, Purchase Order, Invoice, Credit Note, Shipment, Buyer Payment, Supplier Payment) migrates independently via its own card below — see each card's own note for what still stays local-only until that entity migrates too. Migrating existing local Suppliers/Buyers requires a full backup export first (a blocking step) and moves them to the cloud copy as the source of truth going forward — the "Ad-Hoc" default Buyer never migrates and stays local-only.
ⓘ Requires Suppliers to already be migrated to Cloud Data above — every Line Item requires a Supplier link. Uses the same Supabase connection configured above.
ⓘ Requires Suppliers to already be migrated to Cloud Data above, even for Contacts with no Supplier link (see docs/architecture-data-model-v1.md for why). Uses the same Supabase connection configured above.
ⓘ Independent of Supplier/Contact migration — Order Request Lines and RFQ Responses stay embedded with the Order Request, so no other entity needs to migrate first. Uses the same Supabase connection configured above.
ⓘ Independent of Supplier/Contact/Order Request migration — Quote lines stay embedded with the Quote, so no other entity needs to migrate first. Uses the same Supabase connection configured above.
ⓘ Requires Suppliers to already be migrated to Cloud Data above (every Purchase Order requires a Supplier link). Uses the same Supabase connection configured above.
ⓘ Requires Buyers, Line Items, Quotes, and Purchase Orders to already be migrated to Cloud Data above (every Invoice requires each of these links). Uses the same Supabase connection configured above.
ⓘ Independent of every other entity's migration — no other entity needs to migrate first. Uses the same Supabase connection configured above.
ⓘ Requires Invoices to already be migrated to Cloud Data above — every Buyer Payment requires an Invoice link. Uses the same Supabase connection configured above.
ⓘ Requires Purchase Orders to already be migrated to Cloud Data above — every Supplier Payment requires a Purchase Order link. Uses the same Supabase connection configured above.
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 |
|---|
Optionally record which Quote this Invoice corresponds to, for audit trail. This does not change the Invoice's status.
REQ/SPEC-QTE-003) — the same two-tier cascade as Margin %. A new "Insurance % Override" field on the quote form overrides the Settings → Rates & FX global default for that one quote (blank = inherit); a new "Ins %" field on each line in the editor grid overrides the quote-level value for just that line (blank = inherit). cQteLine() gained a 5th parameter carrying the quote-level override down to this per-line resolution; qteEffectiveInsRate(line, quoteInsRatePct) is the new line-vs-quote resolver, mirroring qteEffectiveMargin(). The quote-level override persists as Quote.insRate (new nullable ins_rate column on the Supabase quotes table, migration 0009_quotes_ins_rate.sql — additive only, null means "inherit," identical in shape to the existing Origin/Dest/Admin override columns); the per-line override rides inside the pre-existing lines jsonb column alongside line.markup, needing no schema change. Price-history snapshots now also record the effective Insurance % and $ used at save time, shown as two new columns in the version-history panel.REQ/SPEC-INV-002). Found live against real production data: two real invoices had product lines saved with Unit Cost 0 and no catalogue link, inflating Gross Profit/Net Profit by $6,321.27 combined ("Pallets x3"/"Pallets x9" line items, and a $5,540.07 freight/customs charge line entered as a full-margin product rather than a pass-through). Quick-add now refuses to add a Product-type line with no Unit Cost and no library link; Save Invoice refuses the whole save if any Product line still lacks both — including a line that had a valid cost when added but was zeroed out afterward directly in the line-item table, which the quick-add-only fix alone would have missed. Pass-through and Charge line types are unaffected — Unit Cost 0 is a normal, expected value for those and is not treated as an error.REQ/SPEC-WEBHOOK-001). Settings → Integrations → Automation Webhooks lets an operator configure one or more rules (trigger + destination https:// URL, individually enabled/disabled). v1 ships one trigger, "Invoice: Buyer Approved" — the moment an Invoice's buyer approval genuinely transitions from unapproved to approved (never on a later correction to an already-approved invoice), a JSON payload POSTs to every enabled rule carrying invoice/buyer data, line items, and the invoice rendered as HTML, so the destination automation (e.g. a Make.com scenario) can build a PDF and email it to the buyer — this app never generates a PDF itself. If the invoice's buyer has no usable email on file, including the Ad-Hoc/BUY-ADHOC placeholder buyer, the webhook is skipped with an explanatory toast rather than sent to an empty recipient. Fire-and-forget, matching the existing Forwarder Webhook precedent: no delivery guarantee or retry if the destination is briefly unreachable. The deferred outbound-webhook half of an operator request whose in-app-rules half shipped as REQ/SPEC-SHIP-001 (v2.9.86).scripts/check-req-citations.js, a scripted citation-checker now mandatory before every future REQ/SPEC gate submission, added after 3 spec-gate rounds spent finding nothing but wrong index.html:NNN citations that were mechanically checkable in seconds. All 10 safety-critical mechanisms in the SPEC's own mutation-testing checklist confirmed via actual mutation testing (each fix reverted in a scratch copy, confirming the exact predicted test(s) fail and nothing else, then restored) before this delivery closed out. 20 new tests. Independent build-gate review: clean PASS, no spec deviations, 1012/1012 confirmed.@gmail.com account with no processor agreement at all — logged as an update to SEC-GAP-025. Since this app has no way to verify a destination's own data-handling practices, adding a webhook rule now requires an explicit attestation checkbox ("I've checked this destination's own data handling... including that any email it sends goes out from a business account, not a personal Gmail") before addWebhookRule() will accept it — mutation-tested to confirm the guard actually blocks an unattested submission, valid URL notwithstanding. The rule record carries a dataHandlingAttestedAt timestamp for audit purposes. 1 new test. Tests: 1013/1013 pass.REQ/SPEC-SHIP-001). Marking an Invoice Paid — whether a status change or a brand-new invoice saved directly as Paid — now auto-creates a linked Shipment record (unless disabled via the new Settings → Integrations toggle, which shows a persistent banner on the Invoices/Shipments tabs while off) seeded with a 7-item trade-document checklist (Purchase Invoice, Commercial Invoice, Packing List, Bill of Lading, Certificate of Origin, Verified Gross Mass Declaration, Insurance Certificate — an 8th, Dangerous Goods Declaration, if the shipment is DG-flagged) that the operator fills in with reference numbers, received dates, and a file-location note as documents arrive; no file upload, location/link tracking only. The default checklist is deliberately destination-agnostic — no Barbados-specific customs forms hardcoded — since FPM's own stated expansion beyond Barbados to Nigeria/Ghana/India would make a single-country checklist actively wrong, not just incomplete; grounded in cited shipping-document research including a Verified Gross Mass requirement (SOLAS-mandatory for every containerized shipment since 2016) that's easy for small-operator tools to miss. Went through this repo's full requirements-gate (3 independent review rounds — round 1 found a design that would have silently wiped the entire checklist on the first ordinary edit to an auto-created Shipment, and a trigger design that missed a brand-new invoice saved directly as Paid; round 2 found the round-1 fixes had introduced two new issues of their own, including a duplicate-Shipment risk from an invoice-number rename; round 3 found only documentation drift) and spec-gate (3 independent review rounds — found a wrong claim about CSS positioning, a citation gap, a new CRUD layer that could silently discard an edit on a cancelled Cloud Data login, and a create-path bug that would have overwritten an operator's own manual dropdown selection) processes, with every finding personally re-verified against the live code before being accepted, and every safety-critical fix confirmed via mutation testing (each fix reverted in a scratch copy, confirming the exact predicted test failure) before this delivery closed out. Independent build-gate review then caught one severe defect: the trade-document checklist's three CRUD functions were correctly built and unit-tested but never wired into any UI — invisible and uneditable in the actual app, despite already-shipped docs describing it as working. Fixed with a new Trade Documents section on the existing Edit Shipment modal, wired to the same functions unmodified. A round-2 confirmatory build-gate pass then found two more real test-coverage gaps in that UI fix (live in-modal re-render, per-row wiring correctness) plus a genuine UX bug — the pre-existing "Docs Status" dropdown on the same modal went stale once the checklist made it auto-computed. Fixed by disabling/syncing that dropdown and closing both coverage gaps. A round-3 confirmatory build-gate pass then caught one more regression from the round-2 fix: opening a new Shipment right after editing a doc-having one left the "Docs Status" dropdown permanently locked, since the create path never reset it. Fixed. A round-4 confirmatory pass found no further defect but flagged a minor test-coverage nit; a live re-verification also caught and fixed a hidden test-authoring bug of its own (a test using the codebase's known `test()`-with-`.then()` anti-pattern that always reported pass regardless of the real behavior). Build-gate is now clean across all 4 rounds. 37 + 5 + 5 + 1 new tests. Tests: 992/992 pass.REQ/SPEC-AI-GAP-012, extends REQ/SPEC-AI-GAP-011's pasted-email RFQ update to also accept a CSV file). A new button lives once per Order Request, above its line items — the operator selects the supplier the file is from and uploads a comma-delimited .csv (Excel/.xlsx not supported — no parser for it exists in this codebase, and adding one would violate the app's no-dependency policy), reviews every parsed row in a mandatory pre-send confirmation step (nothing is sent to the AI without this), and the AI attempts to match each row to one of the Order Request's own line items by description. Each confidently-matched line gets its own independent diff review — old → proposed, Apply or Discard individually, no bulk "Apply All" — reusing the exact same editRfqResponse()/saveRfqResponse() mechanism the email-parse feature already uses, so a matched line with no existing response from that supplier creates one; a line that already has one is updated in place with the same id-rotation/staleness-banner behavior. Rows the AI can't confidently match are listed separately for manual review, never guessed at or silently dropped. Re-running Import on the same Order Request (a correction, or a second supplier's file) preserves any still-pending, unapplied proposals from an earlier run rather than discarding them. Went through this repo's full two-person requirements-gate and spec-gate process before any code was written — both independent reviews found real issues in the first drafts (a genuine concurrency risk where the batch UI's own file-import buttons didn't actually prevent a double-save race through the pre-existing, shared RFQ response modal, a failed Apply being silently reported as success, and a test that would have passed regardless of whether the fix worked), all fixed before implementation began. 14 new tests. Tests: 945/945 pass.SEC-GAP-021 — stored XSS via unsanitized status text in class attributes, found by a proactive security audit run immediately after the Cloud Data migration went live. 7 render sites built a CSS class string by slugifying raw status text and concatenating it into class="tag s-<slug>", breakable out of the attribute by a status value containing a double-quote — concretely reachable via CSV Invoice import, which falls through to the raw cell value for any unrecognized Status, capable of exfiltrating localStorage (the Anthropic API key, Supabase session) to an attacker-controlled endpoint. Fixed with three new fixed-lookup-table helpers (invStatusClass(), poStatusClass(), conStatusClass()) mirroring the pre-existing, already-safe shpStatusClass() pattern.INV-GAP-004 — the empty-string/numeric bug fixed the same day in migrateInvToSupabase() was still present in the two far-more-frequently-hit paths, persistInvChange() (called from 12+ sites) and saveInv()'s own cloud-save block. numOrNull() promoted to a shared helper, applied at both.LI-GAP-002/CON-GAP-007 — saveLI()/delLI()/saveCon()/delCon() gated on bare _sb truthiness instead of their own entity's migration marker, unlike every other entity's own save/del function. Fixed at all 5 gate sites.PMT-GAP-004 — added missing test coverage for the duplicate-payment confirmation guard shipped earlier the same day (no functional defect, an audit-coverage gap only)..DS_Store (same class of accidental-inclusion miss as SEC-GAP-020); logged SEC-GAP-022/023/024 as open, accepted gaps (GDPR erasure path on Cloud Data, single-shared-role RLS monitoring, and a handful of minor items) rather than silently dropping them. 15 new tests. Tests: 931/931 pass.delSup() never warned about or cleaned up Line Items referencing a deleted Supplier (SUP-GAP-001, new) — found while helping an operator diagnose 6 real Line Items (an irrigation/drip-tape product line) that had silently lost their Supplier reference, with no way to recover which supplier it was from data alone, since the deleted supplier predated the app's event log and delSup() gave no warning at delete time. delSup() already warned about linked Purchase Orders, invoice references, and Order Request RFQ responses before deleting a supplier — it now also counts and warns about Line Items linked via supId, in the same warning list. supId is left in place as a historical record on delete, not nulled, matching the existing RFQ-response precedent. 1 new test. Tests: 916/916 pass.SH-GAP-003 (new): pullAll()'s Shipment merge matched local/pulled records via s.ref && s.ref === p.ref, which can never match two blank refs against each other, so every blank/trailing-empty row in the linked Google Sheet created a brand-new phantom Shipment on every single sync, forever — confirmed live as 500 of 503 Shipment records being entirely blank shells, created in exactly 20 timestamped bursts of 34 or 4 records matching 20 sync runs. Fixed by dropping a pulled Shipment row with no ref before attempting any match, mirroring the existing SYNC-GAP-001-class drop already in place for id-keyed entities with no resolvable id. SH-GAP-002 (previously logged at REQ-CLOUD-007 ship time, deliberately deferred): mapRec()/unmapRec()'s linkedInvs asymmetry let an ordinary Sheets sync corrupt it from an array into a string — confirmed live as 3 real Shipments that would crash rShp()/editShp()/openFwdReq() the instant any of them was opened, aborting renderAll() for every other entity in the same pass. unmapRec() now splits it back into an array at the source; a new backfillShLinkedInvs() heals any record already corrupted before this fix existed. Also extends Scan for phantom records so a blank-shell Shipment (real ID, every other field blank) is now detected, and adds Supplier Payment to its entity coverage, closing a blind spot that existed since that ledger shipped. Separately, resolved unresolved git merge-conflict markers left in main by PR #135 across 7 docs files — a commit on that PR's source branch had claimed to resolve them but hadn't, and the merge carried the broken content through untouched. 7 new tests. Tests: 915/915 pass.PO-GAP-005). Surfaced during architecture research for the planned cross-platform backend migration (see docs/architecture-data-model-v1.md §6.1) as the review's single highest-priority finding, not reported by an operator directly — but confirmed as a live, currently-shipping defect. qteToPoConvert(), the function behind "Convert to PO" on an Accepted Quote, built each generated Purchase Order using a different field-naming scheme than every other Purchase Order code path in the app, at both the document level (lines/dt/currency/fpm/rec instead of lineItems/date/cur/fpmFunded/fpmRecovered) and, one level deeper, on every line item (liId/up/a stray per-line cur instead of lid/cost/no per-line currency, plus a missing sku key). The resulting Purchase Order was created successfully — it got an id, a number, a slot in the Quote's linkedPOIds[], and a "PO created" success toast — but was then functionally invisible to the rest of the app: editPO()'s line-item editor opened empty, rPO()'s table row and the printed PDF showed $0/no line rows, renderPoSourceDriftWarn() silently never fired, the currency always displayed as USD regardless of the Quote's real currency, and the PO never counted toward FPM-funding totals. This was the app's primary, steered conversion path. Fixed both levels of the field-naming mismatch in qteToPoConvert(), and added a new migrateQtePoShape() migration — wired into the same 5 call sites as the existing backfillInvoicePOs() migration (sync pull, both CSV/Sheets PO-import paths, full JSON restore, and app boot) — to correct any Purchase Order already created with the broken shape, at both the document and line-item level, the next time it loads on any device. Went through requirements-gate (2 rounds, both CONDITIONAL PASS — round 1 caught that the original draft addressed only the parent PO's fields and missed the nested line-item mismatch entirely, and that the migration's proposed wiring precedent was the wrong sibling function; both fixed before round 2 passed) → spec-gate (CONDITIONAL PASS, advisories only — the reviewer independently applied the full diff to a scratch copy and ran the real test suite, then executed both required mutation tests themselves, reaching 687/687 passing) → mutation-tested a second time after implementation (reverting the qteToPoConvert() diff alone reproduced exactly the 4 expected test failures; removing the migration function's body alone, with its call sites left in place, reproduced the predicted "not a function" cascade). 6 new tests. Tests: 687/687 pass.IMPORT-GAP-001). User reported their Suppliers tab had become "majorly corrupted" after a CSV re-import — several rows turned out to be fragments of one other supplier's multi-line Notes/address content, each torn off into its own new record with a real, sequential SUP-#### number and dash placeholders in every other column. Root cause: parseImportCSV() split the file into rows on every literal newline before any CSV quote-state tracking existed, so a quoted multi-line cell (exactly what Excel/Sheets produce for a wrapped Notes/Address field) had each of its internal lines torn into a separate phantom row — invisible to the existing "Scan for phantom records" tool, since every fragment gets a real, non-empty id (that tool only detects a missing id). Directly answers the user's other question too: confirmed by reading the entire Apps Script sync path that Google Sheets sync is not involved — this is specifically a CSV-import-time defect. Rewrote parseImportCSV() as a single-pass character-stream parser that tracks quote state continuously (a newline is a row separator only when not inside an open quote), plus added RFC 4180 escaped-quote ("") support the old parser never had. Went through requirements-gate (PASS, 2 advisories fixed) → spec-gate (PASS, every acceptance criterion — including the two highest-risk, escaped-quote and unterminated-quote safety — independently verified by actually running the proposed code against adversarial inputs, plus the full test suite run against the patched file) → build-gate (PASS, no findings — the mutation-test claim of "exactly 5 tests fail on revert" verified precisely). Shared by every CSV import type (Suppliers, Line Items, Invoices, POs, Order Requests, Contacts) — the fix and its cross-entity test both confirm it applies uniformly, not just to Suppliers. 10 new tests, closing a pre-existing zero-coverage gap for Supplier CSV import. Tests: 681/681 pass.editRfqResponse()/saveRfqResponse() edit path unmodified, so the id-rotation/staleness-banner behavior for a committed response is inherited automatically rather than reimplemented. Went through requirements-gate (CONDITIONAL PASS, 5 advisories fixed) → spec-gate (CONDITIONAL PASS — 1 real blocking finding: the pending-proposal state wasn't scoped per Order Request line, but multiple lines' comparison panels can be open at once, so a stale Apply button from an abandoned line could silently act on a different line's proposal; fixed by tracking which line produced the current proposal and verifying it before acting, plus 4 advisories) → build-gate (CONDITIONAL PASS — a stray one-character formatting change and incomplete version-ship housekeeping, both fixed here; the feature logic itself had no findings). The cross-line fix was independently mutation-tested twice (once by this build, once by the build-gate reviewer): removing the primary guard doesn't corrupt data — a second guard still catches it — but does silently break the legitimate follow-up Apply on the unaffected line, proving the guard is genuinely load-bearing. 17 new tests. Tests: 671/671 pass.inv.pos[], written on every invoice save but never once read anywhere in the app — the real, live answer to "which POs belong to this invoice" was instead an ad-hoc filter (po.invId===inv.id||po.invNum===inv.num) independently retyped at 3 call sites (the Accounts page's per-invoice view, and two separate FPM-deposit auto-recovery blocks). inv.pos[] is now the authoritative record: a new getInvoicePOs(inv) helper reads it at all 3 sites, a new backfillInvoicePOs() migration keeps it correct, and delPO() now cleans up a deleted PO's id from any invoice that referenced it (previously left dangling forever, silently, since nothing read the field to notice). Check-first, and three escalating rounds of independent review, found the app links a PO to an invoice through six different mechanisms, not just the one (autoPos()) originally assumed — CSV import, Google Sheets record import, and pullAll()'s bulk Sheets pull can all link a PO to an invoice too, and each now re-syncs inv.pos[] immediately rather than only at the next app boot. The last review round caught a genuine algorithm bug before any code was written: the backfill's original per-invoice matching could assign the same PO to two different invoices at once when a PO's invId and invNum disagreed (exactly what a Sheets re-link produces) — fixed by resolving each PO to a single home invoice, preferring whichever field a re-link actually changes (invNum) over the one it doesn't. Logged, not fixed: PO-GAP-004, a narrow, pre-existing savePO() edge case found along the way (renaming an invoice can silently clear a linked PO's invId on its next save). 11 new tests. Tests: 620/620 pass.ACCT-GAP-001, AI-GAP-010), the same defect class as v2.9.64's totals-bar fix. The Accounts page's per-invoice columns (Sup. Dep. Paid, FPM Funded, Sup. Bal. Due, Total to Chase) and per-supplier columns (Total COGS, Dep. Paid, Bal. Due to Sup.) now convert each linked PO's figure via toDisp() before summing, instead of raw-summing across POs that may be in different currencies — re-scoping check-first found this was wider than originally logged, since fpmFunded/totalToChase/totalCOGS weren't named in the original gap entry. Invoice-native columns (Invoice Total, Buyer Dep. Rec., Buyer Bal. Due) are unchanged, still shown in each invoice's own currency. The AI assistant's get_kpis tool has the identical fix applied to invoiceRevenue/netProfit/outstanding/poBalanceDue, and its response now includes an explicit currency field so the assistant always knows what unit a figure is in, rather than guessing. Dep. Coverage % and avgMargin needed no code change — proven algebraically that a ratio of two sums over the same record set, both converted the same way, is unaffected by the choice of display currency. Went through a full requirements-gate → spec-gate → build-gate review cycle; spec-gate caught a pre-existing test that would have broken once get_kpis started converting currencies (fixed by pinning its test's display currency to match its PO's own, consistent with the established regression-guard pattern). 5 new tests. Tests: 609/609 pass.toDisp() mechanism before summing, mirroring the pattern already correct on the Dashboard. Went through a full requirements-gate → spec-gate review cycle (2 revision rounds total) given it touches financial totals display. Also fixed: the Purchase Orders list didn't re-render after recording or deleting a Supplier Payment (data was correct, only the list view was stale until navigating away and back); the Edit PO deposit field displayed raw unrounded floats from cross-currency conversion instead of a clean 2dp figure; the read-only state on that field had almost no visual signal beyond a small note (now a clearly disabled cursor/background). Also fixed a demo-data gap: the PO meant to demonstrate the "legacy deposit, no ledger records" fallback path was seeded with $0 instead of a real figure, making it untestable as designed. Logged two related, pre-existing (not introduced by v2.9.63) currency-mixing gaps found along the way but deliberately not fixed here to keep this fix scoped: ACCT-GAP-001 (Accounts per-invoice/per-supplier columns) and AI-GAP-010 (the AI assistant's get_kpis tool). 10 new tests. Tests: 604/604 pass.PO.dep field directly. New getPOEffectiveDep(po) is now the single point of truth for every one of those 10 read sites: once a PO has linked Supplier Payment records, its Deposit Paid figure is reconciled from the ledger (native-currency amounts summed directly and exactly; a different-currency payment converts via its own historically-locked FX rate, not today's live rate — lockFxRate() now snapshots the full rate table, not just the one rate it used, so this second conversion leg is genuinely historical too). A PO with no linked records, or a currency this mechanism can't yet safely reconcile (EUR — logged as PROC-GAP-002), keeps showing its manually-entered figure exactly as before, with a note explaining why. The "Supplier Deposit Paid" field itself becomes read-only once ledger records exist, directing the operator to the payment ledger instead of creating two ways to change the same number. Went through 2 independent review rounds each for requirements-gate and spec-gate; round-1s each caught a real blocking defect (a missed 10th read site plus a EUR-currency PO that would have shown a silently wrong, mislabeled figure on its own PDF; and a read-only/note state that could leak from an edited PO into a freshly-opened new one). 19 new tests. Tests: 594/594 pass.PO.dep/PO.fpmFunded fields' complete lack of history. Each payment locks in a GBP-equivalent snapshot (lockFxRate()) at save time using the existing toGBP() mechanism and current FX rates — the snapshot never changes later even if rates are updated afterward, so historical totals stay accurate. New getPOTotalPaid() sums a PO's payments correctly across multiple native currencies by summing the locked GBP-equivalents, not raw amounts. This is a standalone ledger alongside the existing PO fields — it does not read from, write to, or replace them, and this phase does not yet link to buyer-side payments or Invoice collections (later sub-phases). Went through 2 independent review rounds each for requirements-gate and spec-gate; round-1s each caught a real blocking defect (a false claim that the buyer-side Payments ledger is unsynced, which would have wrongly justified giving the new entity an inert field-map entry — corrected once the facts showed buyer Payments is actually bulk-synced, so the new entity instead follows the genuine no-sync precedent already used by Order Requests/Buyers; and a missing backup/restore wiring that would have made the new ledger's only backup — the local JSON export — silently drop every record). 12 new tests. Tests: 575/575 pass.loadDemoData()) with 7 new scenarios so REQ-INTEG-001 Phase 1/2 can actually be exercised locally instead of against production data (interim mitigation for SDLC-GAP-003, no staging environment): an Order Request (DORD-0001) with 2 RFQ responses from different suppliers, for the comparison panel; a Quote (DQ-0002) converted from it whose source RFQ response was re-committed afterward, reproducing the Phase 1 Quote staleness banner; an unapproved Pro-forma invoice (DINV-0002); an approved, non-stale invoice (DINV-0003) for the clean "Progress to Invoicing" path; an invoice (DINV-0004) that was approved then had a line edited, showing Phase 2's automatic approval-clearing already having fired (both events present in the log); and an invoice/PO pair (DINV-0005/DPO-0002) reproducing the Phase 1 PO staleness banner. One scenario in the original brief — an illegal Order-Request-stage auto-advance from "Progress to Invoicing" — was not built: check-first confirmed Progress to Invoicing never touches Order Request stage in the shipped Phase 2 design (an explicit, confirmed decision from that build), so there is no such transition to exercise. Also fixed a real pre-existing gap while extending Demo Mode: loadDemoData()/clearDemoData() never included Order Requests or the Line Item catalogue at all — both are now covered by the idempotency check and cleanup. 6 new tests. Tests: 563/563 pass.buyerApprovedAt/buyerApprovedBy/approvalMethod (closed-set: Email, WhatsApp, WeChat, Phone / Verbal, Other)/approvalNote, confirming the buyer's out-of-band approval (email, WhatsApp, WeChat, phone/verbal — never inside the app). Editing any line item (price, quantity, or an added/removed line) after approval automatically clears all four fields, forcing re-approval — header-only edits never clear it. A new "Progress to Invoicing" action, available once approved, never changes the invoice's status; it only logs the authorization event and, optionally, records a manual, non-binding link to a Quote (linkedQuoteId/linkedQuoteNum) for audit trail — there is no automatic Quote→Invoice path in this codebase (confirmed, third time this initiative), so the link is a deliberate manual selection, not a derived one. Both actions bypass saveInv()'s form-rebuild entirely (direct record mutation, matching the same sv()/persistence mechanism), so they can't be tripped up by unrelated in-progress edits in the main modal. Also extended the same field-preservation pattern already used for pos to both CSV-import code paths (processImport()/processImportRecords()), which otherwise would have silently dropped an already-recorded approval on a re-import. 19 new tests. Tests: 557/557 pass.sourceQuoteId field, added in v2.9.58 (REQ/SPEC-INTEG-001 Phase 1) as forward-compatibility for a possible future Quote-aware Invoice-creation path. Confirmed with the business: direct Quote→Invoice traceability is not needed — the real workflow is always Quote → PO → Invoice, and that chain is already fully covered by two pre-existing, already-shipped mechanisms: qteToPoConvert() (sets quoteId/quoteNum on the PO) and autoPos() (sets invId/invNum on the PO). Removed the field default and edit-preserve line from saveInv() and the one test that covered it. PO.quoteId/PO.quoteNum/PO.invId/PO.invNum are untouched — those remain correct and in active use. 1 test removed. Tests: 538/538 pass.ORD-GAP-003 and PO-GAP-003. A Quote line created via "Create Quote" from a committed RFQ response now carries sourceOrdId/sourceOrdLineId/sourceRfqResponseId back to its source; if a different RFQ response is committed on that Order Request line afterward, re-opening the Quote shows a warning banner rather than silently going stale. A PO auto-generated from an Invoice (autoPos()) now captures each line's invoice unit price at generation time; if the Invoice's price, quantity, or line items change afterward — including a brand-new line added for the same supplier — re-opening the PO shows an equivalent warning. Neither banner auto-fixes anything; both are detection-and-surface only, per this phase's explicit scope. Invoice also gains a schema-only sourceQuoteId field with no automatic population in this phase — no Quote→Invoice conversion path exists anywhere in the codebase to populate it from (confirmed by direct search, twice independently), so it's added now purely so a future phase doesn't need a second schema migration. Went through 2 independent review rounds each for requirements-gate and spec-gate — every round 1 surfaced a real, code-verified blocking finding (the original PO staleness design could only ever catch 2 of the 3 drift types PO-GAP-003 itself names, never a newly-added line; separately, the price-comparison mechanism would have permanently false-flagged every PO that already exists today, since its new price-snapshot field has no backfill) — both resolved and confirmed PASS in a confirmatory round 2. One accepted, disclosed residual gap remains: a brand-new-supplier line added to an already-invoiced order has no PO to attach a warning to. 18 new tests. Tests: 539/539 pass.SYNC-GAP-001, fixed in v2.9.47 for new pulls, but pre-existing corrupted records were never cleaned up). A record is phantom if it has no internal id; Contacts use a compound check (!id || (!name && !email)) since backfillConIds() otherwise silently "heals" a corrupted Contact with a fresh id on every load, masking it from a plain !id test. The scan is read-only and shows a preview before anything changes; a zero-found scan reports clean and skips the confirm/backup step entirely. Removal is followed by sequential renumbering of Supplier/Line Item/Buyer/Contact/Order Request reference numbers only — Invoice/PO/Quote/Credit Note numbers are permanently excluded, since (unlike the others) their num field is a real cross-entity lookup key elsewhere in the app (Credit Note linking, CSV/Sheets import matching, pullAll()'s business-key matcher) and renumbering them would break already-issued documents. Renumbering preserves relative creation order and is gated behind a mandatory checkbox-confirmed backup (mirroring the existing Supabase-migration backup gate). Every removal and renumbering is written to the same Activity-tab event log every other mutation uses, so an affected Supplier/Contact/etc.'s own Activity tab shows what happened to it. A self-check after cleanup scans the known foreign-key fields (supplierId, supId, buyerId, contactId incl. nested RFQ-response contacts, sourceContactId, lid) for anything left dangling and reports it if found. Also hardens pullAll() itself: an id-keyed entity (Supplier/Payment/Contact) pulled from Sheets that resolves to no usable id is now dropped with a console warning instead of silently entering the database, closing off the live version of the same gap class going forward. Went through 2 independent review rounds (requirements-gate and spec-gate), each an initial round with real blocking findings followed by a confirmatory re-review — both closed PASS. One pre-existing test's expectations were updated to match the corrected pullAll() behavior (a header-keyed blank stub used to survive as a phantom Supplier; it is now dropped at the source). 15 new tests. Tests: 521/521 pass.ov-rfq modal. Responses are ranked by landed cost converted to GBP using the same cQteLine() rate engine Quotes already use (LCL basis, since no Quote/freight-mode decision exists yet at comparison time) — the nominal cheapest quoted price is not always the landed-cheapest once freight/duty/DG and currency are factored in. A response with CBM not entered is flagged with a warning next to its landed cost rather than silently looking cheapest. Exactly one response per line can be marked Committed; re-committing replaces the prior choice and no response is ever deleted. Converting an Order Request to a Quote now pre-fills one Quote line per committed response, carrying over supplier, quantity (the line's own base qty, if set), and unit cost — correctly currency-converted to the new Quote's working currency, not copied verbatim. delSup()/delCon() extended to warn on/clean up RFQ response references. Went through 1 independent spec-gate round (CONDITIONAL PASS — 1 blocking finding: the Quote hand-off initially dropped currency conversion entirely, which would have silently corrupted every downstream Quote calculation for a non-USD committed response; fixed and confirmed via a second independent review, PASS). While building this feature's tests, found and fixed a latent defect in the test harness itself: 8 pre-existing async tests (the Order Request per-line gap check's semantic-check tests) were registered with the synchronous test() helper despite having internal awaits — their assertions ran as detached, untracked microtasks that always reported a false-positive pass, and separately mis-referenced their mock configuration variable (ctx._mockAnthropic instead of the module-scope _mockAnthropic the mock fetch actually reads), meaning the mock was silently never engaged. Converted to testAsync() and fixed the variable reference — all 8 now genuinely exercise and verify real behavior. 14 new tests. Tests: 506/506 pass.unlinkSupCon() and openSupConPicker()'s link action now call logEv() (verbs linked/unlinked, naming the supplier), matching every other Contact mutation already logged. delCon()'s own event emission, separately deferred at the time pending DB.events shipping, was found already implemented once checked against the live code — this fix closes the one remaining real gap. 2 tests extended (no new test count).get_suppliers/get_buyers tools return only {id,num,name,country,currency}/{id,num,name,currency} — no contact/email/phone PII field is ever assembled into the payload sent to the AI. get_pos's status filter description fixed to the real vocabulary (Deposit Paid, Settled) instead of a stale, never-matching list. New create_invoice, create_line_item, and create_credit_note action blocks added to handleAIAction(), following the existing propose-never-auto-save pattern (AI proposes a payload, operator reviews and saves in the real form) — each rejects on missing required fields before opening its modal. Closes AI-GAP-006/AI-GAP-008/AI-GAP-009.conCheckEnquirySemantic(), a single-shot, non-conversational Anthropic call mirroring the existing ordCheckLineGapsSemantic() pattern, flagging ambiguous/incomplete enquiry text and phrasing follow-up questions. The API payload is built defensively field-by-field (company name + enquiry text only) rather than assumed safe by precedent, since the Contact form has PII fields directly adjacent to the enquiry text being read. Manual trigger only — no auto-run on save, no field mutation, no persistence.create_line_item's stated minimum fields, resolved in v2; CON-004: 1 blocking finding on a missing mandatory AI_SYSTEM_PROMPT update, resolved in v2) before build. 23 new tests. Tests: 492/492 pass.@supabase/supabase-js client (first use of the DASH-GAP-001 vendoring precedent). Gated by a separate Supabase sign-in the first time a session loads with it configured, cached silently thereafter; the background refresh is fire-and-forget (mirrors the existing pullAll() pattern) so it never blocks app startup or the rest of the app behind a login. Migrating existing local Suppliers/Buyers is a one-time, explicit action requiring a full backup export first (mandatory, checkbox-gated) and a pre-flight duplicate-name scan; every local reference to a migrated Supplier/Buyer (Quotes, POs, Line Items, Contacts, Invoices) is remapped to the new cloud id in the same pass. The "Ad-Hoc" default Buyer never migrates and stays local-only permanently. Pre-migration data is archived locally for 30 days as a rollback safety net (Settings → Cloud Data → "Restore Pre-Migration Suppliers/Buyers", which also disconnects Cloud Data so the restore isn't immediately overwritten by the next background refresh). Went through 4 requirements-gate rounds and 4 spec-gate rounds (each independent, each catching genuine issues — an incomplete ID-remap, a whole-app auth-gate regression, two fictional/false cited-precedent claims, a missed second Buyer-creation path, and a restore that didn't actually hold — all resolved). Fix (build-gate round 2): pullAll()'s Sheets pull-back for Suppliers (simpleEnts including 'sup') raced against the new Supabase background refresh once Cloud Data is configured — now skipped for Suppliers in that case. Fix: openSbLoginModal()/showBlockingBackupModal() re-entrancy guard added (a second call no longer silently orphans the first pending callback/resolver). ~9 pre-existing sync tests converted to testAsync() per spec. 19 new tests. Tests: 473/473 pass.getProductPriceHistory()) for comparing what different suppliers have historically charged for similar items. Always shows cost (what the supplier charges), never this app's own landed-cost or sell-price figures, so cross-source and cross-supplier comparisons aren't distorted by freight/margin assumptions. No currency conversion in this version — each point displays in its own recorded currency. Purely additive: no new entity, no new Sheets sync mapping, nothing exported or synced.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.
Sign in to access shared Supplier/Buyer data. This is separate from the app password and only applies to Cloud Data.
Before migrating Suppliers/Buyers to the cloud, export a full backup. This cannot be automatically verified as complete — please confirm you have downloaded and can locate the export file.
Before cleaning up phantom records and renumbering, export a full backup. This cannot be automatically verified as complete — please confirm you have downloaded and can locate the export file.