Foodops
All docs

Purchasing · Updated 2026-08-20 · v0b06c7e

Recording Expenses (Foodops)

Audio narration for this article is generated with the weekly docs refresh.

Plain-English summary

Expenses are recorded in the admin back-office, not on the till. There are two everyday ways to capture money going out:

  • Expense Records (/expenses/records) — for a one-off or reimbursable expense you already paid or are paying now (fuel, cleaning supplies, a staff reimbursement, a cash purchase). You pick who it was for, which account it was Paid Through, one or more expense-category lines with amounts and tax, and you can attach the receipt.
  • Bills (/expenses/bills) — for a supplier invoice you'll pay later (a payable). A bill sits as owed until you record a Payment Made against it.

Both live under the Expenses area of the admin app and are backed by the expenses service (hubits-api-expenses). This is separate from taking cash out of the register drawer during a shift (that's a cash withdrawal / pickup on the register session, not a business expense).

When you'd use this

  • Logging a cash purchase (fuel, gas cylinder, cleaning supplies) with the receipt attached
  • Reimbursing a staff member for something they bought for the business
  • Recording a supplier invoice as a Bill to pay at month-end
  • Setting up a Recurring Bill for a fixed monthly cost (rent, subscription)
  • Getting expense tax onto the Input Tax Statement for GST claim-back

How to access

  • Expense Records: /expenses/records (list), /expenses/records/new (create), /expenses/records/:id (details), /expenses/records/:id/edit. [code: foodops-admin-app/apps/admin/src/App.tsx:1285-1327]
  • Bills: /expenses/bills · Payments Made: /expenses/payments-made · Recurring Bills: /expenses/recurring · Supplier Credits: /expenses/supplier-credits.
  • Sidebar: under Expenses (alongside Suppliers, Purchase Orders, Purchase Receipts).
  • Requires expense/purchasing role permissions (create is gated separately from view).

Walkthrough — record a one-off expense

Open Expenses → Records → New (/expenses/records/new). The form is served by ExpenseRecordForm. [code: foodops-admin-app/apps/admin/src/components/expenses/expense-records/ExpenseRecordForm.tsx; hubits-api-expenses/Hubits.Expenses.Api/Controllers/ExpensesController.cs:152 (POST /api/v1/expenses)]

  1. Expense date — the date the expense was incurred. Required. [code: ExpenseRecordForm.tsx:265 ("Expense date is required")]
  2. Expense For — who the spend relates to: Supplier, Customer, Employee, or Other. Pick Supplier for a vendor purchase, Employee for a staff reimbursement. When Supplier is chosen, a supplier must be selected. [code: ExpenseRecordForm.tsx:28 (ExpenseFor), :268 (supplier required when expenseFor === 'supplier')]
  3. Paid Through — the account the money came out of (a bank or cash account from the chart of accounts). Required. [code: ExpenseRecordForm.tsx:267 ("Paid Through account is required"); expensesLookupService.paidThroughAccounts()]
  4. Tax inclusive — a toggle controlling whether the line amounts already include tax (default on). Flipping it recomputes tax on every line. [code: ExpenseRecordForm.tsx:106,243-250 (isTaxInclusive)]
  5. Add expense line(s) — each line has:
    • Expense Category — the account to book the cost against (the chart of accounts filtered to Expense-type accounts). At least one line with a category and an amount is required. [code: ExpenseRecordForm.tsx:162-165 (category = expense-type CoA), :275-278 ("At least one line with a category and amount is required")]
    • Description, Amount, Tax Type (a tax rate; drives the computed Tax Amount), and optional Notes. [code: apps/admin/src/types/expenses/expense.ts (ExpenseLineItem: expenseAccountId, amount, taxTypeId, taxAmount, notes)]
    • Totals roll up as Sub-total, Tax total, and Gross total. [code: ExpenseRecordForm.tsx:253-256]
  6. Attach the receipt — add the receipt image/PDF to the record. [code: ExpenseRecordForm.tsx:86 (receiptFile); hubits-api-expenses/ExpensesController.cs:564 (POST {id}/files)]
  7. Save. The expense posts via POST /api/v1/expenses. Depending on your org's setup an expense can move through a Draft → Pending Approval → Approved status flow (or Rejected, if declined), and can be flagged billable (To Be Billed) to on-charge to a customer/project. "Submitted" is not a separate step — the backend's GET .../list-summary endpoint reports its count as identical to Pending Approval (Submitted = pendingApproval in ExpensesController.GetListSummary). [code: apps/admin/src/types/expenses/expense.ts (ExpenseListFilter: Draft/Submitted/PendingApproval/Approved/ToBeBilled; isBillable)]

The Expense Records list shows running totals (today, last-30-days, fiscal-year, to-be-billed) and filters by status, supplier, and date range. [code: apps/admin/src/types/expenses/expense.ts (ExpenseListResponse: todayTotal/last30DaysTotal/fiscalYearTotal/toBeBilledTotal)]

Walkthrough — record a supplier bill (payable)

For a supplier invoice you'll settle later, use Expenses → Bills → New (/expenses/bills/new) instead of an Expense Record. A Bill captures the supplier, line items, and due date, and stays outstanding until you record a Payment Made (/expenses/payments-made) against it. Bills can also be generated from a Purchase Order (see Purchase Orders → "Create Bill"). For fixed repeating costs, a Recurring Bill (/expenses/recurring) auto-generates the bill on a schedule. [code: hubits-api-expenses/Hubits.Expenses.Api/Controllers/{BillsController.cs,PaymentsController.cs,RecurringBillsController.cs}]

Key concepts

  • Expense Record — a one-off/reimbursable expense you've paid (or are paying now) out of a Paid-Through account. Immediate, not a payable.
  • Bill — a supplier invoice you owe and will pay later; an accounts-payable document. Settled via Payments Made.
  • Paid Through account — the bank/cash account an Expense Record was paid from. Distinguishes "money already left this account" from a Bill (not yet paid).
  • Expense Category — the chart-of-accounts expense account the cost is booked to (e.g. Utilities, Repairs, Supplies). Drives your P&L and expense reporting.
  • Billable / To Be Billed — an expense you intend to on-charge to a customer or project rather than absorb.
  • Tax inclusive — whether the amounts you type already include tax; when off, tax is added on top. Feeds the Input Tax Statement.

Common questions

Q: How do I add an expense? A: Go to Expenses → Records → New (/expenses/records/new) in the admin app. Set the date, pick who it's for and the Paid Through account, add one or more expense-category lines with amounts, attach the receipt, and Save.

Q: What's the difference between an Expense Record and a Bill? A: An Expense Record is something you've paid (or are paying now) — it comes straight out of a Paid-Through account. A Bill is a supplier invoice you owe and will pay later; it sits as a payable until you record a Payment Made. Use Records for cash/immediate spend, Bills for "pay the supplier at month-end."

Q: Can I attach the receipt? A: Yes — you can attach the receipt image/PDF to the Expense Record, and to bills/documents via their Files tab.

Q: Can I record a staff reimbursement? A: Yes — create an Expense Record with Expense For = Employee and pick the staff member; choose the account you reimbursed them from as Paid Through.

Q: Where do I see how much I've spent? A: The Expenses list shows today / last-30-days / fiscal-year totals. For tax, use the Input Tax Statement report (/reports/input-tax-statement) which sums tax paid on purchases/expenses. (The legacy "Expense Record Details" report was not ported to the new Reports rail — browse the data under Expenses instead. See Reports.)

Q: Is recording an expense the same as taking cash out of the till? A: No. Cash physically removed from the register drawer mid-shift is a cash withdrawal / pickup on the register session (part of till reconciliation), not a business expense. An Expense Record is a back-office accounting entry against a Paid-Through account. They're separate.

Q: Can I set up a recurring monthly expense (rent, subscription)? A: Yes — use a Recurring Bill (/expenses/recurring). It auto-generates the bill on the schedule you set.

Q: Can I claim the tax on expenses? A: Expense lines carry a tax type; the tax you pay on expenses flows into the Input Tax Statement (/reports/input-tax-statement) for GST claim-back prep.

Q: Do expenses need approval before they count? A: They can — an expense can move through Draft → Submitted → Pending Approval → Approved depending on your org's setup. Approve pending expenses from the Expense Records list / detail.

Q: Can I on-charge an expense to a customer? A: Yes — mark the expense billable (it shows as To Be Billed), then bill it to the customer/project later.

Q: Can I export or bulk-import expenses? A: The expenses service exposes export and import endpoints, so an expense export and a bulk import are supported at the API level. [code: hubits-api-expenses/Hubits.Expenses.Api/Controllers/ExpensesController.cs:408 (POST export), :433 (POST import)] Exact placement of the buttons in the admin UI isn't live-walked in this pass — look for Export/Import on the Expense Records list.

Edge cases and known issues

Save is blocked — "Paid Through account is required"

  • Symptom: Can't save an Expense Record
  • Cause: No Paid-Through (bank/cash) account selected — it's mandatory for a Record (unlike a Bill)
  • Workaround: Pick the account the money came from; if none exist, add a bank/cash account in the chart of accounts first

Save is blocked — "At least one line with a category and amount is required"

  • Symptom: Validation error on save with lines that look filled
  • Cause: A line is missing its Expense Category or has a zero amount
  • Workaround: Ensure every line you keep has both a category and a positive amount; remove empty lines

Tax totals look doubled / halved

  • Symptom: Tax total isn't what you expected
  • Cause: The Tax inclusive toggle is set the wrong way for how you typed the amounts
  • Workaround: Toggle Tax-inclusive to match your inputs (on = amounts already include tax); totals recompute

Related

  • Purchase Orders — POs → Receive items → Create Bill (the payable side)

  • Suppliers — the payee for supplier expenses/bills

  • Reports — Input Tax Statement (expense tax); legacy Expense Record Details not ported

  • Recording Expenses on mobile — the admin app in a phone browser

  • 2026-08-17 — foodops-admin-app 47ff627..474b25d (ExpenseRecordForm.tsx) touches this entry's cited form component with two items worth flagging for the next live-walk; neither applied as an edit yet since the supplied diff hunk is truncated mid-explanation. (1) A new STATUS_NAMES map is now in code: Draft/Pending approval/Approved/Rejected. 'Rejected' is now confirmed as a real, named status in the admin UI (previously only inferred from the 2026-08-12 backend list-summary note) — worth adding to this entry's status-flow description once live-walked. 'Submitted' has no entry in this map, reinforcing (not yet proving — the EXPENSE_STATUS enum definition itself isn't in this diff) the 2026-08-12 suspicion that 'Submitted' isn't a distinct FE step from 'Pending Approval'; needs the enum or a live walk confirmed before editing the entry's documented 'Draft → Submitted → Pending Approval → Approved' flow. (2) The form gains a new payMode: 'paid' | 'owed' field plus an AccountKind = 'money' | 'owed' | 'unknown' categorization of Paid-Through accounts. An inline comment describes payMode as 'a lens over the single paidThroughAccountId the API takes, not an extra field' — i.e. a UX reframing of the existing account picker as a paid-vs-owed question, not a new API field. If borne out by the full diff, this cuts against this entry's clean Key-Concepts/FAQ framing that 'Paid Through' always means money already left the account (as opposed to a Bill, which is owed) — an Expense Record's Paid-Through account may already be able to be a liability (owed) account today. The supplied diff cuts off exactly where paidThroughAccounts()'s account-kind logic is explained, so this isn't confirmed enough to edit blind — flag as a priority follow-up once the rest of the diff or a live walk of the new payMode toggle is available. Also new in this range: a lineMode: 'simple' | 'split' field, suggesting a simplified single-line entry mode alongside the existing multi-line split editor (additive, not yet documented). Unrelated to the above: this file was also refactored to extract ExpenseLinesEditor and ExpenseReceiptPanel as separate components and to add a SupplierFormDialog import (consistent with the already-logged 2026-08-11 inline-supplier-creation note) — no new contradiction from the refactor itself. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-16 — hubits-api-expenses e505b62..ca7c174 (superset of the already-reviewed e505b62..3780bb3 range; the list-summary endpoints and Payment/SupplierCredit GetById projections previously logged on 2026-08-12 reappear here unchanged and are not re-noted). Two genuinely new items in this range, both source-only: (1) BillsController.Create and .Update now call PurchaseOrderHelper.RecalculateBilledTotalsAsync — creating a Bill rolls the linked Purchase Order's billed/outstanding totals forward, and editing a Bill that re-points it at a different PO (or unlinks it) recalculates both the old and new PO. This entry's 'record a supplier bill' section notes Bills can be generated from a PO ('Create Bill') but says nothing about the PO's own billed/outstanding figures staying in sync afterward — worth a line once the Purchase Orders billed-total display is live-walked (may belong in purchase-orders.md rather than here). (2) A commit titled 'fix(bills): eager-load ProductVariant so approval stops throwing NRE' lands in this range — likely closing the gap behind the Bill-approval 400 validation already flagged as an undocumented edge case on 2026-08-12 (line item with no product selected). The diff hunk for BillsController.ChangeStatus itself isn't in the supplied evidence, so the exact before/after behavior isn't confirmed here — still flagged as a pending Bills-approval edge-case doc task. Separately, a shared-middleware Idempotency-Key feature (feat(idempotency): honour Idempotency-Key via the shared middleware) landed API-wide; no admin-app hunk shows a client sending the header, so it's not user-facing enough to document yet. None of this contradicts any claim currently in the entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-12 — hubits-api-expenses e505b62..3780bb3 adds several user-facing behaviors this entry doesn't yet cover. (1) Approving a Bill (BillsController.ChangeStatus) now validates before posting journals: if any line item has no product/variant selected, or the org has no default Accounts Payable account configured, approval is blocked with a specific 400 message ('Cannot approve this bill: line item "X" has no product selected...' / '...the default Accounts Payable account is not set up for this organisation') instead of a prior unhandled NRE — worth a line in the Bills walkthrough/edge-cases once the exact admin-UI error surfacing is live-walked. (2) New GET list-summary endpoints on BillsController, PaymentsController, SupplierCreditsController and ExpensesController return per-tab row counts (Bills: All/Draft/Approvals/Unpaid/Paid; Payments: All/Draft/Submitted/Recorded/PendingAllocation; SupplierCredits: All/Open/PartiallyApplied/Applied; Expenses: All/ToBeBilled/Draft/PendingApproval/Approved/Rejected) — this surfaces a Rejected expense status not mentioned anywhere in this entry's Draft→Submitted→PendingApproval→Approved description, and shows 'Submitted' is counted identically to 'PendingApproval' rather than as a distinct bucket, worth reconciling once the admin list-tab UI is live-walked. (3) Payment Made detail now projects the allocated Bill's own reference, invoice date, due date, gross total and outstanding onto each allocation row (PaymentsController.GetById), so the Payment Voucher / Allocations table can show real bill info instead of a bare BillId — purely additive, no contradiction of documented behavior. None of this contradicts existing claims; all are additive and source-only (not live-walked), so no edits applied — flagged for the next live-walk pass. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-12 — hubits-api-expenses e505b62..b8b7a25 (BillsController.cs, PaymentsController.cs, SupplierCreditsController.cs, ExpensesController.cs). Two changes are user-facing enough to flag, none contradict the entry: (1) Bill approval now validates before posting journals — approving a Bill whose line item has no product selected previously threw an unhandled NRE (bare 500); it now returns a 400 with 'Cannot approve this bill: line item "X" has no product selected, so its cost account cannot be determined.', and a second check requires the org to have a default Accounts Payable chart-of-accounts entry before approval succeeds. The entry currently documents zero edge cases for the Bill-approval step (only two Expense Record validation errors are listed) — this is a new, real error message worth a Bills-section edge case once live-walked. (2) PaymentsController's GetById now projects the allocated Bill's reference/invoice-date/due-date/gross-total/outstanding into each allocation row (previously only BillId, a raw GUID) — a Payment detail-page enhancement for the /expenses/payments-made flow. Also fixed: PaymentsController and SupplierCreditsController's list-tab filter parameter was previously ignored ('every tab returned the same rows'), now wired via *TabParser.Parse(query.List). Four new GET .../list-summary endpoints were added (Bills, Payments, SupplierCredits, Expenses) returning per-tab row counts — likely backing tab-count badges in the list UI, but this diff is API-only (no foodops-admin-app hunk supplied), so whether/how the frontend consumes it is unconfirmed; don't add it to the walkthrough until a corresponding admin-app change or live UI is seen. SupplierCreditsController.GetById also now returns ExternalReference/DueDate/Terms/WarehouseName/PurchaseOrderId+Reference/CreatedDate and richer LinkedBills (BillId, Amount) — detail-page fields for a surface (/expenses/supplier-credits) the entry only lists as a route, not walked. All source-only; flag as follow-up doc tasks (Bills approval edge case, Payment/Supplier-Credit detail pages) rather than editing blind. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-11 — foodops-admin-app@4e51283 (range 47ff627..4e51283) adds inline supplier creation to the Expense Record supplier picker (ExpenseRecordForm.tsx): the SupplierCombobox now takes an onCreateNew callback that opens a SupplierFormDialog pre-filled with the typed query, and saving it selects the new supplier directly on the form (onSaved={(saved) => set('supplier', saved)}). This doesn't contradict the entry's existing 'when Supplier is chosen, a supplier must be selected' claim, but it's a new capability not yet documented — you no longer have to leave the Expense Record form to add a missing supplier first. Worth a follow-up line in the walkthrough/FAQ once live-walked; the diff doesn't show the trigger UI text (e.g. whether it's a 'Create "X"' row in the combobox dropdown), so don't guess the exact copy. A second hunk in the same file removes shadow-sm from NativeSelect's className — pure styling, not user-facing behavior worth noting. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-11 — foodops-admin-app@ae2f0f7 (commit range 47ff627..ae2f0f7) adds inline supplier creation to the Expense Record form's supplier picker (ExpenseRecordForm.tsx): the SupplierCombobox now has an onCreateNew handler that opens a new SupplierFormDialog pre-filled with the typed query; saving it calls set('supplier', saved), populating the form's Supplier field directly without leaving the Expense Record form/modal. This is additive to, not a contradiction of, the entry's existing 'when Supplier is chosen, a supplier must be selected' claim — a supplier can now be created on the fly instead of only picked from the existing list. The rest of this diff range (BillsPage.tsx hideBelow prop, NativeSelect shadow removal) is cosmetic styling with no user-facing behavior change; the 'Upload documents' button on BillsPage in this same diff was already captured in the 2026-08-10 verification note (commit range 47ff627..cb5f1c2) and is not new. Source-only — the inline supplier-creation dialog's exact UI (field set, validation) is not yet live-walked; flag as a follow-up doc task for the 'record a one-off expense' walkthrough step 2 (Expense For / supplier selection) rather than editing blind. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-10 — foodops-admin-app@cb5f1c2 (commit range 47ff627..cb5f1c2) adds an 'Upload documents' button to the Bills list toolbar (BillsPage.tsx), next to 'New Bill'. It navigates to a new route, /expenses/documents?upload=1, tooltipped 'Drop a supplier PDF — AI drafts the bill for review' — an AI-assisted bill-creation path (upload a supplier invoice PDF, AI drafts the Bill for review) alongside the existing manual Bills → New flow this entry documents. The entry's 'How to access' list and the 'record a supplier bill (payable)' walkthrough don't yet mention this route or flow. Only the entry point is confirmed from source; the actual /expenses/documents upload/review UX is not yet live-walked or screenshotted — flag as a follow-up doc task rather than editing the walkthrough blind. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-17 — Authored (codebase-only; not live-walked). Grounded the Expense Record flow against foodops-admin-app@289bf0e (ExpenseRecordForm.tsx: expense date / Expense For supplier·customer·employee·other / Paid Through required / tax-inclusive / expense-category lines from expense-type CoA / receipt attach / Draft→Submitted→PendingApproval→Approved + billable) and hubits-api-expenses@f6986b9 (ExpensesController: POST /api/v1/expenses create, update, list/detail, export/import, {id}/files, {id}/operation-logs, {id}/history; plus Bills/Payments/RecurringBills/SupplierCredits controllers). Distinguished Expense Records (paid) from Bills (payable) and from register cash withdrawals. Live walkthrough + screenshots + exact UI copy still UI-TODO.

Go beyond POS.
Choose the total solution.

Focus on creating the best experience for your guests while we handle the rest.