Appearance
Purchase Invoice Complete Walkthrough - Example-Based Documentation
Overview
This document demonstrates the complete lifecycle of a purchase invoice using a real payload example, showing exactly which tables and columns are affected during both Add (Draft) and Post phases.
Example Payload
json
{
"id": "73b9e8d2-fe21-4ef7-2e68-08ddfb5b40cf",
"invoiceDate": "2026-01-28T23:06:04",
"description": null,
"warehouseId": 53,
"warehouseName": "Default Warehouse",
"vendorId": 44,
"vendorName": "مؤسسة هدية الجودة للدعاية والاعلان",
"currencyRate": 1,
"paymentTermId": 9,
"reference": "",
"currencyId": 4,
"currencyName": "Saudi Riyal",
"currencyNameAr": "ريال سعودي",
"isExternalPurchase": false,
"invoiceDetails": [
{
"id": "2b55acb1-180e-48d6-9107-08ddfb5b40dd",
"itemId": 169,
"itemCode": "4137",
"itemName": "نوت وسط",
"itemVariantId": 245,
"categoryId": 50,
"categoryType": "Storable",
"uomId": "56835e4d-4593-4481-9839-10a6321a42d3",
"uomCode": "G8roW",
"quantity": 600,
"cost": 10,
"netCost": 10,
"subCost": 6000,
"discountPercentage": 0,
"discountAmount": 0,
"vatPercentage": 15,
"vatAmount": 900,
"totalAfterDiscount": 6000,
"grandTotal": 6900,
"isServiceItem": false,
"trackingType": "NoTracking",
"hasExpiryDate": false,
"stockInEntryMode": "Manual",
"taxId": 18,
"vatId": 18,
"vatName": "الضريبة القياسية"
}
],
"sourceId": "73b9e8d2-fe21-4ef7-2e68-08ddfb5b40cf",
"sourceType": 2,
"sourceCode": "PO-2025-00004"
}
```
---
## Phase 1: Add Purchase Invoice (Draft)
**Handler:** AddPurchaseInvoiceCommandHandler
**Action:** Creates draft purchase invoice — no inventory or GL impact in this phase.
### Calculations (Before Saving)
**Line Item Calculations:**
- Quantity × Cost = Net Amount
600 × 10 = 6,000 SAR
- VAT Amount (if not included):
6,000 × 15% = 900 SAR
- Grand Total:
6,000 + 900 = 6,900 SAR
**Header Totals:**
- TotalNetAmount = 6,000 SAR
- TotalDiscount = 0 SAR
- TotalAfterDiscount = 6,000 SAR
- GrandTotal = 6,900 SAR
- LocalGrandTotal = 6,900 SAR (currencyRate = 1)
### Table 1: PurchaseInvoiceHeader (INSERT)
**Operation:** INSERT new record
| Column Name | Value | Source | Notes |
|--------------------|--------------------------------|---------------------------------|--------------------------------------------|
| Id | [Auto-Generated GUID] | System | e.g., `73b9e8d2-fe21-4ef7-2e68-08ddfb5b40cf` |
| Code | "PI-2026-0001" | Sequence Service | Generated by UpdateLastSequenceCommand |
| InvoiceDate | 2026-01-28 23:06:04 | Payload | Transaction date |
| InvoiceDueDate | calculated from payment term | PaymentTerm | Last installment date |
| VendorId | 44 | Payload | Vendor identifier |
| VendorName | "مؤسسة هدية الجودة للدعاية والاعلان" | Payload | Display name |
| WarehouseId | 53 | Payload | Receiving warehouse |
| Status | "Draft" | SetAsDraft() | Draft status |
| CurrencyId | 4 | Payload | Saudi Riyal |
| CurrencyRate | 1.0 | Payload | Exchange rate to base currency |
| TotalNetAmount | 6000.00 | CalculateHeaderTotals() | Sum of line net amounts |
| GrandTotal | 6900.00 | CalculateHeaderTotals() | Total + VAT |
| IsExternalPurchase | false | Payload | |
| CreatedDate | [Current Timestamp] | Audit | |
| CreatedBy | [Current User ID] | Security | |
| TenantId | [Tenant GUID] | Multi-tenancy | |
### Table 2: PurchaseInvoiceDetail (INSERT)
**Operation:** INSERT new record (one per line item)
| Column Name | Value | Source | Notes |
|--------------------|--------------------------------|---------------------------------|--------------------------------------------|
| Id | [Auto-Generated GUID] | System | e.g., `2b55acb1-180e-48d6-9107-08ddfb5b40dd` |
| PurchaseInvoiceHeaderId | [Header GUID] | FK | Parent invoice |
| ItemId | 169 | Payload | Inventory item |
| ItemCode | "4137" | Payload | |
| ItemName | "نوت وسط" | Payload | |
| ItemVariantId | 245 | Payload | |
| UomId | "56835e4d-4593-4481-9839-10a6321a42d3" | Payload | |
| Quantity | 600.0 | Payload | |
| Cost | 10.00 | Payload | Unit cost |
| SubCost | 6000.00 | Computed | |
| DiscountPercentage | 0 | Payload | |
| VatPercentage | 15.0 | Payload | |
| VatAmount | 900.00 | Computed | |
| TotalAfterDiscount | 6000.00 | Computed | |
| GrandTotal | 6900.00 | Computed | |
| TrackingType | "NoTracking" | Payload | |
| HasExpiryDate | false | Payload | |
| StockInEntryMode | "Manual" | Payload | |
| CreatedDate | [Current Timestamp] | Audit | |
| CreatedBy | [Current User ID] | Security | |
### Table 3: PurchaseInvoiceTracking (INSERT)
**Operation:** INSERT when detail.InvoiceTracking is provided (NoTracking record created if present)
| Column Name | Value | Source | Notes |
|--------------------|--------------------------------|---------------------------------|--------------------------------------------|
| Id | [Auto-Generated GUID] | System | |
| PurchaseInvoiceDetailId | [Detail GUID] | FK | |
| TrackingNo | NULL | NoTracking | |
| Quantity | 600.0 | Detail.Quantity | |
| ExpireDate | NULL | No expiry | |
| TrackingType | "NoTracking" | Detail.TrackingType | |
| CreatedDate | [Current Timestamp] | Audit | |
### Table 4: PurchaseInvoiceBalance (INSERT)
**Operation:** INSERT new record(s) per payment term installment
| Column Name | Value | Source | Notes |
|--------------------|--------------------------------|---------------------------------|--------------------------------------------|
| Id | [Auto-Generated GUID] | System | |
| PurchaseInvoiceHeaderId | [Header GUID] | FK | |
| DueDate | [From PaymentTermId 9] | Payment Term | |
| DueAmount | 6,900.00 | Payment Term % of GrandTotal | |
| DueBalance | 6,900.00 | Initially = DueAmount | |
| CreatedDate | [Current Timestamp] | Audit | |
### Summary of Add Phase (Draft)
**Tables Affected:**
- ✅ PurchaseInvoiceHeader — 1 INSERT
- ✅ PurchaseInvoiceDetail — 1 INSERT
- ✅ PurchaseInvoiceTracking — 1 INSERT (if tracking object present)
- ✅ PurchaseInvoiceBalance — 1 INSERT (installment record)
**Inventory Impact:** NONE — No stock movement in draft phase
**GL Impact:** NONE — No journal entries in draft phase
**Validations Performed:**
- ✅ Invoice date in open financial period
- ✅ Vendor exists and has payable account configured (validation step)
- ✅ Price/cost and tax metadata read from Tax table
- ✅ Payment term applied
## Phase 2: Post Purchase Invoice
**Handler:** PostPurchaseInvoiceCommandHandler
**Action:** Finalizes invoice, creates stock-in and accounting journal entries.
### Prerequisites Check (before posting)
1. ✅ Invoice Status = Draft
2. ✅ Period allows posted journals
3. ✅ Vendor has payable account configured
4. ✅ Item 169 has inventory asset account configured (for storable items)
5. ✅ Tax 18 has GL account configured
6. ✅ Warehouse 53 exists and accepts stock-in
### Table 1: PurchaseInvoiceHeader (UPDATE)
**Operation:** UPDATE existing record
| Column Name | Before (Draft) | After (Posted) | Change Description |
|--------------------|------------------------------|-------------------------------|------------------------------------------|
| Status | "Draft" | "Posted" | Status changed to Posted |
| StockInId | NULL | [StockIn GUID] | Reference to stock-in transaction |
| StockInCode | NULL | "SI-2026-0001" | Stock-in sequential code (example) |
| InvoiceJournalId | NULL | [Journal GUID] | Purchase journal entry ID |
| InvoiceJournalCode | NULL | "JE-2026-0200" | Journal entry code |
| PostedDate | NULL | 2026-01-28 23:10:00 | Timestamp of posting |
| UpdatedDate/By | [Original] | [Current] | Audit fields |
### Table 2: StockInHeader (INSERT - Inventory Module)
**Operation:** INSERT new record (created by Inventory service)
| Column Name | Value | Source | Notes |
|--------------------|--------------------------------|---------------------------------|--------------------------------------------|
| Id | [Auto-Generated GUID] | System | |
| Code | "SI-2026-0001" | Sequence Service | |
| TransactionDate | 2026-01-28 23:06:04 | Invoice.InvoiceDate | |
| WarehouseId | 53 | Invoice.WarehouseId | |
| SourceDocumentType | "PurchaseInvoice" | Fixed | |
| SourceDocumentId | [Invoice Header GUID] | Invoice.Id | |
| TotalCost | 6000.00 | Sum of detail costs | |
| JournalId | [Journal GUID] | Inventory journal created by Inventory service | |
| CreatedDate | 2026-01-28 23:10:00 | Audit | |
### Table 3: StockInDetail (INSERT - Inventory Module)
**Operation:** INSERT new record (per inventory line)
| Column Name | Value | Source | Notes |
|--------------------|--------------------------------|---------------------------------|--------------------------------------------|
| Id | [Auto-Generated GUID] | System | |
| StockInHeaderId | [Header GUID] | FK | |
| ItemId | 169 | Detail.ItemId | |
| ItemCode | "4137" | Detail.ItemCode | |
| ItemVariantId | 245 | Detail.ItemVariantId | |
| UomId | "56835e4d-4593-4481-9839-10a6321a42d3" | Detail.UomId | |
| Quantity | 600.0 | Detail.Quantity | |
| UnitCost | 10.00 | Detail.Cost | |
| LineCost | 6000.00 | Quantity × UnitCost | |
| TrackingType | "NoTracking" | Detail.TrackingType | |
| SourceDetailId | [Invoice Detail GUID] | Link to invoice line | |
| CreatedDate | 2026-01-28 23:10:00 | Audit | |
### Table 4: StockInTracking (INSERT)
**Operation:** INSERT new record (created by Inventory service)
| Column Name | Value | Source | Notes |
|--------------------|--------------------------------|---------------------------------|--------------------------------------------|
| Id | [Auto-Generated GUID] | System | |
| StockInDetailId | [StockIn Detail GUID] | FK | |
| TrackingNo | NULL | NoTracking | |
| Quantity | 600.0 | StockInDetail.Quantity | |
| UnitCost | 10.00 | Unit cost | |
| CreatedDate | 2026-01-28 23:10:00 | Audit | |
### Table 5: ItemStockDetail (INSERT / UPDATE - Inventory Module)
**Operation:** UPDATE or INSERT depending on existing stock record
**Before Post (example):**
- AvailableQuantity = 0 (no prior on-hand)
**After Post:**
- AvailableQuantity = 600.0 — increased by 600
- TotalCost = 6000.00
- UnitCost = 10.00 (WAC or last cost, depending on policy)
**Update Logic (example):**
- If existing ItemStockDetail exists for ItemVariantId 245 & WarehouseId 53, update AvailableQuantity += 600 and adjust TotalCost / UnitCost per costing method; otherwise insert a new ItemStockDetail record.
### Table 6: JournalEntry (INSERT - Purchase Journal)
**Operation:** INSERT new record (created by Accounting service)
| Column Name | Value | Source | Notes |
|--------------------|--------------------------------|---------------------------------|--------------------------------------------|
| Id | [Auto-Generated GUID] | System | |
| Code | "JE-2026-0200" | Sequence Service | |
| JournalDate | 2026-01-28 23:06:04 | Invoice.InvoiceDate | |
| SourceDocument | "PurchaseInvoice" | Fixed | |
| SourceDocumentId | [Invoice Header GUID] | Invoice.Id | |
| TotalDebit | 6900.00 | Sum debits | |
| TotalCredit | 6900.00 | Balanced totals | |
| FinancialYearPeriodId | [Period ID] | Invoice.FinancialYearPeriodId | |
| CreatedDate | 2026-01-28 23:10:00 | Audit | |
### Table 7: JournalEntryLine (INSERT Multiple - Purchase Journal)
**Operation:** INSERT 3 records (example)
**Line 1: Inventory Asset (Debit)**
- AccountId: 1030 (Inventory) — Debit 6000.00
**Line 2: VAT Input (Debit)**
- AccountId: 2040 (VAT Input/Recoverable) — Debit 900.00
**Line 3: Accounts Payable (Credit)**
- AccountId: 2010 (Vendor Payable) — Credit 6900.00
**Accounting Summary (JE-2026-0200)**
- DR Inventory 6,000.00
- DR VAT Input 900.00
- CR Vendor Payable 6,900.00
### Table 8: VendorFinancial (UPDATE)
**Operation:** UPDATE existing vendor financials
**Before Post:**
- OutstandingPayable = X
**After Post:**
- OutstandingPayable += 6,900.00
**Update SQL (example):**
```sql
UPDATE VendorFinancial
SET OutstandingPayable = OutstandingPayable + 6900.00,
TotalPurchases = TotalPurchases + 6000.00
WHERE VendorId = 44Summary of Post Phase
Tables Affected:
✅ PurchaseInvoiceHeader — 1 UPDATE (status, journal/stock references)
✅ StockInHeader — 1 INSERT (inventory transaction created by Inventory service)
✅ StockInDetail — 1 INSERT (line detail)
✅ StockInTracking — 1 INSERT (NoTracking record to store quantity)
✅ ItemStockDetail — 1 INSERT/UPDATE (increased AvailableQuantity)
✅ JournalEntry — 1 INSERT (Purchase journal in Accounting service)
✅ JournalEntryLine — 3 INSERTS (Inventory, VAT, Payable)
✅ VendorFinancial — 1 UPDATE (outstanding payable increased)
Inventory Impact:
Item: 4137 (نوت وسط)
Warehouse: Default Warehouse (53)
Before: Available Qty = (example) 0
After: Available Qty = 600
GL Impact (Combined Journals):
DR Inventory (Asset) +6,000.00
DR VAT Input (Asset/Recoverable) +900.00
CR Vendor Payable (Liability) +6,900.00
Complete Transaction Flow Summary
Step-by-Step Execution
User submits Add Purchase Invoice request
AddPurchaseInvoiceCommandHandler.Handle()
Validate invoice date → Get FinancialYearPeriodId
Validate vendor accounting setup, tax metadata, UOM & item existence
Generate sequence code → "PI-2026-0001"
Calculate line totals and header totals
Create payment term balances (DueDate(s), DueAmount(s))
INSERT PurchaseInvoiceHeader (Status = Draft)
INSERT PurchaseInvoiceDetail (line(s))
INSERT PurchaseInvoiceTracking (if present)
INSERT PurchaseInvoiceBalance (installment(s))
SaveChanges → Database transaction committed (Draft returned)
User triggers Post request
PostPurchaseInvoiceCommandHandler.Handle()
Validate preconditions (status, period, vendor accounts, tax GL)
Build purchase journal payload and call Accounting service → returns JournalId/Code
- Accounting creates JournalEntry and JournalEntryLines
- Call Inventory service AddPostedStockIn() → returns StockInId/Code and Inventory Journal references
- Inventory creates StockInHeader, StockInDetail, StockInTracking, updates ItemStockDetail
Update PurchaseInvoiceHeader with StockIn and Journal references, set Status = Posted, set PostedDate
Update VendorFinancial (Outstanding payable)
SaveChanges → All references persisted
Publish webhook/event (if enabled)
Return InvoiceId → Frontend receives posted invoice
Database Transaction Isolation
Add Phase (Draft)
- Single DB transaction: INSERT header, details, balances → COMMIT
Post Phase
- Main transaction in Purchase.Invoice service:
- Call Accounting.AddJournal (external)
- Call Inventory.AddPostedStockIn (external)
- Update PurchaseInvoiceHeader, VendorFinancial
- COMMIT
- External services (Accounting, Inventory) run their own transactions and return identifiers which are persisted by the Purchase service.
Error Scenarios
Add Phase Validation Failures
Invalid Period → "Invoice date must be in an open financial period"
Missing Vendor → "Vendor not found"
Price/Cost missing → "Item cost is required for storable items"
Missing Tax metadata → "Tax configuration for taxId 18 is missing"
Post Phase Validation Failures
Invoice Not Draft → "Invoice must be in draft status to post"
Missing Vendor Payable Account → "Vendor payable account not configured"
Missing Item Inventory Account → "Inventory GL account not configured for item 169"
Missing Tax GL Account → "Tax account required for tax 18"
Period Closed → "Cannot post journals in closed period"
End of Walkthrough
This example demonstrates a typical purchase invoice with:
✅ Single storable item (NoTracking)
✅ Direct purchase (possibly from PO)
✅ No discount on line
✅ VAT not included (calculated separately)
✅ Single payment installment (payment term id 9)
✅ Inventory stock-in on post
✅ Purchase journal created (Inventory + VAT Debit, Vendor Payable Credit)
Document Version: 1.0
Example Date: 2026-01-28
Currency: Saudi Riyal (SAR)
Total Amount: 6,900.00 SAR (including 15% VAT)