|
|
|
@@ -13,7 +13,7 @@ Replace the previous loading event design with a dispatcher-driven, multi-PO mod
|
|
|
|
|
1. **No more auto-creation.** Loading sheets are no longer generated automatically when a PO is
|
|
|
|
|
confirmed. A dispatcher manually triggers population.
|
|
|
|
|
2. **No more `Open → Active`.** `Ride.Status` collapses to `Open`/`Cancelled`/`Closed` — no
|
|
|
|
|
`Active` stage. Loading sheet confirmation no longer changes `Ride.Status` at all.
|
|
|
|
|
`Active` stage. Loading sheet confirmation no longer changes `Ride.Status`.
|
|
|
|
|
3. **One sheet aggregates multiple POs.** A single loading sheet is created from all Confirmed POs
|
|
|
|
|
matching a dispatcher's filter (route/agent/shop). No longer one sheet per PO.
|
|
|
|
|
4. **Ride set at populate time.** The ride is provided when the dispatcher triggers population —
|
|
|
|
@@ -22,15 +22,17 @@ Replace the previous loading event design with a dispatcher-driven, multi-PO mod
|
|
|
|
|
loaded from different warehouses. Stock movements are generated per item's warehouse.
|
|
|
|
|
6. **Item versioning.** Loading sheet items are versioned the same way as Shop PO items — every
|
|
|
|
|
edit soft-deletes current rows (`IsActive = false`) and writes a fresh set.
|
|
|
|
|
7. **Stock moves at confirmation.** When the dispatcher confirms the sheet, stock moves from each
|
|
|
|
|
item's warehouse to the vehicle. No driver receipt confirmation step.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Background / Context
|
|
|
|
|
|
|
|
|
|
Same physical mechanics as `04_loading_event.md`: a vehicle is a location with
|
|
|
|
|
`LocationType.Vehicle` in the stock ledger. Confirming receipt (or resolving a discrepancy)
|
|
|
|
|
creates stock movements — debit source warehouse(s), credit vehicle. What changes is *when and how
|
|
|
|
|
a `LoadingSheet` comes into existence*, *which POs it covers*, and *how warehouse selection works*.
|
|
|
|
|
`LocationType.Vehicle` in the stock ledger. Confirming the loading sheet creates stock movements —
|
|
|
|
|
debit source warehouse(s), credit vehicle. What changes is *when and how a `LoadingSheet` comes
|
|
|
|
|
into existence*, *which POs it covers*, and *how warehouse selection works*.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
@@ -43,18 +45,12 @@ a `LoadingSheet` comes into existence*, *which POs it covers*, and *how warehous
|
|
|
|
|
| Id | Guid | PK |
|
|
|
|
|
| ~~`FK_OrderId`~~ | — | **Removed.** One sheet now aggregates multiple POs — replaced by `LoadingSheetOrder` linking table. |
|
|
|
|
|
| **`FK_RideId`** | Guid | **Required. Set at population time** — dispatcher provides the ride when triggering populate. |
|
|
|
|
|
| ~~`FK_SourceWarehouseId`~~ | — | **Removed.** Warehouse is tracked per item, not per sheet — a single sheet can be fulfilled from multiple warehouses. |
|
|
|
|
|
| Status | LoadingSheetStatus | See state machine below. |
|
|
|
|
|
| ~~`FK_SourceWarehouseId`~~ | — | **Removed.** Warehouse is tracked per item, not per sheet. |
|
|
|
|
|
| Status | LoadingSheetStatus | `Planned` or `Confirmed`. |
|
|
|
|
|
| **`PopulatedBy`** | Guid | **New.** UserId of dispatcher who triggered population. |
|
|
|
|
|
| **`PopulatedAt`** | DateTime | **New.** When population was triggered. |
|
|
|
|
|
| ~~`AssignedAt`~~ | — | **Removed.** No separate assign step. |
|
|
|
|
|
| ~~`AssignedBy`~~ | — | **Removed.** Same reason. |
|
|
|
|
|
| ConfirmedAt | DateTime? | Set when loading sheet is confirmed (items locked). |
|
|
|
|
|
| ConfirmedBy | Guid? | UserId of whoever confirmed the sheet. |
|
|
|
|
|
| DispatchConfirmedAt | DateTime? | Path B only — see Open Questions. |
|
|
|
|
|
| DispatchConfirmedBy | Guid? | Path B only. |
|
|
|
|
|
| ReceiptConfirmedAt | DateTime? | Path B only. |
|
|
|
|
|
| ReceiptConfirmedBy | Guid? | Path B only. |
|
|
|
|
|
| ConfirmedAt | DateTime? | Set when loading sheet is confirmed (items locked, stock moves). |
|
|
|
|
|
| ConfirmedBy | Guid? | UserId of whoever confirmed the sheet. TBD — see Open Questions. |
|
|
|
|
|
| CreatedAt | DateTime | From Template. |
|
|
|
|
|
|
|
|
|
|
### New Entity — `LoadingSheetOrder` (linking table)
|
|
|
|
@@ -76,8 +72,6 @@ One loading sheet aggregates multiple Confirmed POs. This table records which PO
|
|
|
|
|
| **`FK_OrderItemId`** | Guid? | **New.** Traces back to the source `OrderItem` from the Confirmed PO. Null for manually added lines. |
|
|
|
|
|
| FK_ItemId | Guid | FK → Item (product) |
|
|
|
|
|
| QuantityDispatched | decimal | Quantity to load from `FK_WarehouseId` |
|
|
|
|
|
| QuantityReceived | decimal? | Driver's received qty (Path B only) |
|
|
|
|
|
| QuantityResolved | decimal? | Supervisor's resolved qty after discrepancy (Path B only) |
|
|
|
|
|
| **`IsActive`** | bool | **Versioning.** Every edit soft-deletes current items (`IsActive = false`) and writes a fresh set. Active items = latest version; soft-deleted = history. |
|
|
|
|
|
| CreatedAt | DateTime | From Template |
|
|
|
|
|
|
|
|
|
@@ -86,36 +80,30 @@ One loading sheet aggregates multiple Confirmed POs. This table records which PO
|
|
|
|
|
```csharp
|
|
|
|
|
public enum LoadingSheetStatus
|
|
|
|
|
{
|
|
|
|
|
Planned, // populated by dispatcher (ride set, items editable/versioned)
|
|
|
|
|
Confirmed, // items locked — stock movement path TBD (see Open Questions)
|
|
|
|
|
DispatchConfirmed, // Path B only: warehouse confirms dispatch
|
|
|
|
|
ReceiptConfirmed, // Path B only: driver confirms receipt, stock moves
|
|
|
|
|
DiscrepancyFlagged, // Path B only: received qty mismatch
|
|
|
|
|
Resolved // Path B only: supervisor resolves discrepancy, stock moves
|
|
|
|
|
Planned, // populated by dispatcher — ride set, items editable and versioned
|
|
|
|
|
Confirmed // items locked, stock movements created
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`Pending` from `04_loading_event.md` is replaced by `Planned` and `Confirmed`. `Assigned` is
|
|
|
|
|
removed — ride is set at populate time, no separate assign step.
|
|
|
|
|
`Pending`, `Assigned`, `DispatchConfirmed`, `ReceiptConfirmed`, `DiscrepancyFlagged`, and
|
|
|
|
|
`Resolved` from `04_loading_event.md` are all removed. Stock moves at `Confirmed` — no driver
|
|
|
|
|
receipt confirmation step in this flow.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## State Machine
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Planned -> Confirmed -> [Path A: stock moves here]
|
|
|
|
|
\-> [Path B: DispatchConfirmed -> ReceiptConfirmed]
|
|
|
|
|
\-> DiscrepancyFlagged -> Resolved
|
|
|
|
|
Planned → Confirmed (stock moves)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Transition | Trigger | What it does |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| (create) → Planned | `POST /api/loading-sheets/populate` | Dispatcher provides `rideId` + filters. System finds all matching Confirmed POs, creates one `LoadingSheet` with `FK_RideId` set, populates `LoadingSheetItem` rows from active `OrderItem` lines, records PO links in `LoadingSheetOrder`. `PopulatedBy`/`PopulatedAt` set. Items editable (versioned via `IsActive`). Dispatcher sets `FK_WarehouseId` per item. |
|
|
|
|
|
| Planned → Confirmed | `POST /{id}/confirm` | Locks the sheet — `LoadingSheetItem` rows frozen. Sets `ConfirmedAt`/`ConfirmedBy`. Stock movement path TBD (see Open Questions). |
|
|
|
|
|
| Confirmed → DispatchConfirmed | `POST /{id}/confirm-dispatch` | **Path B only.** Warehouse manager confirms dispatch quantities. |
|
|
|
|
|
| DispatchConfirmed → ReceiptConfirmed | `POST /{id}/confirm-receipt` | **Path B only.** Driver's received quantities all match. Stock movements created — one debit per `LoadingSheetItem.FK_WarehouseId`, one credit to the ride's vehicle. No ride status transition. |
|
|
|
|
|
| DispatchConfirmed → DiscrepancyFlagged | `POST /{id}/confirm-receipt` | **Path B only.** Any line mismatch. No stock movements yet. |
|
|
|
|
|
| DiscrepancyFlagged → Resolved | `POST /{id}/resolve` | **Path B only.** Supervisor enters `QuantityResolved` per line. Stock movements created using resolved quantities. |
|
|
|
|
|
| Planned → Confirmed | `POST /{id}/confirm` | Locks the sheet — `LoadingSheetItem` rows frozen. Stock movements created: one debit per `LoadingSheetItem.FK_WarehouseId`, one credit to the ride's vehicle. Sets `ConfirmedAt`/`ConfirmedBy`. |
|
|
|
|
|
|
|
|
|
|
A `Planned` sheet stays as `Planned` indefinitely until the dispatcher confirms it — there is no
|
|
|
|
|
timeout or auto-cancellation.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
@@ -145,18 +133,14 @@ and writes a fresh set. Latest version = `IsActive = true` rows.
|
|
|
|
|
|
|
|
|
|
## Stock Movements
|
|
|
|
|
|
|
|
|
|
One stock movement pair per `LoadingSheetItem` (active rows only). Created exactly once.
|
|
|
|
|
Created at `Confirmed`. One pair per active `LoadingSheetItem`:
|
|
|
|
|
|
|
|
|
|
- **Debit**: `LoadingSheetItem.FK_WarehouseId` (the warehouse that item loads from)
|
|
|
|
|
- **Credit**: the vehicle on the assigned ride (`LoadingSheet.FK_RideId → DeliveryRun.FK_VehicleId`)
|
|
|
|
|
- **Credit**: vehicle on the assigned ride (`LoadingSheet.FK_RideId → DeliveryRun.FK_VehicleId`)
|
|
|
|
|
- **Type**: `Load`
|
|
|
|
|
|
|
|
|
|
Since different items can have different `FK_WarehouseId` values, one loading sheet can produce
|
|
|
|
|
stock movements from multiple warehouses into one vehicle.
|
|
|
|
|
|
|
|
|
|
**When** movements are created depends on the path chosen (see Open Questions):
|
|
|
|
|
- **Path A** — stock moves at `Confirmed`
|
|
|
|
|
- **Path B** — stock moves at `ReceiptConfirmed` or `Resolved`
|
|
|
|
|
stock movements from multiple warehouses into one vehicle in a single confirmation action.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
@@ -166,15 +150,11 @@ Base route: `/api/loading-sheets`
|
|
|
|
|
|
|
|
|
|
| Method | Route | Who | Description |
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
| POST | `/populate` | Dispatcher | Trigger population: `{ rideId, routeId?, agentId?, shopId? }` — `rideId` required, at least one filter required. Creates one `LoadingSheet` at `Planned` with `FK_RideId` set. |
|
|
|
|
|
| POST | `/populate` | Dispatcher | `{ rideId, routeId?, agentId?, shopId? }` — `rideId` required, at least one filter required. Creates one `LoadingSheet` at `Planned` with `FK_RideId` set. |
|
|
|
|
|
| GET | `/` | Any | List sheets. Filter by `routeId`, `agentId`, `shopId`, `rideId`, `status`. Paginated. `routeId` resolves via `Store.FK_RouteId → Order.FK_StoreId → LoadingSheetOrder.FK_OrderId` server-side. |
|
|
|
|
|
| GET | `/{id}` | Any | Get sheet detail. Active items only by default; `?history=true` includes soft-deleted versions. |
|
|
|
|
|
| PUT | `/{id}/items` | Dispatcher | Rewrite items while `Planned`. Each item must include `FK_WarehouseId`. Soft-deletes current rows (`IsActive = false`) and writes a fresh set. |
|
|
|
|
|
| POST | `/{id}/confirm` | Dispatcher | Lock the sheet. `Planned → Confirmed`. Items frozen. |
|
|
|
|
|
| POST | `/{id}/confirm-dispatch` | Warehouse mgr | **Path B only.** `Confirmed → DispatchConfirmed`. |
|
|
|
|
|
| PUT | `/{id}/receipt-items` | Driver | **Path B only.** Set `QuantityReceived` per line. |
|
|
|
|
|
| POST | `/{id}/confirm-receipt` | Driver | **Path B only.** `→ ReceiptConfirmed` or `DiscrepancyFlagged`. |
|
|
|
|
|
| POST | `/{id}/resolve` | Supervisor | **Path B only.** Enter `QuantityResolved` per line `→ Resolved`. |
|
|
|
|
|
| POST | `/{id}/confirm` | TBD | Lock the sheet. `Planned → Confirmed`. Items frozen. Stock movements created. |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
@@ -192,11 +172,12 @@ Base route: `/api/loading-sheets`
|
|
|
|
|
8. Items are editable (versioned via `IsActive`) while `Planned` — frozen at `Confirmed`.
|
|
|
|
|
9. Every item edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`) and writes
|
|
|
|
|
a fresh set. Active rows = latest version; soft-deleted = history.
|
|
|
|
|
10. Stock movements are created exactly once — one debit per item warehouse, one credit to the
|
|
|
|
|
vehicle. Timing depends on path chosen (see Open Questions).
|
|
|
|
|
11. No `Ride.Status` transition is triggered at any point in this flow.
|
|
|
|
|
12. All list endpoints return paginated responses.
|
|
|
|
|
13. Vehicle capacity check is not enforced in this step.
|
|
|
|
|
10. Stock movements are created at `Confirmed` — one debit per item's warehouse, one credit to the
|
|
|
|
|
vehicle — all in one transaction.
|
|
|
|
|
11. A `Planned` sheet stays `Planned` until confirmed — no timeout or auto-cancellation.
|
|
|
|
|
12. No `Ride.Status` transition is triggered at any point in this flow.
|
|
|
|
|
13. All list endpoints return paginated responses.
|
|
|
|
|
14. Vehicle capacity check is not enforced in this step.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
@@ -205,50 +186,49 @@ Base route: `/api/loading-sheets`
|
|
|
|
|
| Code | Who |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `loading.view` | All roles |
|
|
|
|
|
| **`loading.populate`** | **New.** Dispatcher — trigger population, edit items, confirm sheet |
|
|
|
|
|
| `loading.dispatch` | Warehouse manager — Path B dispatch confirmation |
|
|
|
|
|
| `loading.receive` | Driver — Path B receipt confirmation |
|
|
|
|
|
| `loading.resolve` | Supervisor — Path B discrepancy resolution |
|
|
|
|
|
| **`loading.populate`** | **New.** Dispatcher — trigger population, edit items |
|
|
|
|
|
| **`loading.confirm`** | **New.** TBD — who can confirm the sheet (see Open Questions) |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Fraud / Control Considerations
|
|
|
|
|
|
|
|
|
|
- Discrepancies are immediately visible and block further progress.
|
|
|
|
|
- `QuantityResolved` is permanently stored per line for audit purposes.
|
|
|
|
|
- `PopulatedBy`/`PopulatedAt` gives an audit trail for who triggered population and when.
|
|
|
|
|
- `ConfirmedBy`/`ConfirmedAt` records who committed stock movements.
|
|
|
|
|
- `FK_OrderItemId` on each item traces the exact source PO line for full audit.
|
|
|
|
|
- `IsActive = false` rows preserve the full edit history of the sheet before confirmation.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Open Questions
|
|
|
|
|
|
|
|
|
|
1. **Path A vs Path B — when do stock movements happen?** Stock can move at `Confirmed` (Path A)
|
|
|
|
|
or at `ReceiptConfirmed`/`Resolved` (Path B). Both paths retained until decided.
|
|
|
|
|
2. **Who confirms the loading sheet?** — the dispatcher who populated it, the warehouse manager,
|
|
|
|
|
or either?
|
|
|
|
|
3. **What happens to a `Planned` sheet if it is never confirmed?** — no cancellation or timeout
|
|
|
|
|
path is designed here.
|
|
|
|
|
4. Vehicle capacity check — still deferred.
|
|
|
|
|
1. **Who confirms the loading sheet?** — dispatcher, warehouse manager, or either? TBD. Controls
|
|
|
|
|
the `loading.confirm` permission assignment.
|
|
|
|
|
2. **What happens to a `Planned` sheet if its source PO is cancelled?** — if a Confirmed PO is
|
|
|
|
|
cancelled after a loading sheet has been populated from it, the sheet's items sourced from that
|
|
|
|
|
PO become invalid. No cascade behaviour designed here.
|
|
|
|
|
3. Vehicle capacity check — still deferred.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Migration Notes (for when this is implemented)
|
|
|
|
|
|
|
|
|
|
- Remove `FK_OrderId` and `FK_SourceWarehouseId` from `Models/LoadingSheet.cs`. Make `FK_RideId`
|
|
|
|
|
required (non-nullable). Remove `AssignedAt`/`AssignedBy`. Add `PopulatedBy`, `PopulatedAt`,
|
|
|
|
|
- Remove `FK_OrderId`, `FK_SourceWarehouseId`, `AssignedAt`, `AssignedBy`,
|
|
|
|
|
`DispatchConfirmedAt`, `DispatchConfirmedBy`, `ReceiptConfirmedAt`, `ReceiptConfirmedBy` from
|
|
|
|
|
`Models/LoadingSheet.cs`. Make `FK_RideId` required. Add `PopulatedBy`, `PopulatedAt`,
|
|
|
|
|
`ConfirmedAt`, `ConfirmedBy`.
|
|
|
|
|
- Add `Models/LoadingSheetOrder.cs` (`FK_LoadingSheetId`, `FK_OrderId`) with composite PK.
|
|
|
|
|
- Add `FK_WarehouseId` (required), `FK_OrderItemId` (nullable), `IsActive` to
|
|
|
|
|
`Models/LoadingSheetItem.cs`.
|
|
|
|
|
- Replace `LoadingSheetStatus.Pending` and `Assigned` with `Planned` and `Confirmed`; update all
|
|
|
|
|
status checks in `LoadingSheetService`/`LoadingSheetRepository`.
|
|
|
|
|
- Remove `POST /api/loading-sheets` (old manual create) and `POST /{id}/assign-ride`; add
|
|
|
|
|
`POST /api/loading-sheets/populate` and `POST /{id}/confirm`.
|
|
|
|
|
`Models/LoadingSheetItem.cs`. Remove `QuantityReceived`, `QuantityResolved`.
|
|
|
|
|
- Replace `LoadingSheetStatus` entirely with `Planned` and `Confirmed`; remove all other values
|
|
|
|
|
and update all status checks in `LoadingSheetService`/`LoadingSheetRepository`.
|
|
|
|
|
- Remove `POST /api/loading-sheets` (old manual create), `POST /{id}/assign-ride`,
|
|
|
|
|
`POST /{id}/confirm-dispatch`, `PUT /{id}/receipt-items`, `POST /{id}/confirm-receipt`,
|
|
|
|
|
`POST /{id}/resolve`; add `POST /api/loading-sheets/populate` and `POST /{id}/confirm`.
|
|
|
|
|
- Wire stock movements into `POST /{id}/confirm` — one debit per `LoadingSheetItem.FK_WarehouseId`,
|
|
|
|
|
one credit to the ride's vehicle, in a single transaction.
|
|
|
|
|
- Remove ride-route-based item population (`GetAggregatedOrderItemsByRouteIdAsync`); replace with
|
|
|
|
|
filter-based multi-PO aggregation in `LoadingSheetService`.
|
|
|
|
|
- Remove all calls to ride-activation logic from `confirm-receipt`/`resolve` — no `Ride.Status`
|
|
|
|
|
transition exists anymore.
|
|
|
|
|
- Seed the new `loading.populate` permission.
|
|
|
|
|
- Resolve Open Question 1 (stock movement timing) before implementing `POST /{id}/confirm` and
|
|
|
|
|
`POST /{id}/confirm-receipt` stock movement logic.
|
|
|
|
|
filter-based multi-PO aggregation.
|
|
|
|
|
- Remove all calls to ride-activation logic — no `Ride.Status` transition exists anymore.
|
|
|
|
|
- Seed `loading.populate` and `loading.confirm` permissions.
|
|
|
|
|
- Resolve Open Question 1 (who confirms) before assigning `loading.confirm` permission.
|
|
|
|
|