- 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>
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` | | | ✓ | ✓ | ✓ | | |
|
||||
|
|
|
|||
20
Tech-Debt.md
20
Tech-Debt.md
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue