docs: PO cancellation + supersede (#53), inventory-reversal tech debt (#55)

- PO-Lifecycle: CANCELLED status + Cancellation & supersede section
- Roles: cancel_po (MANAGER + SUPERUSER)
- Purchase-Orders: cancelled POs exportable with CANCELLED watermark
- Data-Model: cancelledAt/cancellationReason/supersededById
- Tech-Debt: TD-3 inventory increments not reversed on cancel (#55)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hardik 2026-06-21 12:23:09 +05:30
parent c6bec19d85
commit 2029ae7083
5 changed files with 53 additions and 3 deletions

@ -75,6 +75,8 @@ The central entity. Key fields:
| `piQuotationNo/Date?`, `requisitionNo/Date?`, `placeOfDelivery?` | quotation/requisition metadata |
| `tcDelivery / tcDispatch / tcInspection / tcTransitInsurance / tcPaymentTerms / tcOthers` | Terms & Conditions text |
| `submittedAt / approvedAt / paidAt / closedAt / createdAt / updatedAt` | lifecycle timestamps |
| `cancelledAt? / cancellationReason?` | set when a manager/superuser cancels the PO |
| `supersededById?` | self-relation → the existing PO that replaces this cancelled one (reciprocal `supersedes`) |
Required FKs: `submitterId → User`, `vesselId → Vessel` (**cost centre**),
`accountId → Account` (**accounting code**). Optional FKs: `companyId`,

@ -99,3 +99,26 @@ Each status renders a colour-coded pill (`components/po/po-status-badge.tsx`):
| PARTIALLY_CLOSED | green-adjacent |
| CLOSED | green/success |
| REJECTED | red/danger |
| CANCELLED | red/danger (greyed row in history) |
## Cancellation & supersede
Orthogonal to the lifecycle above: a **MANAGER** or **SUPERUSER** can **cancel** a PO
from **any** state (permission `cancel_po`; `lib/po-state-machine.ts``canCancel`). The
UI requires typing the word `cancel` and a **mandatory reason** in a confirmation modal
(`components/po/cancel-po-controls.tsx`); the action (`cancelPo`) sets a terminal
`CANCELLED` status with `cancelledAt`/`cancellationReason`, logs a `CANCELLED` audit row,
and notifies the submitter + Accounts.
A cancelled PO's value **drops out of every spend tracker/graph** automatically — those
filter on `POST_APPROVAL_STATUSES` or explicit whitelists, none of which include
`CANCELLED`. Cancelled POs stay visible (greyed in history) and **exportable**, but the
PDF/XLSX export carries a diagonal **CANCELLED** watermark.
A cancelled PO may optionally be **superseded** by the existing PO that replaces it
(`supersedePo`, by PO number; self-referential `supersededById`). No vessel/account/vendor
match is enforced, the link can be added any time, and the replacement shows the
reciprocal "supersedes" link.
> Approval-time inventory increments are **not** reversed on cancel yet — deferred (see
> [Tech Debt](Tech-Debt); inventory is feature-flagged off).

@ -102,9 +102,13 @@ approved, not creation).
## Export (PDF / XLSX)
`/api/po/[id]/export?format=pdf|xlsx` returns the PO as a document. It is
**gated to `MGR_APPROVED` and later** (a DRAFT export returns HTTP 403). The
approver's name (and uploaded signature) appears as signatory; company details
populate the header; optional line-item descriptions are included.
**gated to `MGR_APPROVED` and later, plus `CANCELLED`** (a DRAFT export returns HTTP
403). The approver's name (and uploaded signature) appears as signatory; company
details populate the header; optional line-item descriptions are included.
**Cancelled POs** are exportable too, but carry a diagonal **CANCELLED** watermark
(CSS overlay in the PDF, an embedded image in the XLSX). See
[Cancellation & supersede](PO-Lifecycle#cancellation--supersede).
**Company branding** (set per company in Admin → Companies → Edit → Branding)
also renders on the export: the uploaded **logo** floats top-left of the header,

@ -35,6 +35,7 @@ The exact `ROLE_PERMISSIONS` map in `lib/permissions.ts`. ✓ = granted.
| `view_all_pos` | | | ✓ | ✓ | ✓ | ✓ | ✓ |
| `approve_po` | | | | ✓ | ✓ | | |
| `reject_po` | | | | ✓ | ✓ | | |
| `cancel_po` | | | | ✓ | ✓ | | |
| `request_edits` | | | | ✓ | ✓ | | |
| `request_vendor_id` | | | | ✓ | ✓ | | |
| `process_payment` | | | ✓ | ✓ | ✓ | | |

@ -10,6 +10,26 @@ goal is visibility: each item records **what**, **why it matters**, and a
## Open
### TD-3 · Inventory increments are not reversed when a PO is cancelled
**What.** Approving a PO with a `siteId` increments `ItemInventory`
(`approvals/[id]/actions.ts`). The cancel feature (#53) intentionally does **not**
reverse that increment, so a cancelled approved PO can leave stale stock behind.
**Why it matters.** Low impact today — inventory writes almost never fire (see TD-1)
and the surface is feature-flagged off — but it becomes a correctness bug the moment
inventory-on-approval is brought live.
**Suggested direction.** When inventory goes live, `cancelPo` should decrement the same
`ItemInventory` quantities for line items with a `productId`, guarding against
double-reversal and negative stock. Tracked as Forgejo issue **#55**; deferred per the
#53 answers.
**Touch points.** `app/(portal)/po/[id]/actions.ts` (`cancelPo`),
`app/(portal)/approvals/[id]/actions.ts` (the increment), `lib/feature-flags.ts`.
---
### TD-1 · Inventory-on-approval is dormant in production
**What.** Approving a PO is meant to add its ordered items to the delivery