Update doc for Shop PO to Invoice operations flow

This commit is contained in:
2026-10-21 13:06:06 +05:30
parent 50d3113c28
commit d79c5e55d0
@@ -0,0 +1,123 @@
# Shop PO → Invoice → Operations — Actual Flow (as implemented)
**Status**: Reference — describes current code behavior, not a plan
**Related docs**: 03_ride_lifecycle.md, 04_loading_event.md, 05_delivery_ride.md, 06_sale_ride.md (original specs — some details below diverge from those specs; this doc reflects what the code actually does today)
---
## Goal
Trace the end-to-end lifecycle of a customer order from the moment a Shop PO is raised through
billing (Invoice) and physical fulfillment (Ride → Loading Sheet → Delivery Task), including every
status transition and stock movement along the way.
---
## 1. Shop PO (raised by a sales agent, on behalf of a store)
**Pages**: `/sales` (list), `/sales/new` (create), `/sales/[id]` (detail)
**Backend**: `Features/ShopPO`
- **Create** (`ShopPOService.CreateAsync`): agent picks a store + items. Unit price resolves from
the active `PriceList` for that store's region, falling back to `Item.SalePrice` if none is
active. Status starts `Pending`.
- **Confirm** (`ConfirmAsync`): sets `ConfirmedAt`/`ConfirmedBy`. Status **stays `Pending`** —
confirming does not change `Status`, it only marks the PO as ready to act on (load/invoice).
- **Cancel**: only allowed while `Status == Pending` (not `PartiallyFulfilled`/`Fulfilled`).
- A confirmed PO with no invoice yet (`ConfirmedAt != null && InvoiceId == null`) is the exact set
`GetConfirmedWithoutInvoiceAsync` and the Loading Sheet picker (`GET /shop-pos?confirmed=true`)
query for.
## 2. Invoice (billing document, one per Shop PO)
**Page**: `/sales/orders` (labeled "Orders" in the nav, but it is actually the Invoices list)
**Backend**: `Features/Invoice`
- Created via `ShopPOService.ConfirmAndCreateInvoiceAsync` — confirms the PO **and** creates an
`Invoice` (`Status: Draft`, `PaymentStatus: Pending`) in one step, linking `ShopPO.InvoiceId`.
The frontend calls it when **Configuration → Invoice Creation** is set to **Auto** (Mode A, the
confirm dialog and PO edit page read the mode); with **Manual** (Mode B, the default) it calls the
plain `Confirm` and the invoice is created later from `/sales/orders` → **Create Invoice**.
- Lifecycle: `Draft → Printed → Finalized`.
- The invoice has its own lines (`InvoiceItem`), copied from the PO's active lines when it is
created (either mode). The PO's original quantity stays on `ShopPOItem` and is shown as
`orderedQuantity`.
- While `Draft`, lines can be edited (`PUT /invoices/{id}/items`): change quantity (`0..ordered`
on PO lines) and price (`>= 0`), **delete** a line (it is kept, struck through, excluded from
the total, and can be restored), or **add** a catalog item that was not on the PO (no ordered
quantity / max). `TotalAmount` is recomputed from the non-deleted lines.
- Finalizing applies the agent's edits from the printed copy to the lines (quantity capped
between `0` and the ordered quantity, or a line removed) and recomputes `TotalAmount`.
- Every change is snapshotted as an `InvoiceVersion` — see
[refactor_docs/order_invoice_lifecycle.md](refactor_docs/order_invoice_lifecycle.md#invoice-line-items--version-history).
- Frontend, `/sales/orders` row icons: **eye** = invoice preview, **pencil** (Draft) = open the edit page for the
draft invoice (`/sales/invoices/[id]/edit`: trash/undo icon per line, add-item row), **printer** (Draft) =
mark as printed and open the print tab; on Printed/Finalized the printer reprints without
changing status; plus finalize and record payment. `/sales/invoices/[id]` shows the items table
(ordered vs billed, Added/Removed badges), the version history, and the same actions.
- Invoice document and print tab: see
[refactor_docs/order_invoice_lifecycle.md](refactor_docs/order_invoice_lifecycle.md#invoice-screens--printing-frontend).
- Payments (`payments` API) apply against the invoice, moving `PaymentStatus`:
`Pending → PartiallyPaid → Paid`.
## 3. Operations: Ride → Loading Sheet → Confirm → Delivery Tasks
**Pages**: `/operations/rides`, `/operations/loading`, `/operations/loading/new`
**Backend**: `Features/Ride`, `Features/LoadingEvent`, `Features/DeliveryRide`
1. **Ride** created with `Status: Open`. A Ride is just a Vehicle + Driver pairing — it has **no**
Route field in the current data model (unlike the original 03/06 specs' "ride type" framing).
2. **Loading Sheet** created against an Open ride (`/operations/loading/new`):
- Pick a **Route** (required, first) → confirmed Shop POs for stores on that route load
(`ConfirmedAt != null`, status `Pending`/`PartiallyFulfilled`, remaining quantity > 0).
- Optionally narrow by **Shop** (store) within that route.
- Pick items/quantities to load, a **Warehouse** (single warehouse per sheet), and finally the
**Ride** to load onto.
- Sheet starts `Status: Planned`.
3. **Set items** (`PUT /loading-sheets/{id}/items`) — editable only while `Planned`. Behavior is
**additive/upsert**, keyed by `(FK_ItemId, FK_WarehouseId, FK_ShopPOItemId)`: a matching row's
`QuantityDispatched` is updated in place, a new combination is added, and rows for combinations
not present in the request are left untouched. This lets a shortfall in one warehouse be topped
up from a second warehouse in a later save without losing the first line.
4. **Confirm sheet** (`LoadingSheetService.ConfirmAsync`): for every line, deducts stock from its
Warehouse and credits it to the Ride's Vehicle (`StockMovement.Type = Load`,
`Warehouse → Vehicle`). Sheet → `Status: Confirmed`. This is the only point stock actually
leaves the warehouse. (Note: unlike the 04_loading_event.md spec, confirming a sheet does **not**
transition the Ride's own status — `RideStatus` only has `Open`/`Cancelled`/`Closed`, no
`Active`.)
5. **Generate delivery tasks** (`DeliveryTaskService.GenerateAsync`): once a ride has at least one
`Confirmed` loading sheet, one `DeliveryTask` (`Status: Pending`) is created per distinct Shop PO
that sheet's `LoadingSheetOrder` rows reference.
6. **Set quantities** on each task (`SetQuantitiesAsync`) — how much of each `ShopPOItem` was
actually delivered, capped at the item's ordered quantity. Only allowed while the task is
`Pending`.
7. **Confirm task** (`DeliveryTaskService.ConfirmAsync`) — closes the loop back to the Shop PO:
- Moves stock **off the vehicle** (`StockMovement.Type = Sale`, `Vehicle → External`) for
whatever was fulfilled, failing if the vehicle doesn't actually have enough stock.
- Task → `Status: Delivered` (all items fully fulfilled) or `PartiallyDelivered`.
- Writes back to the Shop PO: `Status = Fulfilled`/`PartiallyFulfilled`, `FulfilledAt` set, and
each `ShopPOItem.QuantityFulfilled` bumped. This is exactly the field the Loading Sheet
picker's "remaining quantity" (`quantity - quantityFulfilled`) depends on — a fully fulfilled
PO drops out of the picker entirely.
8. **Fail task**: marks `Status: Failed`. No stock movement, no Shop PO status change.
---
## Stock ledger across the whole flow
```
Warehouse --(Loading Sheet confirm, Type=Load)--> Vehicle --(Delivery Task confirm, Type=Sale)--> External/Customer
```
Every step writes a `StockMovement` row, so `StockBalance` for each Warehouse/Vehicle stays
reconciled from Shop PO creation all the way through delivery.
---
## Known gaps at time of writing
- No frontend UI calls `ConfirmAndCreateInvoiceAsync` — only the plain `Confirm` action (no
invoice) is wired up on the Shop PO screens.
- `ShopPOController`/`LoadingSheetController` Create/Confirm endpoints, like most other
create/confirm endpoints in this codebase, rely on the TEMPORARY `currentUser.UserId ?? request.XxxUserId`
fallback pattern documented in `testbug.md` (#28) until the real auth middleware is fixed.