Back-office surfaces

The back office (/manage) exposes seven surfaces around stock and supplier ordering. They all read the same tables (Product.stockLedger, StockAction, SupplierOrder), but they do not all write — and when they do, every movement goes through the canonical write paths (recordPhysicalMovement, manageStock checked, a StockAction emitted in the same transaction). This page tells you which screen to open for which need, and what each button actually does in the database.

Which screen do I need?

flowchart TD
  Q0{"What do you want to do?"}
  Q0 --> A["Fix a stock figure"]
  Q0 --> B["Understand a stock figure"]
  Q0 --> C["Drive replenishment"]
  A --> A1{"How big?"}
  A1 -->|"A few products"| S1["vendors/stocks<br/>inline editing"]
  A1 -->|"Warehouse count"| S2["inventory/counts<br/>physical count"]
  B --> B1{"Which angle?"}
  B1 -->|"Line-by-line journal"| S3["inventory/stock-actions"]
  B1 -->|"Timeline + KPIs"| S4["inventory/stock-ledger"]
  C --> C1{"What exactly?"}
  C1 -->|"What will run out?"| S5["inventory/mon-stock"]
  C1 -->|"Review supplier emails"| S6["vendors/orders/email-review"]
  C1 -->|"Suspiciously inflated stock"| S7["vendors/orders/phantom-stock"]

And if the question is "is everything OK?": /manage/inventory/health (a read-only dashboard) plus the cron-5 health chip on the stock-ledger screen.

At a glance

Screen Route Reads Writes Key permission
Vendor stocks /manage/ecommerce/vendors/stocks product × vendor grid, valuation inline stock, bulk import, snapshots vendors:read + stock:write
Stock ledger /manage/ecommerce/inventory/stock-ledger KPIs, product timeline, divergences manual movements, reconcile, confirmations stock:read / stock:write
My stock /manage/ecommerce/inventory/mon-stock 30-day "will run out" projection supplier-draft deletion, ad-hoc order stock:read + vendors:write (writes)
Physical counts /manage/ecommerce/inventory/counts counting sessions adjustments at finalize stock:write
Stock actions /manage/ecommerce/inventory/stock-actions the full StockAction journal delete with/without stock effect, bulk stock:read / stock:write
Supplier orders /manage/ecommerce/vendors/orders calendar, stats, email queue send/defer/cancel emails, trust actions vendors:*, stock:write
Inventory health /manage/inventory/health alerts + indicators — (read-only) stock:read

Vendor stocks — /manage/ecommerce/vendors/stocks

The workhorse grid: every product with its default vendor, current stock, on-order and draft quantities. This is the spot-correction screen.

