Update Po invoice flow.md file

This commit is contained in:
2026-10-21 13:04:44 +05:30
parent 34d33571de
commit 50d3113c28
@@ -82,6 +82,89 @@ Invoice
├── IsActive, CreatedAt — from Template ├── IsActive, CreatedAt — from Template
``` ```
### Invoice Line Items & Version History
> **Implemented.** Class/column names follow the code: the PO is `ShopPO`/`ShopPOItem`, and the
> invoice FK is `FK_ShopPOId` (not `FK_OrderId` as sketched above).
The invoice no longer bills straight from the PO. It owns its own lines, and every change to them is
snapshotted, so the original, printed and finalized quantities can all be compared.
```
InvoiceItem — the live billing lines
├── FK_InvoiceId, FK_ShopPOItemId, FK_ItemId
├── Quantity (int) — what is billed now (0..ShopPOItem.Quantity)
├── UnitPrice (decimal)
└── IsActive, CreatedAt — from Template
InvoiceVersion — immutable snapshot of the invoice
├── FK_InvoiceId, VersionNumber — unique per invoice, starts at 1
├── Stage (InvoiceVersionStage) — Created, DraftEdited, Printed, Finalized
├── TotalAmount, CreatedBy
└── Items → InvoiceVersionItem — FK_ShopPOItemId, FK_ItemId, Quantity, UnitPrice
```
- **Add / remove lines (Draft):** `PUT /api/invoices/{id}/items` takes edits keyed by `invoiceItemId`
(change quantity/price, or `isRemoved: true` to strike the line — it is kept, excluded from the
total, shown struck through on the invoice view and print copy in every status, and can be
restored while Draft) or by `itemId` (add a catalog item
that was not on the PO; `unitPrice` required, quantity > 0, no duplicate of a live line). Added
lines have no `ShopPOItem` (`FK_ShopPOItemId` is nullable → no `orderedQuantity`, no max). Versions
store `IsRemoved` per line. At `Finalize`, adjustments are keyed by `invoiceItemId` and can also
set `isRemoved`. When the loading sheet is linked to invoices, it must count added lines.
- **Original PO quantity** stays on `ShopPOItem.Quantity` (locked once the PO is confirmed) and is
returned as `orderedQuantity` on each invoice line.
- `InvoiceItem` rows are copied from the PO's active lines at invoice creation (Mode A and Mode B).
- A version is written, in the same transaction as the change, when the invoice is:
| Stage | Written when |
|---|---|
| `Created` (v1) | Invoice created |
| `DraftEdited` | `PUT /api/invoices/{id}/items` while `Draft` |
| `Printed` | `POST /api/invoices/{id}/print` — the quantities on the paper copy |
| `Finalized` | `POST /api/invoices/{id}/finalize` — after the agent's edits are applied |
- Comparing `Printed` vs `Finalized` shows what the agent changed on paper.
- Migrations `AddInvoiceItems` and `AddInvoiceVersions` backfill lines and one baseline version for
invoices that already existed (stage mapped from the invoice's status at migration time, so they
have no earlier history).
### Invoice Screens & Printing (frontend)
> **Implemented** in `distribution_fe`.
**Invoice document** — one shared component (`components/invoice/invoice-template.tsx`) renders the
invoice for the preview, the draft editor and the print tab:
- **A4**: `INVOICE` header with Invoice No. (`INV-xxxxxxxx`), date and PO ref; a Status row shown
until the invoice is Finalized; *Bill To* (store name, plus address/phone from the PO); item table
(Item · Qty · Unit Price · Amount); totals (Subtotal, Total, and Paid / Balance Due when payments
exist); payments list; notes lines for the agent's edits on non-finalized copies; signature lines.
Soft slate rules, no heavy colour.
- **80mm / 58mm receipts**: single black-and-white column with dashed rules (`*** DRAFT ***` /
`*** PRINTED ***`, `qty x price` lines, heavy-ruled TOTAL, Paid/Balance, payments, "Thank you!").
- Money `Rs. 12,450.00`, quantities up to 3 decimals, dates `dd/MM/yyyy`.
- Deleted lines are shown **struck through in every status** and excluded from all totals.
- Not included (no data in the app): company/"From" block, logo, bank details, discount, tax, due date.
**Print tab** — `/invoices/{id}/print` (own route group `(print)`, no dashboard chrome). Has a
paper-size selector (A4 / 80mm / 58mm), sets `@page` with `margin: 0` (so the browser's own
URL/date header and footer are not printed; the A4 sheet pads itself with 14mm), and opens the
print dialog automatically. *Print* on a Draft asks who printed it, calls `POST /print`, then opens
this tab (the tab is opened inside the click so popup blockers allow it). Printed/Finalized
invoices have *Reprint*, which opens the tab without changing status.
**Draft editor** — pencil icon (Draft only, also *Edit Items* on the invoice page) opens a full page, `/sales/invoices/{id}/edit` (not a modal), showing the invoice template in edit mode: quantity/price
inputs, a trash icon per line (undo icon restores a deleted line), and an add-item row aligned to
the table columns (item dropdown excluding items already on the invoice, quantity, price
auto-filled from the store's price list, Enter or **+** to add). Save is disabled until something
changes, a summary shows what is pending (the browser warns before leaving with unsaved edits), and an item picked but not yet added is included on save. Non-Draft invoices redirect back to the invoice page.
Each save writes a `DraftEdited` version.
**Configuration** — *Invoice Creation* card: two toggles, **Auto** (Mode A) and **Manual** (Mode B,
default), mutually exclusive (turning one on turns the other off; the active one is locked on), each
with a one-line explanation and a process line (`Confirm PO → … → Edit draft → Print → Finalize`).
### New Enum — `InvoiceStatus` ### New Enum — `InvoiceStatus`
```csharp ```csharp
@@ -140,6 +223,7 @@ Draft → Printed → Finalized
|---|---|---| |---|---|---|
| (create) → Draft | **Mode A:** combined with PO confirm above | Invoice created at `Draft`. `Order.InvoiceId` set. | | (create) → Draft | **Mode A:** combined with PO confirm above | Invoice created at `Draft`. `Order.InvoiceId` set. |
| (create) → Draft | **Mode B:** `POST /api/invoices` `{ orderId }` | Office user selects a Confirmed PO (filtered by `agentId` / `routeId` / `shopId`) and creates the Invoice at `Draft`. `Order.InvoiceId` set. | | (create) → Draft | **Mode B:** `POST /api/invoices` `{ orderId }` | Office user selects a Confirmed PO (filtered by `agentId` / `routeId` / `shopId`) and creates the Invoice at `Draft`. `Order.InvoiceId` set. |
| Draft (edit) | `PUT /api/invoices/{id}/items` | Office user edits the draft's lines (quantity `0..ordered`, price `>= 0`). Status stays `Draft`; `TotalAmount` recomputed; a `DraftEdited` version is saved. |
| Draft → Printed | `POST /api/invoices/{id}/print` | Invoice is physically printed. `PrintedAt`/`PrintedBy` set. Agent takes the printed copy to the field and makes handwritten edits during the ride. | | Draft → Printed | `POST /api/invoices/{id}/print` | Invoice is physically printed. `PrintedAt`/`PrintedBy` set. Agent takes the printed copy to the field and makes handwritten edits during the ride. |
| Printed → Finalized | `POST /api/invoices/{id}/finalize` | After ride/day-end, agent returns printed invoice. Office worker enters any edits the agent made (quantity adjustments, returns, damage, discounts) and submits. `TotalAmount` locked. `FinalizedAt`/`FinalizedBy` set. | | Printed → Finalized | `POST /api/invoices/{id}/finalize` | After ride/day-end, agent returns printed invoice. Office worker enters any edits the agent made (quantity adjustments, returns, damage, discounts) and submits. `TotalAmount` locked. `FinalizedAt`/`FinalizedBy` set. |
@@ -162,7 +246,10 @@ Draft → Printed → Finalized
Invoice creation for both modes. It is stored as a plain column with no DB-level FK constraint. Invoice creation for both modes. It is stored as a plain column with no DB-level FK constraint.
7. An Invoice cannot skip `Printed` — `Draft → Finalized` directly is not a valid transition. 7. An Invoice cannot skip `Printed` — `Draft → Finalized` directly is not a valid transition.
8. `Invoice.TotalAmount` is only authoritative once `Finalized`; through `Draft`/`Printed` it 8. `Invoice.TotalAmount` is only authoritative once `Finalized`; through `Draft`/`Printed` it
reflects the current line items. reflects the current invoice lines (`InvoiceItem`), not the PO lines.
8a. Invoice lines are editable only while `Draft`. At `Finalize`, only quantities can be adjusted
(never above the ordered quantity). Every change writes an `InvoiceVersion` snapshot — see
[Invoice Line Items & Version History](#invoice-line-items--version-history).
9. `Order.Status` (fulfillment tracking: Pending/PartiallyFulfilled/Fulfilled/Cancelled) is a 9. `Order.Status` (fulfillment tracking: Pending/PartiallyFulfilled/Fulfilled/Cancelled) is a
separate axis from `Invoice.Status` — not merged in this pass. separate axis from `Invoice.Status` — not merged in this pass.
10. `Order.FK_SaleId` is optional — a PO can exist and be confirmed before any `Sale` is recorded. 10. `Order.FK_SaleId` is optional — a PO can exist and be confirmed before any `Sale` is recorded.
@@ -201,8 +288,9 @@ Draft → Printed → Finalized
already has a linked Invoice, what happens to that Invoice (voided? stays Draft?) is unresolved. already has a linked Invoice, what happens to that Invoice (voided? stays Draft?) is unresolved.
If the PO's items were pulled into a loading sheet, what happens to those loading sheet items is If the PO's items were pulled into a loading sheet, what happens to those loading sheet items is
also unresolved. Both cases need explicit cascade rules before implementation. also unresolved. Both cases need explicit cascade rules before implementation.
6. **Exact adjustment payload for `POST /finalize`** — quantity adjustments, returns, damage, and 6. **Exact adjustment payload for `POST /finalize`** — *partly resolved:* quantity adjustments
discounts are assumed but the payload shape is not fully specified. "Free issues" handling (items (`{ shopPOItemId, adjustedQuantity }`) are implemented and persisted on the invoice lines.
Returns, damage and discounts are still open, as is the payload shape for them. "Free issues" handling (items
given to the shop at no charge) is a known case that needs its own field(s) in the payload. given to the shop at no charge) is a known case that needs its own field(s) in the payload.
--- ---