Skip to main content

Purchase Invoice — Types & Selection Scenarios

Ship-11.a.3 introduces 5 Purchase Invoice types. This document explains when to choose which type, what validations run on the backend, and several end-to-end scenarios.

Migration (origin, legacy): merchant_database/db_kesles_merchant/migrations/legacy/057_purchase_invoices_advance_type.sql. The purchase_invoices table now lives in db_kesles_merchant_inventory (owned by inventory_service; see merchant_database/db_kesles_merchant_inventory/migrations/v1/009_purchasing_domain.sql and 010_migrate_purchasing_from_main_db.sql). Backend: services/dashboard_api/internal/app/dashboard_purchase_invoices.go + db_purchasing.go (handler/validation); 3-way match and PO cap aggregation run in inventory_service (services/inventory_service/internal/inventory/store_purchase_invoices.go) and are reached over HTTP. Frontend: apps/merchant_dashboard/lib/dashboard/features/home/presentation/widgets/forms/purchase_invoice_form_dialog.dart

1. Summary of the 5 Types

TypeWire valueWhen to usePOVendorGR link requiredQty-based
FinalfinalGoods invoice after GR is postedRequiredSame as POYes (per line)Yes
Advance / DPadvanceDown payment / DP / pre-ship before GRRequiredSame as PONoNo (milestone)
ShippingshippingSeparate freight forwarder invoiceOptionalFree (forwarder)NoNo
CustomscustomsCustoms duty + import VAT from DJBC/brokerOptionalFree (DJBC/broker)NoNo
Credit Notecredit_noteCredit note / return, value can be negativeOptionalFreeNoNo

2. Decision Tree — Pick the Type

Tagihan apa yang kamu terima?

├── Barang (yang dipesan di PO) ─────────────────┐
│ │ │
│ ├── Barang SUDAH datang + GR sudah di-post │
│ │ dan ini tagihan pelunasan/settlement ───► final
│ │
│ └── Barang BELUM datang (DP, pre-ship,
│ termin pembayaran, milestone) ──────────► advance

├── Ongkos kirim / freight / forwarder
│ dari vendor ekspedisi sendiri ──────────────► shipping

├── Bea Cukai / PPN Impor
│ dari DJBC atau broker kepabeanan ───────────► customs

└── Retur / koreksi / kelebihan bayar ──────────► credit_note

Practical question: "Who is the issuing vendor of this invoice?"

  • Same as the goods vendor on the PO → final or advance
  • Forwarder (DHL, FedEx, local courier) → shipping
  • DJBC / import broker → customs
  • Goods vendor, but with negative / correction value → credit_note

3. Scenarios

3.1 Simple Domestic Purchase — Goods Only

Kesles buys 100 EDC machines from a domestic vendor, paid once after the goods are received. No DP, no separate freight (shipping already included in goods price), no customs.

PO-2026-00XX (Approved → Sent)
└── GR-2026-00XX (Posted)
└── Invoice #1 — type = final ✅ ONLY ONE invoice
total = qty × price + VAT

Form fields in Create Invoice:

  • Invoice Type: Final (goods, 3-way match)
  • Vendor Invoice #: invoice number from the vendor
  • Purchase Order: pick the PO that already has a posted GR
  • Lines: auto-pulled from GR (no manual typing required)
  • VAT: PPN 11% if applicable

Backend validation on Approve:

  • 3-way match (invoice qty ≤ remaining GR qty, price ±5% from PO)

3.2 Purchase With DP (Payment Terms)

Kesles buys 500 terminals from CoherentPlus Sdn Bhd (Malaysia). Terms: 50% DP at PO issuance, 50% settlement when goods are ready to ship. Shipping + customs paid separately.

PO IKN-2026-001 (Approved)
├── Invoice #1 — type = advance "50% DP upon PO issuance" ← bayar duluan
├── Invoice #2 — type = advance "50% Pre-Ship pelunasan barang" ← bayar sebelum GR

│ (Barang tiba di Priok, masuk gudang, GR di-post)

├── Invoice #3 — type = shipping "BL #ABC-123 — freight DHL" ← dari forwarder
└── Invoice #4 — type = customs "SPPB-20260422-000123" ← dari DJBC/broker

Important notes:

  • Invoices #1 and #2 do not require GR — they are designed to be paid before the goods arrive.
  • Cumulative total of advance + final for one PO is capped at PO.total_amount × (1 + tolerance) (default tolerance 5%, can be overridden by super_admin with ?force=true).
  • Shipping + customs invoices are not capped against the PO total because the vendor differs and the charge baseline is different.

3.3 Imported Purchase With Post-GR Settlement

Variant of 3.2: DP paid, the remaining 50% is paid only after the goods are received (not pre-ship). Then the settlement invoice = final (3-way match active).

PO IKN-2026-002
├── Invoice #1 — type = advance "30% DP"

│ (GR di-post dengan serial number)

├── Invoice #2 — type = final "Pelunasan 70% pasca-GR" ← 3-way match
├── Invoice #3 — type = shipping "Freight"
└── Invoice #4 — type = customs "Bea cukai + PPN impor"

Difference from 3.2: Invoice #2 = final, will be validated for qty + price vs GR + PO. Whereas in 3.2 Invoice #2 = advance because it is paid before the goods arrive (pre-ship).


3.4 Partial Return

