Purchases module
The purchasing console. PO ledger + Purchase Cart (vendor-grouped, per-line steppers) + Vendors management + PO lifecycle (Choose Vendor → New PO → Order Card → issue → receive). Split out of the former two-face Products module in v0.7.69; the Inventory side became its own Inventory module. Rebuilt to prior-implementation POs-tab parity in v0.7.66.
| Tab label | Purchases |
| Tab accent | #0d9488 (teal) |
| Display order | 20 |
| Folder | src/modules/purchases/ |
| Migrations | 020_purchasing.sql, 021_po_line_variant_nullable.sql, 023_purchasing_view_perm.sql, 025_po_status_lifecycle.sql, 026_bucket_line_cost.sql, 033_special_order_po_link.sql |
What it owns
src/modules/purchases/
index.ts — registers the module with the Kernel
view.ts — markup + styles for the PO ledger + Purchase Cart + Vendors
client.ts — browser JS (Cart, PO lifecycle, Vendor popup)
api.ts — server-side routes (/api/m/purchases/*)
What it does
The Purchase Cart (Bucket)
Staging area for reorders. Variants drop into the Cart from Inventory's Reorder Dashboard, from a Stock-Card + Add to PO, from a Special-Order deposit (v0.7.86 auto-post), or from the Cart's own SKU search. Grouped by vendor (the variant's is_primary_supplier decides the group); each vendor group accumulates until built into a PO.
Inline line controls. Each Cart line has qty stepper, unit cost, vendor reassignment, and a cost-override that survives the PO build (migration 026 stores cost_override_cents on order_queue).
Create a SKU from the Cart (BIKE.L2-0049, v0.7.70). The Cart's Add-a-product search gains a + New SKU button that opens the create-product modal; the new SKU is then searchable + addable to the Cart.
Clear bucket (BIKE.L2-0050). Empty all staged Cart lines across every vendor at once, kbConfirm-gated.
PO lifecycle
Build & issue. Click a vendor's Cart group → opens a draft PO with that supplier's lines pre-loaded; adjust quantities, costs, notes; Issue stamps status='ordered' (aligned to the prior implementation's vocabulary in v0.7.39), writes the timestamp, drops the rows from order_queue.
PO status vocabulary (migration 025):
| Status | Meaning |
|---|---|
pending_order | Editable draft; Order Card writes accepted |
ordered | Issued; receive controls live |
received | Every line fully received |
closed | Manually closed |
cancelled | Voided |
The PO ledger. Lists POs by status with colour-coded chips + a column-sort + a vendor filter chip (v0.7.42). Clicking a row loads the Order Card.
Editable Order Card (BIKE.L2-0042). Pending POs are editable from the Order Card itself — change a line's quantity or unit cost, add a new product to the PO, remove a line. Totals recompute server-side on every save so the displayed total can never drift from the line sum. Once a PO transitions to ordered, the Order Card flips to read-only with receive controls on each line.
New PO from scratch (BIKE.L2-0041 / 0048). Build a PO without going through the Cart: pick a vendor → empty PO opens → line editor walks through adding variants by SKU search or barcode.
Receiving depth (BIKE.L2-0044 / 0083)
When stock arrives against an issued PO, Receive supports the realistic edge cases:
- Cost override on receive. Vendor's invoice came in different from the PO line cost? The receive line carries an overridden actual cost; the variant's cost-of-goods picks it up.
- Cost ladder. Successive receipts on the same variant build a ladder of supplier costs visible on the Vendor popup + the Inventory variant detail.
supplier_productscost stamp. Each receipt stamps the receiving cost ontosupplier_productsso the next "default cost" for that variant from that vendor reflects the most-recent reality.- Default-0 + over-receipt. A line can default-receive 0 (mark received-but-skipped) or over-receive (vendor sent more than ordered); both write movement rows of the appropriate type.
- Discrepancy roll-up. The Order Card summarises ordered-vs-received counts across all lines.
Receipts fire movement_type='receive' rows through core/movements.ts — quantity_on_hand updates, the Bucket → PO → on-hand loop closes.
Vendors management
The Vendors section on this tab lists every supplier. Each row opens the Vendor popup — rich vendor record (BIKE.L2-0051): account #, contact address, website, B2B portal URL, drop-ship-capable flag, logo, display_color_hex for Cart group-header colour. Edits go through PUT /api/m/purchases/suppliers/:id; the Order Card chrome reads the logo + display colour for visual grouping.
Special orders → PO Bucket link (migration 033, v0.7.86)
A Sales special-order deposit auto-posts a line to the Purchases Cart the moment the deposit is taken. Once the Cart line becomes a PO line, migration 033's link table binds the Sales transaction → Bucket line → PO line. The Sales ledger row for that special order then tracks live PO status: Special order → Ordered → Partially received → Ready for pickup. One PO line links to exactly one special-order transaction; one PO can carry lines for several different customers' special orders at once (per-line link, not per-PO).
Permissions
| Permission | What it gates |
|---|---|
screen.purchases | The Purchases tab is visible at all |
purchasing.view | Read the ledger + Order Card + Cart |
purchasing.edit | Cart actions + draft / issue / receive POs · Vendor edits |
See also
- Three strata — Kernel · Meta · Modules — the architectural shape
- Inventory module — the stock console that used to be Face A
- Sales module — special-order deposits auto-post here (v0.7.86)
- Service module — Wait-for-Parts tickets often link through Sales special-orders to this module
- Keep-mounted SPA navigation — a half-built PO survives a tab switch to look something up in Inventory