Inventory management + stocktake + label generation for Halo PSA. Handles serialised and non-serialised items across multiple locations.
- Node.js v16+
- Halo API credentials with scopes:
read:items,read:assets,read:suppliers,read:pos
npm installConfigure .env:
HALO_CLIENT_ID=...
HALO_CLIENT_SECRET=...
HALO_BASE_URL=https://halo.example.com
HALO_TOKEN_URL=https://halo.example.com/auth/token
PORT=3000
BASE_PATH= # optional path prefix, e.g. /halo-stock (empty = site root)
# IP allowlist (DNS-resolved; leave empty to disable in dev)
ALLOWLIST_HOST=your-allowlist.example.com
PROXY_IP=10.0.0.1
Run:
npm startOpen http://localhost:3000.
- Create sessions with live Halo data extraction
- Count by location (Stock site / Stock bin)
- Serialised items: per-asset verification by serial/inventory_number
- Non-serialised items: quantity entry
- Capture additional/unexpected serials during counting
- Refresh individual items from Halo (syncs serials + stock locations, prunes gone serials, adds new expected ones, downgrades items when all serials are
Unknown-*placeholders) - Edit mode: add/remove items, lock counted column when idle
- IP allowlist (DNS-resolved, X-Forwarded-For aware behind HAProxy)
- Dense client-side report: all counted items with cost / price / expected / counted / variance / counted$ / variance$ / reason
- Variance table with reasons + cost impact totals
- Negative values colour-coded red
- Server-generated A4 PDF (pdfkit), accountant-ready
- PDFs cached to
data/reports/, regenerable on demand, deletable from UI
Three modes:
- Purchase Order — enter PO ref (e.g.
P50128-1), generates one label per received unit - Product Lookup — typeahead across all Halo items, select to pull in-stock instances
- Asset / Serial — search by asset tag or serial number, single label
Per label:
- Item name (emoji stripped) + sell price (ex-GST × 1.1 for inc-GST)
- Description, SKU (
qbosku), Halo Product ID - Barcode: serialised items use
inventory_number→key_field→ UPC fallback; non-serialised items use UPC/EAN → SKU → item ID - Auto symbology: 12-digit → UPC-A, 13-digit → EAN-13, else Code128
GET /api/stocktakes— list allGET /api/stocktake/:id— full session (lazy report upgrade)POST /api/start-stocktake-creation— async creation with progress pollingGET /api/stocktake-progress/:creationIdPOST /api/create-stocktake— sync creationPOST /api/update-quantityPOST /api/update-serialPOST /api/add-serialPOST /api/update-variance-reasonPOST /api/reopen-stocktakePOST /api/refresh-halo— re-pull selected items, reconcile countedDataPOST /api/stocktake/:id/add-item— add Halo item to active stocktakePOST /api/stocktake/:id/remove-itemPOST /api/complete-stocktakeDELETE /api/stocktake/:id
GET /api/stocktake/:id/report-pdf— generate + cache A4 PDFGET /api/reports— list cached PDFsGET /api/reports/:filename— serve cached PDFDELETE /api/reports/:filename— remove cached PDF
GET /api/po/search?q=— PO searchGET /api/po/:id— PO with pre-expanded labelsGET /api/products— list all items for typeaheadGET /api/products/:id/instances— in-stock instances + pre-expanded labelsGET /api/asset-lookup?q=— asset/serial lookupPOST /api/labels/generate— generate Dymo 99010 PDF
server.js— Express server + routes + IP allowlistlib/halo-api.js— Halo OAuth2 client (token auto-refresh), PO/item/asset extraction, label-shape mappinglib/stocktake-manager.js— session persistence, counting, reconciliation, differential reportlib/label-generator.js— pdfkit + bwip-js, Dymo 99010 layoutlib/report-generator.js— pdfkit A4 report with dense tables
public/index.html— single-page app, vanilla JS, tabs: Dashboard / New / Active / Reports / Labels
- File-based JSON in
data/ data/index.json— stocktake indexdata/stocktake-*.json— sessionsdata/reports/— cached PDFs
- Halo's
supplier_part_codefield often contains garbage; real UPC lives inqbosku—pickUPC()validates both - Halo assets may have no
inventory_number; barcode helper falls back tokey_field(real serial) → UPC →A<id> - PO line
price/basepriceis purchase cost; sell price must be fetched from item details (item.baseprice) - Refresh logic syncs stockLocations to countedData so updates don't 404 after stock moves
Unknown-*placeholder serials (assets with no key_field/inventory_number) trigger downgrade to non-serialised when all serials are placeholders
Production (this repo runs under systemd):
[Service]
WorkingDirectory=/root/halo_stocktake
ExecStart=/usr/bin/node server.js
EnvironmentFile=/root/halo_stocktake/.env
Restart=on-failureReverse proxy: HAProxy → Express. The allowlist middleware trusts X-Forwarded-For only when the TCP source is PROXY_IP.
Internal — Elliotts Tech.