Of the 500 units in 3.2, 10 units are damaged on inspection and returned. The vendor issues a credit note for 10 units + their VAT.

Invoice #5 — type = credit_note "Retur 10 unit serial SN123-SN132"
total_amount = -(10 × unit_price + PPN) ← NEGATIF

A credit note does not automatically reduce stock in payment_terminal_inventory — that is done via a separate inventory return flow (not covered by the invoice).


4. Backend Validation per Type

4.1 On Create / Update (POST, PATCH /purchase-invoices)

Validationfinaladvanceshippingcustomscredit_note
PO required
Vendor required
Each line requires goods_receipt_item_id
Line forbidden to have goods_receipt_item_id
Qty > 0 required
Unit price may be negative
Description required per line

4.2 On Approve (POST /purchase-invoices/{id}/approve)

Checkfinaladvanceshippingcustomscredit_note
3-way match (qty ≤ GR remaining, price ±5% PO)
Cumulative cap: Σ(advance+final approved) ≤ PO×1.05

super_admin can override both with ?force=true (audit-logged in the server log, printed as purchase_invoice approve forced by super_admin …).

The default tolerance (5%) can be overridden via the env var PURCHASE_INVOICE_MATCH_TOLERANCE=0.03 for 3%.


5. Practical FAQ

Q: Domestic goods + freight already included, no DP, which type? A: final only. One invoice, 3-way match.

Q: Goods arrived but the vendor wants payment first before issuing the invoice. Pick which? A: If there is no formal invoice from the vendor yet, skip — do not create an invoice without a vendor document. Once it exists (even at 100% value), choose advance if the GR is not yet posted, or final if it has been.

Q: Freight is included in the goods price (DDP / CIF). Do I still need a shipping invoice? A: No. shipping is only used when the forwarder issues its own invoice. If freight rides on the goods invoice, let it stay in the subtotal / other_charges of the final invoice.

Q: Can one PO have multiple advance invoices? A: Yes. Example terms 30% + 40% + 30% = three advance invoices. The cumulative cap still applies (total cannot exceed PO × 1.05).

Q: Can advance be used for a PO that is already received? A: Schema-wise yes (the backend does not block), but processually it is rarely sensible — typically once goods are received, only final remains unpaid. Use advance only before GR.

Q: What is the difference between other_charges_amount and a shipping invoice type? A: other_charges_amount = minor charges riding on the same invoice (e.g., packing cost from the goods vendor). shipping = a SEPARATE invoice from a forwarder vendor that is a different legal entity. Use the right field so AP reconciliation is clean per vendor.

Q: Credit note for a different-vendor PO? A: Allowed — purchase_order_id is optional for credit_note. Fill milestone_label with the reference PO/invoice being returned for traceability.


6. Possible Error Messages

Error codeMeaningHandling
three_way_match_failedFinal line qty or price does not match GR/POFix qty/price or request super_admin override
po_cap_exceededTotal advance+final > PO × (1+tolerance)Check other invoices on the same PO; or super_admin override
validation_failed (per field)Wrong payload (e.g., advance has gr_item_id, or final qty=0)Check the form UI — conditional fields should already prevent this
duplicate_invoice (409)Combination vendor_id + invoice_number already existsUse a unique invoice number per vendor
invoice_not_editable (409)Tried to edit an already approved/paid invoiceVoid first, or create a credit_note correction

7. Document Relationship Diagram

┌──────────────┐
│ Vendor │
│ (master) │
└──────┬───────┘


┌─────────────┐ ┌──────────────┐
│ │ │ │
│ Purchase ├────────►│ Goods │
│ Order (PO) │ │ Receipt │
│ │ │ (GR) │
└──────┬──────┘ └──────┬───────┘
│ │
┌─────────┼──────────┐ │
│ │ │ │
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌──────────┐ ┌────────────┐
│advance│ │advance│ │ shipping │ │ final │
│ #1 │ │ #2 │ │ invoice │ │ invoice │
│ DP │ │Preship│ │ (opsion) │ │ (3-way │
└───────┘ └───────┘ └──────────┘ │ match) │
└────────────┘


┌────────────┐
│ customs │
│ invoice │
│ (opsional) │
└────────────┘

advance + final di-cap vs PO total
shipping + customs tidak di-cap

8. Code References

  • Type validation enum: normalizePurchaseInvoiceType in db_purchasing.go (dashboard_api)
  • GR-requirement branching: invoiceTypeRequiresGRLink in db_purchasing.go
  • Cumulative cap: poCapSummary type in db_purchasing.go; on approve the handler calls inventoryClient.getApprovedInvoiceSumHTTP (dashboard_purchase_invoices.go), backed by inventory_service
  • 3-way match: inventoryClient.getInvoiceThreeWayMatchHTTP (dashboard_purchase_invoices.go) → Store.VerifyThreeWayMatch in store_purchase_invoices.go (inventory_service); mismatch shape = threeWayMatchError type in db_purchasing.go
  • Tolerance: purchaseInvoiceThreeWayTolerance in dashboard_purchase_invoices.go
  • UI conditional sections: _InvoiceTypeX.poRequired, .allowsFreeVendor, .showMilestoneLabel in purchase_invoice_form_dialog.dart
  • Migration columns + constraints: legacy/057_purchase_invoices_advance_type.sql (origin); current schema in db_kesles_merchant_inventory migrations 009/010