Skip to content

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_ADMIN or SUPER_ADMIN role) 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 buildSupplierLedgerData from src/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). createSupplierTransaction accepts an accountId for 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.deduct can apply a TDS rate; the handler splits the payment into a net payment + TDS liability line. Quarterly export via finance.tds.export.
  • Advance allocation — finance.allocations.supplier_advance allows 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.

  • 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.