Skip to main content

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 labelPurchases
Tab accent#0d9488 (teal)
Display order20
Foldersrc/modules/purchases/
Migrations020_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):

StatusMeaning
pending_orderEditable draft; Order Card writes accepted
orderedIssued; receive controls live
receivedEvery line fully received
closedManually closed
cancelledVoided

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_products cost stamp. Each receipt stamps the receiving cost onto supplier_products so 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.tsquantity_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.

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

PermissionWhat it gates
screen.purchasesThe Purchases tab is visible at all
purchasing.viewRead the ledger + Order Card + Cart
purchasing.editCart actions + draft / issue / receive POs · Vendor edits

See also