This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Purchase Orders
This page covers the mechanics specific to purchase orders: numbering, GST, the create/edit forms, company invoicing, accounting codes, and Excel import. For the status graph see PO Lifecycle; for the schema see Data Model.
PO numbering
lib/po-number.ts generates a structured number:
COMPANY_CODE / VESSEL_CODE / PO_ID / FY e.g. PMS/HNR1/9000/2024-25
- COMPANY_CODE —
company.code(fallbackPMS). - VESSEL_CODE —
vessel.code(fallbackGEN). - PO_ID — globally sequential integer.
nextPoId()scans existing structured numbers and floors at 8999, so the first system-generated ID is 9000 — avoiding clashes with imported POs (which keep their original, typically low, IDs). - FY — Indian financial year (Apr–Mar) rendered
YYYY-YY(e.g. Apr 2025–Mar 2026 →2025-26).
parsePoNumber() splits a number back into its four parts (returns null for
old-format numbers). Imported POs keep their original PO number verbatim.
GST calculation
GST is per line item. gstRate is a Decimal(5,4) on POLineItem, default
0.18 (18%):
line totalPrice = quantity × unitPrice × (1 + gstRate)
PO totalAmount = Σ line totalPrice (GST-inclusive)
The PO form shows a live summary below the line-items table:
- Taxable = Σ (qty × unitPrice)
- GST = Σ (qty × unitPrice × gstRate)
- Grand Total = Taxable + GST
This is applied in the Server Actions that compute totalPrice per line and the
PO totalAmount.
Creating / editing a PO
/po/new is a multi-section form (mirrored, pre-filled, by /po/[id]/edit):
- Header — Title (required), description, Cost Centre (Vessel, required), Accounting Code (leaf only, required), Company (optional), Vendor (optional, added later), Date Required, Project Code.
- Line items — dynamic rows: Name (searchable against the catalogue),
Description, Qty, Unit, Size, Unit Price, GST Rate. As-you-type name search
shows matching products with per-vendor price hints
(
/api/products/search). - Terms & Conditions — Delivery, Dispatch, Inspection, Transit Insurance,
Payment Terms, Others (all optional text →
tc*fields). - Documents — drag-and-drop / browse uploader (see File Storage).
Footer: Save as Draft / Submit for Approval (and Update & Resubmit
when editing an EDITS_REQUESTED PO, which returns it to MGR_REVIEW).
Validation lives in lib/validations/po.ts (Zod), which also exports
TC_FIXED_LINE and TC_DEFAULTS. URL pre-select is supported:
/po/new?vesselId=<id>.
Form selector gotcha (for tests): the PO form labels are visual-only — no
htmlFor/idbinding. Usename-attribute selectors (input[name="title"],select[name="vesselId"]). See Testing.
Companies (multi-company invoicing)
A PO is billed under a sister Company (PurchaseOrder.companyId, optional).
The selected company's name, code, gstNumber, address, phone/mobile,
contact + invoice email, and invoice address populate the exported PO header /
invoice block — falling back to hardcoded Pelagia defaults when no company is
linked. Managed at /admin/companies.
Accounting codes
The PO Accounting Code is a leaf in the 3-level Account hierarchy
(Top → Sub → Leaf, 6-digit numeric). Only leaf codes are selectable; the form
groups leaf codes by sub-category in a searchable, portal-rendered combobox
(components/ui/searchable-select.tsx). Line items can carry their own per-line
accountId. Seed data: prisma/accounting-codes-data.ts.
"Accounting Code" replaces the older "Account" label. The Cost Centre is a separate concept — it is the Vessel. See Glossary.
Payments
When Accounts records a payment, a compulsory payment date
(PurchaseOrder.paymentDate) is captured: the input defaults to today and
rejects future dates (validated in processPaymentSchema / markPaid).
Partial payments accumulate into paidAmount and hold the PO in
PARTIALLY_PAID until fully settled. The editable poDate drives the
exported "Date": poDate ?? approvedAt ?? createdAt (i.e. approval date once
approved, not creation).
Export (PDF / XLSX)
/api/po/[id]/export?format=pdf|xlsx returns the PO as a document. It is
gated to MGR_APPROVED and later, plus CANCELLED (a DRAFT export returns HTTP
403). The approver's name (and uploaded signature) appears as signatory; company
details populate the header; optional line-item descriptions are included.
Cancelled POs are exportable too, but carry a diagonal CANCELLED watermark (CSS overlay in the PDF, an embedded image in the XLSX). See Cancellation & supersede.
Company branding (set per company in Admin → Companies → Edit → Branding)
also renders on the export: the uploaded logo floats top-left of the header,
the uploaded stamp/seal overlays the approver's signatory block, and a fixed
brand-colour bar (#92D050) runs full-width along the bottom. Each piece is
optional except the bar, which is always drawn. See
File Storage.
Import PO → CLOSED
/po/import parses a Pelagia-format Excel PO (lib/po-import-parser.ts,
/api/po/import) and saves it directly as CLOSED — a historical record
that bypasses approval. It:
- auto-detects the company (by header/code),
- auto-matches the vessel by code,
- auto-creates the vendor and any unknown products, and upserts per-vendor prices,
- keeps the original PO number verbatim.
The parser stops at "INSTRUCTIONS TO VENDORS", excludes T&C rows from line items,
extracts vendor name / PI quotation / place of delivery, and normalises a GST
rate written as 18 to the fraction 0.18. Import is restricted to
MANAGER / SUPERUSER / ADMIN (TECHNICAL/ACCOUNTS → 403).
Pelagia Portal (PPMS)
Overview
Build & Run
System
Product
- Feature Catalogue
- Pages and Navigation
- Workflows
- Purchase Orders
- Vendors and GST Lookup
- Inventory and Catalogue
- Inventory on Approval
- Notifications
- File Storage
- Design System
Planned
Quality
Ops
Engineering
Pelagia Portal (PPMS) — internal purchase-order management. Self-hosted on pms1, live at pms.pelagiamarine.com. This wiki tracks the shipped product; authoritative sources are the repo code, App/CLAUDE.md, Docs/, and CHANGELOG.md.