Reads: vendors.getVendorsStocks (vendor filter via autocomplete). The query also triggers the server-side stock valuation (#2796) — total value in euros at wholesale price, with a missingPriceCount counter for in-stock products missing a purchase price (and therefore unvalued).

Writes:

Action TRPC procedure Permission Ledger effect
Inline stock edit vendors.updateProductStock stock:write COMPLETED StockAction, source MANUAL (INCREMENT/DECREMENT/ADJUSTMENT per delta), optional reason
Bulk CSV/XLSX import vendors.bulkUpdateProductStock inventory:write one transaction, one SA per changed product
Inventory snapshot vendors.createStockSnapshot / deleteStockSnapshot stock:write freezes a dated valuation (items + value), no stock movement

The export dialog produces a CSV/XLSX of the current state; the import dialog feeds it back. Snapshots (#2796) keep a dated valuation history, with a per-vendor breakdown derived from the frozen items — useful for month-end accounting.

Tip

Inline editing validates server-side (integer ≥ 0) and does not reindex the product search engine — it is built for fast, repeated corrections. Enter = save, Escape = cancel.

Stock ledger — /manage/ecommerce/inventory/stock-ledger

The densest screen: the ledger's truth made readable. Four areas:

  1. KPI cards — Physical stock (with a backorder subtitle when the signed ledger is negative), Reserved, Available, Pending outflows (undelivered orders).
  2. Overview table (stockActions.getProductStockOverview) — one product per row, URL-persistent filters: category, vendor, critical-only, health, divergences only (stock ≠ stockLedger), untracked products in/out, sorting, date window (default today → +14 days).
  3. Product timeline (stockActions.getStockLedger) — the line-by-line movie of a selected product's inflows/outflows, with detail modals for customer orders and supplier orders.
  4. Cron-5 health chip — keeps liveness and activity apart. Its headline is liveness ("did the cron run?"), read from the heartbeat the task stamps in bext-KV at the end of every run (health:auto-validate:{tenantId}:{siteId}, 7-day TTL): ≤ 5 h green, ≤ 12 h amber, > 12 h red — against the expected cadence (12:01 canonical + a 2-hourly sweep via /etc/cron.d). The timestamps of the last auto-validation (supplier-reception ⇣ and client-delivery ⇡ sides) are shown as secondary info, "last activity" — a quiet day legitimately produces no row, so seal age alone never means the cron is dead. If bext is unreachable, the chip falls back to the activity-age status and says so in the popover. See Safety nets.

Writes (three modals, all stock:write — except the reconcile preview previewLedgerReconciliation, which is stock:read; only its apply applyLedgerReconciliation writes):

Modal Procedure What happens
Manual movement stockActions.createManualStockMovement physical movement via recordPhysicalMovement — SA emitted, Product.stock mirror recomputed
Ledger reconcile previewLedgerReconciliation (preview) then applyLedgerReconciliation lists every product where Product.stock ≠ Product.stockLedger; each "fix" emits a COMPLETED INVENTORY_ADJUSTMENT SA of (stock − ledger) to realign while preserving the stockLedger == Σ SA invariant
Pending confirmations stockActions.confirmStockAction moves a PENDING SA (supplier-delivery projection) to COMPLETED — applies the movement
Warning

The reconcile modal is a divergence-repair tool, not an editing shortcut. A dry-run preview always shows first, and you can heal only a selection. If divergences keep reappearing, some write site is bypassing the contract — see Write paths.

My stock — /manage/ecommerce/inventory/mon-stock

The merchant's daily view: "What will run out, and what's on the way?". A rolling 30-day projection over the same source as the ledger overview (stockActions.getProductStockOverview), presented as product cards with:

  • the uncovered demand panel (customer orders with no supplier order to match);
  • group by vendor (?group=true, deep-linkable);
  • ad-hoc supplier-order creation straight from a card (URL-driven modals: ?newOrder=1 for a blank composer, ?orderVendor=<id> + ?orderSlot=yyyy-MM-dd to target a vendor/slot, #2832).

Writes very little: deleting a supplier draft (vendors.deleteSupplierOrder) from the demand panel, and creating orders through the modal — which goes through the same guarded chain as event-driven ordering. No direct stock movement.

This is the screen to hand a non-technical operator: they read the projection, they never touch the ledger.

Physical counts — /manage/ecommerce/inventory/counts

The warehouse counting flow, in three phases:

inventoryCount.create — choose the scope (product search; items can be added/removed until the session is finalized, via addItems / removeItem). The session is a draft: nothing is written to stock.

inventoryCount.setCountedQty — enter the actually-counted quantity for each row. You can stop, resume, correct. Still no stock write.

inventoryCount.finalize (stock:write) — the only moment that writes. For each row the delta is computed against the signed ledger (Product.stockLedger, never against the clamped mirror — rule 17) and an INCREMENT/DECREMENT movement is emitted per the variance sign. manageStock=false products are skipped entirely (no write, no SA). A zero variance with a diverged mirror repairs the mirror without emitting an SA. Finalizing also stamps a dated valuation snapshot.

Note

Under backorder (STOCK_LEDGER_ALLOW_BACKORDER=true, in production since 2026-07-09) a product can sit at stockLedger = −3 with stock = 0. Counting "5" on that product creates a movement of +8, not +5 — the signed ledger is the authority. This is the expected behavior.

inventoryCount.cancel abandons an unfinalized session with no stock trace.

Stock actions — /manage/ecommerce/inventory/stock-actions

The journal. Each row is a StockAction: type (INCREMENT/DECREMENT/ADJUSTMENT), source (MANUAL, ORDER_UPDATE, SUPPLIER_DELIVERY, INVENTORY_ADJUSTMENT…), status (PENDING/COMPLETED/CANCELLED), before/delta/after quantities, author, reason. Read via stockActions.getAll (stock:read), with filters and per-row detail.

Note

This is the canonical journal page. The old /manage/dashboard/stock-actions route redirects here (2026-07-11 consolidation), preserving URL parameters.

Writes — deletion only, and this is the tricky part:

  • Two-way delete (stockActions.deleteWithOptionalStockEffect, stock:write): the dialog explicitly asks whether Product.stock/stockLedger should be adjusted back (the movement is physically reversed) or whether only the journal row is removed (stock stays as is). There is no generic confirm — this choice is the confirmation.
  • Bulk delete (stockActions.bulkDelete): a single server round-trip that processes the ids in order, server-side — two stock-affecting deletes on the same product must never race.
Warning

Deleting with stock effect is a physical reversal: it goes through the same write paths and, under backorder, restores stock = max(0, reversed ledger). Deleting without stock effect deliberately breaks the journal↔counter link — reserve it for cleaning rows already known to be wrong (import duplicates, etc.), otherwise the I5 audit will flag the drift.

Supplier orders — /manage/ecommerce/vendors/orders

The replenishment hub. Tabs: orders, history, per-site view, per-vendor view, and a calendar (VendorOrderCalendarView, with drag-and-drop of orders between slots). Header stats via vendors.getSupplierOrdersStats.

Email review queue — /manage/ecommerce/vendors/orders/email-review

The "what you reviewed is what gets sent" guarantee of the stage → review → send pipeline. Lists vendors.listStagedSupplierOrders (filters: vendor, dates, today-only, deferred, modified, group-by-vendor); each row shows the effective values (merged overrides) and a drift chip when the items changed since staging.

Action Procedure Effect
Send vendors.sendStagedSupplierOrderEmail re-renders from current items, throws CONFLICT if the items fingerprint drifted since staging ("Re-stage then resend")
Re-stage vendors.stageSupplierOrderEmail regenerates the preview + fingerprint
Defer / un-defer vendors.deferSupplierOrderEmail shifts deferUntil
Cancel the order vendors.cancelSupplierOrderFromReview cancels + tags provenance; a CANCELLED order will never be emailed

Phantom stock — /manage/ecommerce/vendors/orders/phantom-stock

Hunting inflated stock (typically fictional stock inherited from the online storefront — see PrestaShop sync). Reads vendors.getStockCoveragePage (products:read) with server-side search (350 ms debounce) and a "suspects only" filter; the trusted column shows what the ordering engine will actually use — capped by the 8 weeks of coverage when the raw stock looks phantom.

Two trust actions (stock:write):

  • vendors.setProductStock — fix the figure (physical movement, SA emitted);
  • vendors.setProductStockTrusted — flip the trust flag without touching the figure (the engine ignores or re-uses the raw stock).

Work through this before switching to more stock-driven ordering: every unresolved suspect translates into under-ordering.

Inventory health — /manage/inventory/health

A read-only dashboard (inventory.getDashboardData): stock alerts, aggregated indicators. No write action — it is the visual companion to the safety nets (the stock-ledger-audit task, the daily 06:30 probe, the cron-5 chip on the ledger screen).

Permissions

Every procedure is RBAC-protected (permissionProtectedProcedure):

Permission Unlocks
stock:read journal, ledger, overviews, my-stock, health
stock:write inline editing, manual movements, reconcile, confirmations, count finalize, journal deletions, snapshots, phantom-stock trust actions
vendors:read / vendors:write vendor stocks grid, supplier orders, email queue
inventory:write bulk stock import (bulkUpdateProductStock)
products:read reading the phantom-stock page

A read-only "daily operator" profile lives happily on stock:read + vendors:read plus access to my-stock and the email queue; the write actions on my-stock (deleting a supplier draft, creating an ad-hoc order) do require vendors:write, though. Keep stock:write for people who understand the difference between deleting a journal row with and without stock effect.

See also