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.
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:
- KPI cards — Physical stock (with a backorder subtitle when the signed ledger is negative), Reserved, Available, Pending outflows (undelivered orders).
- 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). - 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. - 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 |
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=1for a blank composer,?orderVendor=<id>+?orderSlot=yyyy-MM-ddto 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.
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.
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 whetherProduct.stock/stockLedgershould 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.
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
- Overview — the whole model on one page
- The stock ledger — signed authority, clamped mirror, invariants
- Write paths — the contract every write button honors
- Supplier orders — event-driven ordering, slots, the write lock
- A day in the life — the 06:00 / 06:30 / 07:15 / 12:01 crons
- Safety nets — audit, probe, pinned tests
- Configuration — the gates (
STOCK_LEDGER_ALLOW_BACKORDER,STOCK_TRUST_MAX_COVER_WEEKS…)