Purchase Orders
What it is
Every purchase order (LPO) you have raised on a supplier, newest first, for the entity currently selected in the entity switcher. One row per order: its number, the supplier, the order value, the date, and two separate states that are easy to confuse and are explained below.
From here you can raise a new LPO, email one to its supplier with the PDF and Excel attached, mark one received, start a renegotiation on it, and delete one or many.
Who can open it
Any signed-in staff account that is active. Two gates sit in front of the page, both in proxy.ts:
- Account status. A signed-in account that is not active is sent back to the login page.
- The feature grant
ops.purchase-orders, but only whileFEATURE_GRANULAR_RBACis switched on. When it is on and your account has not been granted that feature, opening the page bounces you to the dashboard with?denied=ops.purchase-ordersin the address bar. The owner account bypasses this unconditionally.
ops.purchase-orders is the same grant that covers /approvals.
One action on this page is gated harder than the page itself. ✓ Received calls an API that
requires the operator floor — that admits accountant, sales, operations and owner, and refuses a
pending or inactive account. Everything else on the page runs under your own database permissions.
Before you start
Only the selected entity's orders are listed. The list is filtered by the entity in the switcher, so an order you cannot find may simply belong to another company. Switch entity and the list reloads.
Two different "received" ideas live on this screen, and they do different things:
| Set by | What it does | |
|---|---|---|
Status to fulfilled | The ✓ Received button | Marks the order fulfilled, appends [Received DD/MM/YYYY] to its notes, and writes stock movements into inventory |
Delivery to partial / complete | The small dropdown under the status badge | Records progress only. Writes no stock movements |
They are not linked. Setting Delivery to complete does not mark the order fulfilled, and pressing
✓ Received does not change the Delivery dropdown.
🔴 Setting Delivery to partial or complete silently locks the order from editing. The edit
screen will then refuse to save with "Goods have been received against this purchase order, so it
can no longer be edited." Nothing on this list warns you before you pick it. Setting Delivery back
to open does restore editing — the lock is read from the current value each time you save — but
nothing on either screen tells you so.
Every control on the page
Header bar, left to right:
| Control | What it does |
|---|---|
| Search PO #, supplier... | Filters the rows already loaded, as you type. Matches PO number, supplier name, status and notes. It does not go to the server, so it only ever searches this entity's orders. |
| Filter | Opens a drawer with Status and Delivery dropdowns, a Clear filters link once either is set, and saved views. The pill shows a count — Filter (1) — while a filter is applied. Filtering happens on the rows already loaded, like the search box. |
| Columns | Shows and hides table columns: PO #, Supplier, Amount, Date, Status, Delivery. Your choice is remembered per user. It lists only columns this table can draw — it used to offer Currency, Entity, Items Count and Notes, none of which this screen loads. |
| ⋯ More | One item: Print, which opens the browser print dialog on the list as displayed. |
| Delete (N) | Only appears once you tick rows. Deletes all of them. |
| + Create LPO | Goes to Create LPO. |
The table — a tick box, then whichever of PO #, Supplier, Amount, Date, Status and Delivery you have left switched on in Columns, then the row actions. PO #, Supplier, Amount, Date and Status are sortable; click the heading, click again to reverse. Delivery is not sortable.
The Supplier cell carries three lines: the supplier name, the order value with its currency, and — once the order has been emailed — a ✉️ with the date it last went out.
The Status cell carries the order status as a badge, and under it the Delivery dropdown
(Open / Partial / Complete / Cancelled). The dropdown is disabled when the order's status is
cancelled.
Row actions, right-hand side:
| Control | When it appears | What it does |
|---|---|---|
| ✓ Received | Only when the status is exactly issued | Asks "Mark this Purchase Order as fully received/fulfilled?", then marks it fulfilled and writes stock movements |
| ↗ Renegotiate | Only when the order has a supplier | Opens the negotiation dialog for the whole order, seeded with the order total as the current rate |
| ✉️ Email / ✉️ Re-send | Always | Sends the PO to the supplier with PDF and Excel attached. Reads Re-send once it has been sent before |
| ⚡ Actions | Always | 👁 View, ✎ Edit, 🗑 Delete |
🔴 A fulfilled order can never be received through this screen. The ✓ Received button is
drawn only for status issued. An order that reached fulfilled some other way, or that sits in
any other status, has no button — even though the API behind it would accept the order.
⚠️ Deleting from ⚡ Actions asks once, inline: "Permanently delete this purchase order and its items?" That is literally what happens — there is no archive and no undo. (It used to ask twice, and the two questions disagreed: one offered to archive, one to delete. Nothing was ever archived.)
Empty state: a 🛒 with "No Purchase Orders found" and "Create an LPO to track vendor costs and logistics."
What it writes
Delivery dropdown calls PATCH /api/purchase-orders/[id], which accepts delivery_state and
nothing else. It updates purchase_orders.delivery_state and writes an update row to the
activity log. The row changes on screen before the server answers, and is put back if it refuses.
✓ Received calls POST /api/purchase-orders/[id]/receive:
purchase_orders— setsstatustofulfilledand appends[Received DD/MM/YYYY]tonotes.stock_movements— one positive movement per line on the order. Safe to press twice: the emitter skips lines it has already written for this order.- Refused for a
cancelledorrejectedorder with "Cannot receive a … purchase order." - The stock write is allowed to fail without failing the action. If it does, the order is still marked fulfilled and the response carries a warning this screen does not display. Inventory is treated as reporting, not financial truth.
✉️ Email / Re-send calls POST /api/purchase-orders/[id]/email-supplier:
- Sends to the supplier's
email, falling back toemail_1thenemail_2. No email on any of the three and nothing is sent — "Supplier has no email on file". - Attaches a PDF and an Excel file, both named after the PO number.
purchase_orders— setsemailed_at,emailed_to, incrementsemailed_count, and promotes adraftorder toissued. Any other status is left alone.- ⚠️ While PO e-signing is switched on, an unsigned order is refused — "This PO must be approved and signed before it can be sent to the supplier."
🗑 Delete, single or bulk, deletes straight from purchase_orders under your own database
permissions. There is no archive and no undo.
When it goes wrong
"Failed: Supplier has no email on file". The supplier record has none of email, email_1 or
email_2. Fix it on the supplier, then re-send.
The ✉️ date never appears even though the mail arrived. The send succeeded and the follow-up write to the order failed. This screen shows its success message either way, so a missing date is the only symptom. The supplier has the order; the record just does not say so.
"This PO must be approved and signed before it can be sent to the supplier." E-signing is on and this order has not been signed. Send it for signature first.
The ✓ Received button is missing. The status is not issued. Check the badge.
"Cannot receive a cancelled purchase order." Exactly what it says — reopen or re-raise the order instead.
The edit screen refuses to save with "Goods have been received…". The Delivery dropdown on this
list is at partial or complete. Set it back to open to edit, then set it again afterwards.
A column you expect is missing. Check Columns — the setting is remembered per user, so a column you hid stays hidden on your next visit. Reset puts all six back.
The list looks short. Check the Filter pill: if it reads Filter (1) or Filter (2), a
Status or Delivery filter is still applied from earlier. Open it and press Clear filters.
Search finds nothing you know exists. It only searches orders already on screen for the selected entity. Check the entity switcher.
Related screens
- Create LPO — raising a new purchase order.
- Purchase Order — one order in full.
- Edit Purchase Order — changing one, and when you may not.
- Suppliers — where the email addresses used above are kept.