Plain-English summary
Suppliers are the vendors you buy ingredients and supplies from. Purchase orders (POs) are the formal records of what you ordered from them, at what price, and when it's due. When the goods arrive, you mark the PO as received — stock goes up, an invoice is recorded, and the supplier's outstanding balance tracks what you still owe. This is Foodops's procurement side, the counterpart to the sales side.
When you'd use this
Every time you reorder chicken, vegetables, drinks, packaging, or cleaning supplies. Without POs, stock goes up "magically" via manual adjustments and nobody knows what's owed to whom. With POs, you have an audit trail: ordered → received → paid, with reports across any date range.
How to access
- Suppliers list:
/expenses/suppliers(admin app —admin.foodops.io) - Purchase Orders list:
/expenses/purchase-orders - Bills / Payments Made / Supplier Credits:
/expenses/bills,/expenses/payments-made,/expenses/supplier-credits - Sidebar: the Purchases section in the admin app
The legacy
haafai.app.foodopsmonolith routes (/suppliers,/purchaseorders) are retired at the July-10 launch — purchasing now lives entirely in the admin app under/expenses/*. The full PO lifecycle (pipeline statuses, separate Receive + Create-Bill documents) is documented in Purchase Orders; this entry focuses on the supplier directory and the procurement loop.

Prerequisites
- At least one outlet with a configured warehouse (the default "Primary Warehouse" is created per outlet during onboarding)
- The Add Supplier permission on the user's role (any admin-level user has this)
- If using approval workflow: a user with PO Approval permission configured in Settings → Users & Roles
Walkthrough A — create a supplier
1. Open the Suppliers list
Navigate to /suppliers. The list shows columns Supplier, Email, Phone, Outstanding (balance owed to this supplier). Filter tabs above the list: All · Overdue · Unpaid · Inactive. Search suppliers by name via the top-left search box.
2. Click "New Supplier"
A full-page form opens at /expenses/suppliers/new — supplier creation is no longer a modal dialog. Required fields are marked with a red asterisk:

| Field | Purpose |
|---|---|
| Name ⃰ | Display name (e.g., "Al Madina Food Suppliers") |
| Registration Number | Supplier's company registration — useful for tax reporting |
| Preference | Preferred contact method (Email / Phone / Mobile) — routes automated communication |
| Email / Phone / Mobile | Contact details |
| TIN Number | Tax Identification Number — required for GST-registered suppliers |
| Default Currency ⃰ | MVR by default; change for international suppliers |
| Tax Type | Zero (no tax) or GST (8% Maldives standard) |
| Payment Term | Due on Receipt / Net 5 / Net 10 / Net 15 / Net 30 / Net 45 / Net 60. Drives when POs from this supplier become "overdue" |
| Credit Limit | Maximum outstanding balance before a new PO triggers a warning |
| Assigned User | Internal staff member responsible for this supplier relationship |
| Billing Address | Address Line 1, Address Line 2, City, Post Code, Country (full country list, defaults to Maldives) |
3. Save
Click Save. The supplier is active immediately, appears in the Suppliers list with Outstanding MVR 0.00, and is available in the supplier dropdown when creating POs.
Walkthrough B — create and receive a purchase order
1. Open the Purchase Orders list
Navigate to /purchaseorders. The list shows columns Reference, Order Date, Supplier, Sub Total, Total, Expected Delivery Date, Status. Status tabs: All · Open · Approvals · Completed · Cancelled.
2. Click "New Purchase Order"
A full-page form opens at /expenses/purchase-orders/new with 4 tabs (Line Items · Address · Terms & Conditions · Settings) plus a header block of shared fields — PO creation is no longer a modal dialog.

Header fields (required marked with *):
| Field | Purpose |
|---|---|
| Supplier ⃰ | Dropdown of existing suppliers |
| Delivery Warehouse ⃰ | Where the stock will land when received. Defaults to "Primary Warehouse" of the outlet |
| Order Date ⃰ | Defaults to today |
| Expected Delivery Date | Optional — drives "expected vs actual" delivery-delay reporting |
| Payment Terms ⃰ | Overrides the supplier's default if needed for this PO |
| Price List | If the supplier has a pre-agreed price list, attach it here |
| Project | Cost-centre tagging for POs that aren't general inventory (e.g., one-off event stock) |
| Notes (Internal) | Free-text, not shown to the supplier |
3. Add line items
On the Line Items tab, use the product picker ("Start typing or scanning... (Press Enter twice to select first item)") or + New Product to create one inline. Each line has:
| Column | Meaning |
|---|---|
| # | Line number |
| Item | Product being purchased |
| Quantity | Ordered quantity |
| Unit | Purchase unit of measure |
| Rate (excl. tax) | Per-unit price before tax |
| Discount | Line-level discount amount or % |
| Tax % | Applied tax rate |
| Sub Total (excl. tax) | Auto-computed |
4. Totals
Below the line items table:
- Sub Total — sum of line sub-totals
- Discount — order-level discount
- Tax Total — sum of tax across all lines
- Shipping Charge — manually entered
- Adjustment — rounding or ad-hoc correction
- Total — final amount
5. Optional tabs
- Address — override the delivery address if different from the warehouse default
- Terms & Conditions — freeform text attached to the printed PO
- Settings — PO-specific configuration (rarely used)
6. Save
Click Save. The PO starts in its pipeline's initial status (default pipeline: Draft). PO statuses are pipeline-driven and only configured transitions are allowed; the default seeded pipeline is Draft → Pending Approval → Approved → Sent to Supplier → Partially Billed → Billed → Closed, with Cancelled as the off-ramp. (Pipelines are org-configurable, so your org's statuses may differ.) See Purchase Orders for the full lifecycle and the status flags that gate receiving/billing.
7. Receive the goods (a separate Purchase Receive)
When the supplier delivers, open the PO and choose Receive items → this creates a separate Purchase Receive document (/expenses/purchase-receipts). Pick the destination warehouse/location (required), enter the actual received quantities per line (may differ from ordered for a partial delivery), and save. Receiving books the StockTransaction rows that increase ingredient stock — this is the moment inventory moves (not PO creation, and not billing). The PO accumulates ReceivedQuantity; you can receive again later for the balance.
8. Bill and pay (separate Create Bill + Payments Made)
Create Bill from the PO details (or tick the receive's "post bill" option) generates the payable. The supplier's Outstanding balance increases when the bill is posted, and the Payment Term starts the due-date countdown ("Net 30" = due 30 days out). Record payments via Payments Made (/expenses/payments-made) against the bill to clear the balance. The Suppliers list's Overdue and Unpaid tabs show who you owe and by how much.
Key concepts
- Supplier — a vendor you buy from. Has contact info, default payment terms, currency, tax type, and an outstanding balance. One organization can have many suppliers.
- Purchase Order (PO) — a formal record of an intent to buy. Includes supplier, delivery destination, line items, prices, and payment terms. Statuses are pipeline-driven (default: Draft → Pending Approval → Approved → Sent to Supplier → Partially Billed → Billed → Closed); a PO is never hard-deleted — cancellation preserves the audit trail. Receiving stock and creating the bill are separate follow-on documents.
- Warehouse — the physical location stock lives in. Each outlet has at least one (Primary Warehouse). Multi-outlet organizations may have centralized warehouses on higher plans.
- Payment Terms — contractual agreement on when invoices are due: Due on Receipt (immediate), Net N (N days after receipt). Drives overdue calculations.
- Credit Limit — a soft cap on supplier exposure. A new PO that would push outstanding above this limit triggers a warning but doesn't block.
- Outstanding balance — sum of (received POs) minus (recorded payments) for a supplier. Shown in MVR on the Suppliers list.
- TIN — Tax Identification Number. Required on Maldives GST-registered supplier records.
- Approval workflow — optional gate: new POs above a threshold enter Approvals status and need a user with PO Approval permission to move them to Open.
Common questions
Q: Can I create a supplier during PO creation if they don't exist yet?
A: Not directly — the supplier dropdown is read-only. Open /suppliers in another tab, create the supplier, come back, and refresh the PO form.
Q: What's the difference between Delivery Warehouse and Address? A: Delivery Warehouse is where stock gets added when received (drives inventory). Address is the physical address printed on the PO document. In a single-outlet org they're usually the same place; in multi-location orgs they can differ (e.g., PO billed to HQ, delivered to Outlet 3).
Q: How do I track partial deliveries?
A: Use Receive items to create a Purchase Receive and enter the actual quantities received per line. Each receive is its own document; the PO accumulates ReceivedQuantity, and you can receive again later for the balance. Billing (Partially Billed / Billed) is driven separately by how much you've billed — receiving alone doesn't move the PO into a "completed" status.
Q: Can I edit a PO after it's saved? A: Yes — the edit endpoint exists and editing is most sensible before receiving/billing. What's allowed is governed by the PO's pipeline status flags and org policy (e.g. avoid editing once it's Billed/Closed). To correct a mistake after stock has been received, create an inventory adjustment or a corrective receive instead.
Q: What happens if I cancel a PO that was already partially received? A: The already-received stock stays in inventory (it physically exists). The cancellation only voids the outstanding lines. The partial receipt remains on the supplier's ledger.
Q: Do I need recipes for POs to work? A: No — POs just deal with raw product stock going up. Recipes are only needed if you want the sales side to auto-deduct stock. You can run the procurement workflow with no recipes at all, just no auto-depletion on sales.
Q: How are overdue POs calculated?
A: Invoice becomes due at receipt_date + payment_term_days. If today is after that date and the balance isn't zero, the PO (and its supplier) appear on the Overdue tab. The aging report groups overdue amounts into buckets (0–30, 31–60, 61–90, 90+ days).
Q: Can I import suppliers from a spreadsheet? A: Yes during onboarding, or on-demand via support. The supplier bulk-import template maps your columns to Foodops fields. Once in, edits happen one-at-a-time through the UI.
Q: What about refunds / credits from a supplier? A: Record as a supplier credit note (negative invoice) against the supplier. Reduces outstanding balance. The workflow is available in the supplier detail view, not the Purchase Orders list.
Q: Can I send the PO to the supplier by email from Foodops? A: Yes — open the saved PO and use the "Email PO" action. Uses the supplier's email on file, includes a printable PDF of the PO. Email body is templatable in Settings → Communications.
Q: Can I add comments or attach files to a supplier or PO record? A: Yes. Both supplier and PO detail views carry a record-level Comments thread and a Files attachment list, separate from the internal Notes field. As of 2026-05-15 you can edit and delete your own comments, and delete an attached file (file deletes are soft-deletes — the audit trail is preserved). Useful for collaborative procurement notes and supporting documents (quotes, delivery photos). [code: hubits-api-expenses/Hubits.Expenses.Api/Controllers/SuppliersController.cs:1307 (UpdateEntityComment), :1320 (DeleteEntityComment), :1379 (DeleteEntityFile); PurchaseOrdersController.cs:1232, :1245, :1304]
Q: Can I delete a supplier or a purchase order? A: Suppliers can't be hard-deleted — instead set them Inactive via the status toggle (they drop off the active dropdown but the ledger history is preserved). Purchase Orders are voided by moving them to Cancelled status, not deleted, so the audit trail stays intact. (Bills, by contrast, do support a true delete.) [code: hubits-api-expenses/Hubits.Expenses.Api/Controllers/SuppliersController.cs:703 (status PUT, no top-level DELETE); PurchaseOrdersController.cs:927 (change-status, no top-level DELETE)]
Edge cases and known issues
Supplier dropdown is empty when creating a PO
- Symptom: Opened the New Purchase Order modal, supplier dropdown shows nothing
- Cause: No suppliers created yet, or all existing suppliers are marked Inactive
- Workaround: Create at least one supplier first, or reactivate an inactive one
Received stock didn't land in the expected warehouse
- Symptom: A Purchase Receive was saved but ingredient stock didn't update in the warehouse you expected
- Cause (intended): The Purchase Receive's destination warehouse was set to a different warehouse than the current active one. This is a feature, not a bug — receives can route to any warehouse
- Cause (known bug, N2): If you edited an existing Purchase Receive, the admin app drops the header warehouse on load (the receive mapper returns
warehouseId/stockLocationIdasnull), so re-saving can post stock to the wrong/default location. [code: foodops-admin-app/apps/admin/src/services/purchaseReceiveService.ts:112-114] - Workaround: Check the receive's destination location. Prefer creating a corrective receive/adjustment over editing a posted receive; if stock is in the wrong warehouse, create a stock transfer to the intended one. A fix for N2 is in progress
Approvals workflow stalls if the approver is out
- Symptom: PO stuck in Approvals status, approver on leave
- Cause: Approval is assigned to a specific user or role
- Workaround: Admins can reassign approval, or disable the approval requirement temporarily in Settings
Tax total doesn't match expected amount
- Symptom: Tax Total at the bottom doesn't equal sum of (line × tax_rate)
- Cause: Order-level discount distributes across lines, changing each line's taxable amount
- Workaround: Verify by exporting the PO — the Sub Total (excl. tax) × tax_rate should match the displayed Tax Total after discount distribution
Plan limits
Plan tiers and their limits are not listed here — they are DB-backed and change with the lineup. See Pricing for the live catalogue.
API and integration notes
- Supplier and PO endpoints are under the expenses service (
/api/v1/*inhubits-api-expenses—SuppliersController,PurchaseOrdersController,PurchaseReceivesController,BillsController,PaymentsController,SupplierCreditsController) - A Purchase Receive books
StockTransactionrows (stock up) at the resolved StockLocation; it does not itself create the payable — that's a separate Bill (or the receive's optional "post bill" flag) - Supplier Outstanding balance reflects posted bills minus payments made / supplier credits, recomputed as those documents change
- CSV export available from the Suppliers and Purchase Orders list views (3-dot menu top-right)
Related
- Recipes & Ingredients — what you're buying is what ingredients feed into recipes
- Settings Overview — warehouses, users/roles, tax configuration
- Reports Catalogue — procurement reports, supplier aging, PO analysis
- User Stories: Purchasing — US-PUR-001 through US-PUR-010
What changed (2026-04-15 → 2026-05-04)
0646e56(2026-04-18) — improved error handling and concurrency management in bill saving process. AP-side, but the supplier ledger relies on it11b1995(2026-04-20) — fixed cash-withdrawal issues and session amount inconsistency; touches theHaafai.ExpensesandHaafai.Inventoryprojects together (POs received during a session were the trigger)ba7d8c5(2026-04-23) — surfacing document generation error on PO/receipt PDFs5 substantive commits total in
Haafai.Expenses/**since the watermark; PO creation, receipt, and supplier UI labels remain stable per the Playwright pass on 2026-04-15. State machine and form fields above unchanged.2026-08-15 — hubits-api-expenses (e505b62..aba2ff2): two additions not covered by prior verification passes. (1) A new ProcurementTolerances service (Hubits.Expenses.Api/Services/PurchaseOrders/ProcurementTolerances.cs, tagged 'SP-5') defines org-level Price/Quantity tolerance settings and a canonical 'exception' predicate for a Purchase Order Receive: a receive line is exceptional if it has a RejectedQty > 0, is short vs its PO line beyond the quantity tolerance (same unit only), or its invoiced price differs from the PO price beyond the price tolerance (same currency/unit only). This is backend-only in this diff — no controller endpoint or foodops-admin-app change consumes it yet — but it directly bears on Walkthrough B step 7 ('Receive the goods'), which currently describes receiving only as entering actual quantities with no mention of rejected quantity or price/quantity tolerance flagging. Worth a UI-TODO follow-up once an endpoint/UI surfaces a receive-exceptions list or badge. (2) BillsController.ChangeStatus's approval guard (first seen 2026-08-11) is hardened: BillHelper.GetBill now eager-loads BillLineItems.ProductVariant so the null-check that returns the 'no product selected' 400 actually has data to check (previously an un-included navigation could itself read null and still NRE depending on load path) — same undocumented bill-approval capability already flagged in the 2026-07-14/2026-08-11 entries as backend scaffolding with no confirmed admin-app UI. (3) PurchaseOrdersController.GetOperationLogs now returns User (OperationLog.Username), Remarks, and Details (ExtraDetails) per log row, not just Date/StatusName as before — the PO History tab (if/when Playwright-walked per the existing UI-TODO) should now show who acted, why, and what changed, not just a bare status timeline. None of this contradicts anything currently documented in this entry. (auto-applied by the truth pipeline; adversarially gated)
2026-08-12 — hubits-api-expenses (e505b62..b8b7a25): the PurchaseOrdersController Assign/AssignedUserName change and the BillsController.ChangeStatus approval guard visible in this diff range are the same code already captured in the 2026-08-11 verification entry (that range's HEAD, 51a6e19, falls inside this one) — not re-flagged here. What's newly visible past that watermark: (1) SuppliersController's Suppliers-list 'Overdue' tab was a latent bug — it used the identical predicate as 'Unpaid' (any Outstanding > 0), so the two tabs returned the same rows; now Overdue requires at least one non-voided Bill with Outstanding > 0 and DueDate < today, matching the per-bill logic this entry's Common Questions already describes for overdue calculation, so users will now see fewer/different suppliers on the Overdue tab than before this fix. (2) The Suppliers list gained HasBalance/CurrencyId/FromDate/ToDate query filters, not currently documented in Walkthrough A's 'Search suppliers by name' description. (3) New GET .../list-summary endpoints were added on SuppliersController, BillsController, PaymentsController, and SupplierCreditsController, returning per-tab row counts (e.g. Suppliers: All/Overdue/Unpaid/Inactive; Bills: Draft/Approvals/Unpaid/Paid; Payments Made: Draft/Submitted/Recorded/PendingAllocation; Supplier Credits: Open/PartiallyApplied/Applied) — likely backing tab-count badges in the admin UI, but no foodops-admin-app diff confirms this shipped yet. (4) PaymentsController.GetPayments now actually honours the list-tab query param via PaymentMadeTabParser (a comment notes it previously never reached the query, so every Payments Made tab silently returned identical rows) and GetById now enriches each bill allocation with the bill's Reference/InvoiceDate/DueDate/GrossTotal/Outstanding instead of a bare BillId. (5) SupplierCreditsController.GetSupplierCredits now honours its list tab similarly, and GetById now returns ExternalReference/DueDate/Terms/WarehouseName/PurchaseOrderId+Reference/CreatedDate on the detail response. None of this contradicts anything currently documented in this entry (which only lightly touches Bills/Payments/Supplier Credits); worth a UI-TODO follow-up once admin.foodops.io's Bills, Payments Made, and Supplier Credits list/detail pages are Playwright-walked to confirm tab counts, working tab filters, and the richer detail fields are actually live. (auto-applied by the truth pipeline; adversarially gated)
2026-08-11 — hubits-api-expenses (e505b62..51a6e19): BillsController.ChangeStatus now explicitly guards bill approval (IsApproved transitions) against two previously-crashing cases — a line item with no ProductVariant selected (was an unhandled NullReferenceException from Bill.PostJournals dereferencing a null CostAccountId, now a 400 'Cannot approve this bill: line item "
2026-07-17 — hubits-api-expenses (c98d0d4..f6986b9): SuppliersController.GetById/Create/Update now support a second address type — ShippingAddress — alongside the already-documented BillingAddress (new MapFlatAddress helper reused for both; Create's UpsertBillingAddress was generalized to UpsertAddress and is now called for both BillingAddress and ShippingAddress). Walkthrough A step 2 and its field table currently document only a single 'Billing Address' block — worth a follow-up (and UI-TODO screenshot) to confirm whether admin.foodops.io's New Supplier form now shows a Shipping Address section. Separately, the same endpoints now expose/accept TaxId/AccountCode as aliases for TINNumber/IdentificationNo ('the React admin app's names for IdentificationNo/TINNumber' per the added code comment), and Contacts now carry FirstName/Position aliases for Name/Designation — API-only evidence with no accompanying foodops-admin-app diff in this batch, but it hints the New Supplier form's 'Registration Number'/'TIN Number' labels (and a Contacts UI not currently documented here) may already be renamed/added in the admin app; needs a live-UI check before touching the field table. Elsewhere in this diff: BillHelper.Create now returns an explicit (bool, string) error message (surfaced through BillsController/SupplierCreditsController) instead of a bare bool, SupplierCreditsController gained clearer validation errors (missing default bill pipeline / no line items / missing tax type) when creating a supplier credit, and PaymentsController.GetById now resolves chart-of-account display names (PaidThroughAccountName/WithholdingTaxAccountName/FxGainLossAccountName) plus a bugfix so editing a payment no longer zeroes ChequeNumber/withholding-tax/FX fields absent from the edit form — all internal robustness/display fixes that don't contradict this entry's documented walkthroughs. (auto-applied by the truth pipeline; adversarially gated)
2026-07-14 — hubits-api-expenses (f182af9..c98d0d4): two additions touch the Bill/Payment side that this entry references (Walkthrough B step 8, 'Bill and pay'). (1) New
GET /v1/bills/{id}/statusesmirrors the existing PO-statuses endpoint, returning the reachable next statuses (IsApproved/IsApprovalRequired/IsFinalState flags) for a bill's pipeline — this scaffolds a bill-level approval workflow analogous to the PO Approval workflow already documented under Key Concepts, but no bill-approval UI/flow is documented here yet. (2) Bill/Payment/SupplierCreditGetByIdresponses now returnJournalEntriesflattened to one row per ledger line (Account name, Debit, Credit, Description) instead of entry totals — implying a per-account 'Journal' breakdown is now (or will be) surfaced on those detail pages. Neither is currently mentioned in this entry; worth a UI-TODO follow-up once the admin-app Bill/Payment detail pages are Playwright-walked to confirm whether a Journal tab and bill-approval control actually appear. Everything else in this diff (PO search null-supplierId fix, BillHelper.Update() TaxType/tag null-safety) is an internal fix with no user-facing behavior change and does not contradict any documented claim. (auto-applied by the truth pipeline; adversarially gated)2026-07-14 — hubits-api-expenses (f182af9..a23943b, 'feat(bills): add GET /v1/bills/{id}/statuses for the approve workflow'): Bills now expose a pipeline-driven statuses endpoint mirroring the PO statuses endpoint, returning the reachable next statuses from a bill's current status plus per-status flags (IsApprovalRequired, IsApproved, IsEditable, IsPaid, IsVoided, IsFinalState, RequiresConfirmation) so the UI can drive an Approve / Send-for-approval action. This entry's Walkthrough B Step 8 ('Bill and pay') and the Common Questions currently describe Create Bill as producing the payable directly with no approval gate — worth a follow-up pass (and UI-TODO screenshot) to confirm whether org-configurable bill pipelines now insert an approval step before a bill posts, similar to the PO Approvals status already documented. Not treated as NEEDS_EDIT because the diff shows a new capability, not a contradiction of the currently-documented immediate-post behavior. Same commit range also adds flattened per-line JournalEntries (Account/Debit/Credit/Description) to Bill/Payment/SupplierCredit detail responses (presumably backing a new Journal tab) — accounting-detail surface, likely more relevant to a bills-specific entry than to this supplier/PO entry, flagged here for triage only. (auto-applied by the truth pipeline; adversarially gated)
2026-07-14 — foodops-admin-app (82ff18d..3202946): Purchase Order creation moved from a modal dialog to a full-page form at /expenses/purchase-orders/new, mirroring the supplier-form pattern already documented in this entry (Walkthrough B step 2 corrected). Separately, the same refactor ('replace ExpenseRecordFormDialog with ExpenseRecordForm for inline page presentation') also converted Bills ('New Bill' → /expenses/bills/new) and Payments Made ('New Payment' → /expenses/payments-made/new) from modal dialogs to page-based creation — not currently detailed in this entry's prose (Bills/Payments are only mentioned in passing), but worth knowing if those flows get walked through in more detail later. (auto-applied by the truth pipeline; adversarially gated)
2026-07-14 — hubits-api-expenses (f182af9..5fc983c): BillsController gained a new
GET /v1/bills/{id}/statusesendpoint (mirroring the existing PO statuses endpoint) that returns the pipeline statuses reachable from a bill's current status, explicitly to support an Approve / Send-for-approval workflow in the UI. This entry currently documents an Approval workflow only for Purchase Orders (Key concepts: 'optional gate... enters Approvals status') and treats Bills purely as the payable created from a PO receive, with no approval/status-pipeline concept. This is backend scaffolding only — no corresponding foodops-admin-app change confirms a Bills approval UI has shipped yet — so treat as a heads-up to watch for, not a documented feature: if/when the admin UI exposes a Bill approval step, this entry's 'Bill and pay' walkthrough section and the Key Concepts/Common Questions should be updated to describe it, mirroring how PO Approvals are currently documented. Separately, PurchaseOrdersController'sSearchendpoint was tightened so a query withsupplierIdset but no text (q) is no longer short-circuited to an empty result — minor null-safety fix, doesn't contradict any documented walkthrough or field. (auto-applied by the truth pipeline; adversarially gated)2026-07-05 — hubits-api-expenses (22c34df..8852799, 'Fixed unable to save edited purchase order'): PO edit/save now explicitly blocks removing a line item that has already been received (ReceivedQuantity > 0) or billed (referenced by a Bill), returning "Cannot remove line item '' because it has already been received or billed." instead of silently deleting it. A DB-constraint backstop returns a similar message for any FK reference the explicit check misses. This confirms and sharpens the existing Common Questions guidance that editing a PO is 'most sensible before receiving/billing' — worth adding a concrete Q&A noting the specific error users will see if they try to delete a received/billed line item while editing a PO, rather than just the general status-flag/org-policy framing currently there. [code: hubits-api-expenses/Hubits.Expenses.Api/Services/PurchaseOrders/PurchaseOrderHelper.cs, PurchaseOrderHelper.Update()] (auto-applied by the truth pipeline; adversarially gated)
2026-07-04 — foodops-admin-app (8138f76..d4ac7b5): Supplier create/edit moved from an in-page modal (
SupplierFormDialog) back to dedicated full-page routes (/expenses/suppliers/new,/expenses/suppliers/:id/edit) — Walkthrough A step 2 needs updating. Separately, SupplierDetailsPage.tsx was restructured with an explicit tab set (Activity, Bills, Payments, POs, Items, Notes, Files, History) replacing the prior ad hoc tabs — not yet documented in this entry and worth a UI-TODO screenshot pass alongside the existing Comments/Files UI-TODO. (auto-applied by the truth pipeline; adversarially gated)
Success: Verified 2026-04-15 · playwright (frontmatter refresh 2026-05-04 · codebase) Status:
stable· Review due: 2026-07-14 This entry was verified against the live app on 2026-04-15. Frontmatter v2 refresh on 2026-05-04: glob retargeted from non-existentHaafai.Purchasing/**→ realHaafai.Expenses/{Views,Services}/**family; the bogushaafai.app.pos: src/purchasing/**glob removed (FoodOps purchasing is monolith-only). Anchorba7d8c5is the freshest substantive commit touchingHaafai.Expenses/**since 2026-04-15. UI labels and state machine unchanged from the Playwright pass — recent commits are bug fixes (concurrency, PDF generation), not flow/label changes.
Note: Re-pointed after backend microservice split 2026-06-03 · codebase Verified_by:
codebase· Last verified: 2026-06-03 ·hubits-api-expenses:5b89882+haafai.app.foodops:bf7c9d2The 2026-05-02 split extracted the suppliers/PO/bills API intohaafai/hubits-api-expenses(prodmain). Added it as the now-primary backing repo with globHubits.Expenses.Api/{Controllers/{SuppliersController.cs,PurchaseOrdersController.cs},Services/{PurchaseOrders/**,Bills/**},ViewModels/PurchaseOrders/**}(all paths verified viagh api ...?ref=main— SuppliersController + PurchaseOrdersController + PO/Bills services + PO view-models confirmed present). The monolithHaafai.Expenses/**supplier/PO/bills paths still exist onmain(split is additive/in-progress), so the monolith glob is retained, consolidated toHaafai.Expenses/{Views/{Suppliers,PurchaseOrders},Services/{PurchaseOrders,Bills}}/**, and re-pinned to prod HEADbf7c9d2. Did NOT addfoodops.api(legacy) orhubits.microservices.
Success: Re-verified 2026-06-03 · codebase (
hubits-api-expenses@5b89882,haafai.app.foodops@bf7c9d2) Supplier + PO walkthroughs (fields, PO state machine, receive→invoice→outstanding flow) still matchSuppliersControllerandPurchaseOrdersControlleron prodmain(API HEAD5b89882, monolith HEADbf7c9d2— both unchanged from the pinned commits). Both controllers expose edit ([HttpPut]) but no top-level hard-delete: suppliers toggle Inactive (statusPUT :703), POs cancel via change-status (:927) — added a Q&A clarifying this. Real change: commit3b378e5(2026-05-15) added comment edit/delete + attached-file soft-delete sub-resources on both entities (Suppliers :1307/:1320/:1379; POs :1232/:1245/:1304) — documented as one new Q&A. Core purchasing flow unchanged; human-curated walkthroughs left intact. UI not screenshotted — flagged UI-TODO.
- 2026-06-29 — codebase: NEW-STACK re-verification for July-10 launch (legacy
haafai.app.foodopsmonolith fully retired). This entry still described the retired monolith UI — corrected: (1) routes/suppliers→/expenses/suppliers,/purchaseorders→/expenses/purchase-orders(verified againstfoodops-admin-app/apps/admin/src/App.tsxroute defs on main; note the/purchases/*strings in App.tsx are nav-title metadata, the real routes are/expenses/*); (2) PO status model rewritten from the old Open/Approvals/Completed/Cancelled to the pipeline model (Draft→Pending Approval→Approved→Sent to Supplier→Partially Billed→Billed→Closed +Cancelled); (3) receive and bill are now separate documents — receiving creates a Purchase Receive (books stock), Create Bill creates the payable; corrected the old "mark Received → auto-invoice + auto-transition to Completed" claim in walkthrough + Common Questions while keeping the voice. Re-pointed source_repos tohubits-api-expenses@55102c3e40+foodops-admin-app@f6ffd0b19d; dropped the monolith glob. KNOWN ISSUE N2 added to the warehouse edge case: editing a Purchase Receive drops its header warehouse (purchaseReceiveService.ts:112-114maps warehouseId/stockLocationId to null). Supplier directory fields, payment terms, credit limit, comments/files edit-delete all still matchSuppliersControlleron main.confidence: medium(new admin UI not yet Playwright-walked →verified_by: codebase).
Additional verification events are appended here by the weekly KB sync and by manual re-verification passes. See _meta/failure-log.md for the global failure index.