Suppliers
Written April 2026 — read this first
Still broadly right, with one correction that matters: a supplier's purchases and its payments now settle that supplier's own SUP-xxxxxxxx ledger, never the 2100 control account, so a paid supplier shows as paid. A supplier transaction whose voucher is posted cannot be edited or deleted; after the voucher is reversed it is corrected through correct_supplier_transaction (finance.supplier_transactions.correct). See Vouchers.
External vendors — airlines, hotels, DMC, visa desks, transport, food. Managed from /suppliers, with a per-supplier detail page that exposes the ledger, hotel/FIT/food/transport assets, and transaction history.
Scope
- List + categories —
src/pages/suppliers/Suppliers.tsx. Tabbed by category (Ticketing / Visa / Hotel / Food / Transport / General), grid or list view, inactive toggle. - Detail page —
src/pages/suppliers/SupplierDetailPage.tsx(route/suppliers/:id). Shows linked inventory (airline blocks, FIT, hotels, food, transfers) plus the transaction ledger. - Service layer —
src/services/supplierService.ts. Fetch, create, update, soft/hard-delete supplier; fetch/create/update/delete supplier transactions. - Math helpers —
src/pages/suppliers/suppliersMath.ts(+ tests) for filtering and category counts.
Every supplier has a permanent supplier code (SU-0005), shown beside the name on the list and
the supplier page, and found by the list's search (PTY-001,
Party codes). The supplier's creditor ledger keeps its SUP- code; its name
carries the supplier code (PTY-004).
Approval
Every new supplier starts as Pending approval (PTY-009). When it is added, the database e-mails sales@alhudatravels.in and puts a notice in the inbox of everyone who can approve it (PTY-010).
- Only a General
Manager, a CEO or an Admin (
IT_ADMINorSUPER_ADMINrole) approves or rejects (suppliers.approve): Approve supplier / Reject on the supplier (the list's detail panel or/suppliers/:id). A rejection needs a reason and switches the supplier off. A rejected supplier is switched back on only by approving it. - While a supplier is pending or rejected, Add Transaction is off and the database refuses a bill or payment entered by hand. The supplier can still be picked for inventory.
- The decision e-mails sales@ with who decided and why.
- Suppliers that existed before 02/10/2026 are approved.
Not built: postings the database makes itself for a pending supplier (a hotel lease, an airline block purchase) are not held back.
Pages
Suppliers.tsx — /suppliers
Route gated by inventory.view today (src/App.tsx:171).
Route permission drift
Semantically the /suppliers route should be gated by suppliers.view, but no such permission exists and the route currently uses inventory.view. This is tracked in docs/PERMISSIONS.md §8 drift item 5. Until that's fixed, revoking inventory.view hides the suppliers page as a side effect.
Categories (Suppliers.tsx:89-96):
| Key | Label | Icon tint |
|---|---|---|
ticketing |
Ticketing | blue |
visa |
Visa | purple |
hotel |
Hotel | orange |
food |
Food | green |
transport |
Transport | yellow |
general |
General | slate |
hotel, food, transport categories show a city filter; others don't (Suppliers.tsx:98 — CITY_ENABLED_SUPPLIER_CATEGORIES).
Create-supplier form (Suppliers.tsx:107-121) collects: name, category, currency (defaults to DEFAULT_CURRENCY), city, logo URL, website URL, contact name/email/phone, address, notes, linked airline, GSTIN. Ticketing suppliers can be linked 1:1 to an airline record (airlineId).
SupplierDetailPage.tsx — /suppliers/:id
Route gated by inventory.view (src/App.tsx:172). Aggregates everything hanging off one supplier:
- Linked hotels, FIT inventory, airline blocks, food packages, ground transfers.
- Supplier transaction ledger (built via
buildSupplierLedgerDatafromsrc/services/ledgerService.ts). - Inline "Add transaction" dialog with type (debit / credit / refund / adjustment), amount + currency, date, description, reference link, linked GL account.
Service layer — src/services/supplierService.ts
Thin wrappers over /suppliers REST endpoints. All functions round-trip through mapSupplier / mapTransaction (supplierService.ts:105-144) to convert API date strings into Date objects and default currency/isActive.
| Function | Endpoint | Purpose |
|---|---|---|
fetchSuppliers({ includeInactive }) |
GET /suppliers |
List. Pass includeInactive=true to include soft-deleted. |
createSupplier(payload) |
POST /suppliers |
Create. |
updateSupplier(id, patch) |
PATCH /suppliers/:id |
Partial update. |
deleteSupplier(id) |
DELETE /suppliers/:id |
Soft-delete (isActive=false). |
hardDeleteSupplier(id) |
DELETE /suppliers/:id?hard=true |
Permanent remove. |
fetchSupplierTransactions(supplierId) |
GET /suppliers/:id/transactions |
Ledger rows. |
createSupplierTransaction(supplierId, payload) |
POST /suppliers/:id/transactions |
Debit / credit / refund / adjustment. |
updateSupplierTransaction(id, patch) |
PATCH /suppliers/transactions/:id |
Edit a transaction. |
deleteSupplierTransaction(id) |
DELETE /suppliers/transactions/:id |
Remove a transaction. |
Transaction types (SupplierTransactionType) are debit (purchase), credit (inflow), refund, adjustment. See Suppliers.tsx:100-105 for the label/colour mapping.
Suppliers in finance
Suppliers are the counterparty on supplier payables in the GL.
- Each transaction posted against a supplier creates or references a journal entry (
SupplierTransaction.journalEntryId).createSupplierTransactionaccepts anaccountIdfor the offsetting GL account (bank / cash / expense). - The supplier ledger is rendered from the same journal data via
buildSupplierLedgerData— it's the same underlying source as the Trial Balance, so the supplier ledger closing balance matches the payable control account. - TDS — when posting a supplier payment, users with
finance.tds.deductcan apply a TDS rate; the handler splits the payment into a net payment + TDS liability line. Quarterly export viafinance.tds.export. - Advance allocation —
finance.allocations.supplier_advanceallows allocating a previously-paid advance to a specific purchase (granular permission, seeded, not yet fully wired per PERMISSIONS.md §8).
Airlines vs ticketing suppliers
An airline (the entity that operates flights) and a ticketing supplier (the vendor through whom tickets are purchased, e.g. a GDS or consolidator) are separate records. A ticketing supplier can optionally link to one airline via airlineId (supplierService.ts:23, :42) — used so that inventory blocks and tickets roll up to the right finance counterparty.
Permissions
Canonical reference: docs/PERMISSIONS.md §4.1 and §6.13.
| Action | Permission | Enforced at |
|---|---|---|
View /suppliers |
inventory.view (should be suppliers.view — drift) |
Route App.tsx:171 |
View /suppliers/:id |
inventory.view (same) |
Route App.tsx:172 |
| Add supplier | suppliers.create |
<PermissionGate> + API |
| Edit supplier | suppliers.edit |
<PermissionGate> + API |
| Delete supplier | suppliers.delete |
<PermissionGate> + API |
| Add transaction | suppliers.edit |
<PermissionGate> |
| Edit transaction | suppliers.edit |
<PermissionGate> |
| Delete transaction | suppliers.delete |
<PermissionGate> |
| Apply TDS on supplier payment | finance.tds.deduct |
<PermissionGate> + API |
| Export 26Q CSV | finance.tds.export |
<PermissionGate> + API |
Roles that hold suppliers.* by default (PERMISSIONS.md §5.2): B2B_* (partial), OPS_MANAGER, OPS_EXEC (edit only), plus super-admin tier.
Related
- Partners — different concept (external sellers, not vendors).
- Hotels / Airline Blocks / FIT / Ground Transfers — per-category inventory rolls up to one supplier.
- Finance module — supplier payables, TDS, advance allocation.
- PERMISSIONS.md §6.13 — authoritative action matrix.
In the native app
The native app (staff, Operations →
Suppliers) lists suppliers by kind with search, opens one with contact, what they sell us, the
balance and the latest fifty transactions, and creates, edits, retires and restores a supplier
(suppliers.create, suppliers.edit) with the fields of the desktop form (no logo URL or
airline link). With no suppliers.view permission (§8, drift item 5), the section opens for
inventory.view or any right the Supplier row policy reads. The transaction rows need
suppliers.edit, inventory.view or finance.view; the balance is the ledger's running figure
(purchases and refunds raise it, payments lower it) and includes vouchers still awaiting approval.
A duplicate on create (same kind, same name) is refused and named — the desktop's POST
/suppliers quietly updates the duplicate in place. The SUP- ledger is made on the first
posting against the supplier, as ensureSupplierCreditorAccount makes it on the desktop
(fin_supplier_creditor_account, 20260929020000).
Seats to pay for (inventory.view, issue #559 step 5): a supplier that sells us airline
blocks or FIT seats lists the live ones; each opens its screen, where a payment is recorded
through record_airline_block_payment (inventory.block_payments.record, pending finance —
FIN-044), the function the desktop uses for a payment against a block or FIT.
Not in the app: recording any other payment, a bill or TDS (the desktop posts these from the
browser on finance.create, createGenericSupplierFinancePosting → createJournalWithLines,
not through a database function a staff member may call — FIN-030, "Not covered yet"), editing or deleting a transaction, deleting a
supplier for good, the logo and airline link.