Update doc for Shop PO to Invoice operations flow
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user