5 Purchase Orders
Hardik edited this page 2026-06-21 12:23:09 +05:30
This file contains ambiguous Unicode characters

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_CODEcompany.code (fallback PMS).
  • VESSEL_CODEvessel.code (fallback GEN).
  • 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 (AprMar) rendered YYYY-YY (e.g. Apr 2025Mar 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):

  1. Header — Title (required), description, Cost Centre (Vessel, required), Accounting Code (leaf only, required), Company (optional), Vendor (optional, added later), Date Required, Project Code.
  2. 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).
  3. Terms & Conditions — Delivery, Dispatch, Inspection, Transit Insurance, Payment Terms, Others (all optional text → tc* fields).
  4. 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/id binding. Use name-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).