Appearance
Purchase Return Invoice Complete Walkthrough - Example-Based Documentation
Overview
This document demonstrates the complete lifecycle of a purchase return invoice using a real payload example, showing exactly which tables and columns are affected during both Add (Draft) and Post phases.
A purchase return invoice (Return Invoice) references a previously-posted purchase invoice and returns some or all of the purchased goods back to the vendor. The system copies item data, costs, taxes, and tracking information from the original invoice — the user only needs to specify which lines to return and how many units.
Example Payload
json
{
"invoiceHeaderId": "73b9e8d2-fe21-4ef7-2e68-08ddfb5b40cf",
"returnInvoiceDate": "2026-02-05T10:00:00",
"description": null,
"returnInvoiceDetails": [
{
"invoiceDetailId": "2b55acb1-180e-48d6-9107-08ddfb5b40dd",
"toReturnQuantity": 200
}
]
}Context: This return references the purchase invoice
PI-2026-0001(600 units of "نوت وسط" @ 10 SAR each). The user is returning 200 of those 600 units.
Phase 1: Add Return Invoice (Draft)
Handler: AddReturnInvoiceCommandHandler
Action: Creates draft return invoice — no inventory or GL impact in this phase. Item data, cost, discount, and tax information are copied from the original invoice detail.
Calculations (Before Saving)
Line Item Calculations (copied from original invoice, recalculated for return quantity):
OriginalQuantity = 600 (from original invoice detail)
ToReturnQuantity = 200 (user input)
TransactionRemainQuantity = current available stock checked at creation time
SubCost = ToReturnQuantity × Cost
200 × 10 = 2,000 SARDiscountAmount = Cost × (DiscountPercentage / 100)
10 × (0 / 100) = 0 SARNetAmount = Cost − DiscountAmount
10 − 0 = 10 SARTotalAfterDiscount = NetAmount × ToReturnQuantity
10 × 200 = 2,000 SARVatAmount (if not included):
2,000 × 15% = 300 SARGrandTotal = TotalAfterDiscount + VatAmount
2,000 + 300 = 2,300 SAR
Header Totals:
- TotalNetAmount = 2,000 SAR
- TotalDiscount = 0 SAR
- TotalAfterDiscount = 2,000 SAR
- GrandTotal = 2,300 SAR
- LocalGrandTotal = 2,300 SAR (currencyRate = 1)
Table 1: ReturnInvoiceHeader (INSERT)
Operation: INSERT new record
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | |
| Code | "RI-2026-0001" | Sequence Service | Generated by UpdateLastSequenceCommand |
| ReturnInvoiceDate | 2026-02-05 10:00:00 | Payload | Must be ≥ original invoice date |
| InvoiceHeaderId | 73b9e8d2-fe21-4ef7-2e68-08ddfb5b40cf | Payload | Reference to original purchase invoice |
| WarehouseId | 53 | Copied from InvoiceHeader | SetHeaderData() copies from original |
| WarehouseName | "Default Warehouse" | Copied from InvoiceHeader | |
| VendorId | 44 | Copied from InvoiceHeader | |
| VendorName | "مؤسسة هدية الجودة للدعاية والاعلان" | Copied from InvoiceHeader | |
| CurrencyId | 4 | Copied from InvoiceHeader | Saudi Riyal |
| CurrencyRate | 1.0 | Copied from InvoiceHeader | |
| PaymentTermId | 9 | Copied from InvoiceHeader | |
| Status | "Draft" | SetAsDraft() | Draft status |
| TotalNetAmount | 2000.00 | CalculateHeaderTotals() | Sum of detail net amounts |
| TotalDiscount | 0.00 | CalculateHeaderTotals() | |
| TotalAfterDiscount | 2000.00 | CalculateHeaderTotals() | |
| GrandTotal | 2300.00 | CalculateHeaderTotals() | Total + VAT |
| LocalGrandTotal | 2300.00 | CalculateHeaderTotals() | GrandTotal × CurrencyRate |
| FinancialYearPeriodId | [Period ID] | Period Validation | Current open financial period |
| Description | NULL | Payload | |
| CreatedDate | [Current Timestamp] | Audit | |
| CreatedBy | [Current User ID] | Security | |
| TenantId | [Tenant GUID] | Multi-tenancy |
Table 2: ReturnInvoiceDetail (INSERT)
Operation: INSERT new record (one per return line)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | |
| ReturnInvoiceHeaderId | [Header GUID] | FK | Parent return invoice |
| InvoiceDetailId | 2b55acb1-180e-48d6-9107-08ddfb5b40dd | Payload / Copied from Detail | Reference to original invoice line |
| ItemId | 169 | Copied from InvoiceDetail | SetDetailData() copies all item info |
| ItemCode | "4137" | Copied from InvoiceDetail | |
| ItemName | "نوت وسط" | Copied from InvoiceDetail | |
| ItemVariantId | 245 | Copied from InvoiceDetail | |
| UomId | "56835e4d-4593-4481-9839-10a6321a42d3" | Copied from InvoiceDetail | |
| ToReturnQuantity | 200.0 | Payload | User-specified return quantity |
| OriginalQuantity | 600.0 | Copied from InvoiceDetail | Original invoice quantity |
| TransactionRemainQuantity | [Available Qty in Stock] | Stock check at creation | Quantity available for return at time of creation |
| Cost | 10.00 | Copied from InvoiceDetail | Unit cost from original invoice |
| SubCost | 2000.00 | Computed — SetSubCost() | ToReturnQuantity × Cost |
| DiscountPercentage | 0 | Copied from InvoiceDetail | |
| DiscountAmount | 0.00 | Computed | Cost × (DiscountPercentage / 100) |
| NetAmount | 10.00 | Computed | Cost − DiscountAmount |
| TotalAfterDiscount | 2000.00 | Computed | NetAmount × ToReturnQuantity |
| VatPercentage | 15.0 | Copied from InvoiceDetail | |
| VatAmount | 300.00 | Computed — CalculateTotalVatAmount() | Sum of all tax amounts on this line |
| GrandTotal | 2300.00 | Computed — CalculateGrandTotal() | TotalAfterDiscount + VatAmount |
| LocalGrandTotal | 2300.00 | Computed | GrandTotal × CurrencyRate |
| TrackingType | "NoTracking" | Copied from InvoiceDetail | |
| HasExpiryDate | false | Copied from InvoiceDetail | |
| InvoiceEntryMode | "Manual" | Copied from InvoiceDetail | |
| CreatedDate | [Current Timestamp] | Audit | |
| CreatedBy | [Current User ID] | Security |
Table 3: ReturnInvoiceDetailTax (INSERT)
Operation: INSERT tax records (one per tax on the detail line, copied from original invoice detail)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | |
| ReturnInvoiceDetailId | [Detail GUID] | FK | |
| TaxId | 18 | Copied from InvoiceDetailTax | |
| TaxCalculationEffect | "Add" | Copied from InvoiceDetailTax | Add = increases total; Deduct = decreases total |
| TaxValueType | "Percent" | Copied from InvoiceDetailTax | Percent or Amount |
| VatPercentage | 15.0 | Copied | |
| VatAmount | 300.00 | Computed — CalculateVatAmount() | TotalAfterDiscount × VatPercentage |
| LocalVatAmount | 300.00 | Computed | VatAmount × CurrencyRate |
| CreatedDate | [Current Timestamp] | Audit |
Table 4: ReturnInvoiceTracking (INSERT)
Operation: INSERT when detail.TrackingType = "NoTracking" (a NoTracking record is created to store quantity)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | |
| ReturnInvoiceDetailId | [Detail GUID] | FK | |
| TrackingType | "NoTracking" | Detail.TrackingType | |
| Quantity | 200.0 | Detail.ToReturnQuantity | |
| VendorBatchNo | NULL | NoTracking | |
| ExpireDate | NULL | No expiry | |
| HasExpiryDate | false | Detail.HasExpiryDate | |
| CreatedDate | [Current Timestamp] | Audit |
Summary of Add Phase (Draft)
Tables Affected:
- ✅ ReturnInvoiceHeader — 1 INSERT
- ✅ ReturnInvoiceDetail — 1 INSERT
- ✅ ReturnInvoiceDetailTax — 1 INSERT (one per tax on the line)
- ✅ ReturnInvoiceTracking — 1 INSERT (if NoTracking entry)
Inventory Impact: NONE — No stock movement in draft phase
GL Impact: NONE — No journal entries in draft phase
Key Difference from Purchase Invoice Add:
Unlike adding a purchase invoice (where the user provides all item/cost data), adding a return invoice only requires the original InvoiceDetailId and the return quantity. All item data (ItemId, Cost, DiscountPercentage, VatPercentage, TrackingType, etc.) is copied automatically from the original invoice detail via SetDetailData(). There is also no payment balance table (PurchaseInvoiceBalance) for return invoices.
Validations Performed:
- ✅ Return invoice date must be ≥ original invoice date
- ✅ Return invoice date in open financial period
- ✅ At least one return detail line with ToReturnQuantity > 0
- ✅ ToReturnQuantity ≤ available quantity in stock (checked per detail)
- ✅ Original invoice detail exists on the referenced invoice
Phase 2: Post Return Invoice
Handler: PostReturnInvoiceCommandHandler
Action: Finalizes return invoice, creates stock-out (goods leave warehouse back to vendor) and reverse accounting journal entries.
Prerequisites Check (before posting)
- ✅ Return Invoice Status = Draft
- ✅ ReturnInvoiceDate ≥ original InvoiceDate
- ✅ Period allows posted journals
- ✅ Vendor has payable account configured
- ✅ Item 169 has inventory asset account configured (for storable items)
- ✅ Tax 18 has GL account configured
Table 1: ReturnInvoiceHeader (UPDATE)
Operation: UPDATE existing record
| Column Name | Before (Draft) | After (Posted) | Change Description |
|---|---|---|---|
| Status | "Draft" | "Posted" | Status changed to Posted via SetAsPosted() |
| StockOutId | NULL | [StockOut GUID] | Reference to stock-out transaction |
| StockOutCode | NULL | "SO-2026-0001" | Stock-out sequential code (example) |
| StockOutJournalId | NULL | [StockOut Journal GUID] | Journal created by Inventory service |
| StockOutJournalCode | NULL | "JE-2026-0201" | Stock-out journal code |
| InvoiceJournalId | NULL | [Journal GUID] | Purchase return journal entry ID |
| InvoiceJournalCode | NULL | "JE-2026-0202" | Purchase return journal entry code |
| PostedDate | NULL | 2026-02-05 10:05:00 | Timestamp of posting |
| UpdatedDate/By | [Original] | [Current] | Audit fields |
Table 2: StockOutHeader (INSERT - Inventory Module)
Operation: INSERT new record (created by Inventory service via InternalAddPostedStockOutCommand)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | |
| Code | "SO-2026-0001" | Sequence Service | |
| TransactionDate | 2026-02-05 10:00:00 | ReturnInvoice.ReturnInvoiceDate | |
| WarehouseId | 53 | ReturnInvoice.WarehouseId | |
| SourceDocumentType | "ReturnPurchaseInvoice" | Fixed | Identifies this is from a purchase return |
| SourceDocumentId | [Return Invoice Header GUID] | ReturnInvoice.Id | |
| TotalCost | 2000.00 | Sum of detail total after discount | |
| JournalId | [StockOut Journal GUID] | Inventory journal from Inventory service | |
| CreatedDate | 2026-02-05 10:05:00 | Audit |
Table 3: StockOutDetail (INSERT - Inventory Module)
Operation: INSERT new record (per inventory line)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | |
| StockOutHeaderId | [StockOut Header GUID] | FK | |
| ItemId | 169 | Detail.ItemId | |
| ItemCode | "4137" | Detail.ItemCode | |
| ItemVariantId | 245 | Detail.ItemVariantId | |
| UomId | "56835e4d-4593-4481-9839-10a6321a42d3" | Detail.UomId | |
| Quantity | 200.0 | Detail.ToReturnQuantity | Quantity leaving the warehouse |
| UnitCost | 10.00 | Detail.Cost | |
| LineCost | 2000.00 | Quantity × UnitCost | |
| TrackingType | "NoTracking" | Detail.TrackingType | |
| SourceDetailId | [Return Invoice Detail GUID] | Link to return invoice line | |
| CreatedDate | 2026-02-05 10:05:00 | Audit |
Table 4: StockOutTracking (INSERT - Inventory Module)
Operation: INSERT new record (NoTracking record to store quantity)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | |
| StockOutDetailId | [StockOut Detail GUID] | FK | |
| TrackingNo | NULL | NoTracking | |
| Quantity | 200.0 | StockOutDetail.Quantity | |
| UnitCost | 10.00 | Unit cost | |
| CreatedDate | 2026-02-05 10:05:00 | Audit |
Table 5: ItemStockDetail (UPDATE - Inventory Module)
Operation: UPDATE existing stock record (goods leave warehouse)
Before Post (example):
- AvailableQuantity = 600.0 (all units from original purchase invoice still on hand)
After Post:
- AvailableQuantity = 400.0 — decreased by 200 (returned to vendor)
- TotalCost adjusted accordingly
Update Logic (example):
- Find existing ItemStockDetail for ItemVariantId 245 & WarehouseId 53
- AvailableQuantity -= 200
- TotalCost -= 2,000.00 (UnitCost × ReturnQuantity)
- UnitCost recalculated per costing method (WAC or last cost)
Table 6: JournalEntry (INSERT - Purchase Return Journal)
Operation: INSERT new record (created by Accounting service)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | |
| Code | "JE-2026-0202" | Sequence Service | |
| JournalDate | 2026-02-05 10:00:00 | ReturnInvoice.ReturnInvoiceDate | |
| SourceDocument | "ReturnPurchaseInvoice" | Fixed | |
| SourceDocumentId | [Return Invoice Header GUID] | ReturnInvoice.Id | |
| TotalDebit | 2300.00 | Sum debits | Balanced entry |
| TotalCredit | 2300.00 | Sum credits | |
| FinancialYearPeriodId | [Period ID] | ReturnInvoice.FinancialYearPeriodId | |
| CreatedDate | 2026-02-05 10:05:00 | Audit |
Table 7: JournalEntryLine (INSERT Multiple - Purchase Return Journal)
Operation: INSERT 3 records (example — 1 vendor line + 1 stock/GIT line + 1 VAT line)
Line 1: Vendor Payable (Debit)
- AccountId: 2010 (Vendor Payable) — Debit 2,300.00
- Source: PostReturnInvoiceCommandHandler — Vendor line uses GrandTotal (TotalAfterDiscount + taxes)
Line 2: Inventory / Goods-In-Transit (Credit)
- AccountId: 1030 (Inventory Asset / GIT) — Credit 2,000.00
- Source: PostReturnInvoiceCommandHandler — Stock line uses TotalAfterDiscount (excl. VAT)
Line 3: VAT Input (Credit)
- AccountId: 2040 (VAT Input/Recoverable) — Credit 300.00
- Source: PostReturnInvoiceCommandHandler — Tax line per tax account; Add-effect taxes are credited (reversing the input VAT originally claimed on the purchase)
Accounting Summary (JE-2026-0202)
- DR Vendor Payable 2,300.00
- CR Inventory Asset 2,000.00
- CR VAT Input 300.00
Note on Tax Lines: Tax lines are grouped by GL account and tax calculation effect. If a tax has
TaxCalculationEffect = Add, the line is Credit (reverses the input VAT recovered on the original purchase). IfTaxCalculationEffect = Deduct, the line is Debit.
Summary of Post Phase
Tables Affected:
- ✅ ReturnInvoiceHeader — 1 UPDATE (status, stock-out and journal references)
- ✅ StockOutHeader — 1 INSERT (inventory transaction created by Inventory service)
- ✅ StockOutDetail — 1 INSERT (line detail)
- ✅ StockOutTracking — 1 INSERT (NoTracking record to store quantity)
- ✅ ItemStockDetail — 1 UPDATE (decreased AvailableQuantity by 200)
- ✅ JournalEntry — 1 INSERT (Purchase Return journal in Accounting service)
- ✅ JournalEntryLine — 3 INSERTS (Vendor Payable debit, Inventory credit, VAT credit)
Inventory Impact:
- Item: 4137 (نوت وسط)
- Warehouse: Default Warehouse (53)
- Before: Available Qty = 600
- After: Available Qty = 400 (200 returned to vendor)
GL Impact (Combined Journals):
- DR Vendor Payable (Liability) −2,300.00 (reduces outstanding payable to vendor)
- CR Inventory Asset −2,000.00 (goods leave warehouse)
- CR VAT Input −300.00 (reverses recoverable VAT from original purchase)
Complete Transaction Flow Summary
Step-by-Step Execution
User submits Add Return Invoice request with InvoiceHeaderId and return details
AddReturnInvoiceCommandHandler.Handle()
Fetch original PurchaseInvoiceHeader (with details, tracking, taxes) by InvoiceHeaderId
Validate return invoice date ≥ original invoice date
Get current FinancialYearPeriodId (validate date is in open period)
Map command to ReturnInvoiceHeader entity; call SetHeaderData() to copy warehouse/vendor/currency/payment term from original invoice
For each ReturnInvoiceDetail:
a. Fetch original InvoiceDetail by InvoiceDetailId
b. Call SetDetailData() → copies ItemId, Cost, Discount, VatPercentage, TrackingType, etc.
c. Set ToReturnQuantity and OriginalQuantity
d. Check available stock quantity → SetTransactionRemainQuantity()
e. Validate ToReturnQuantity ≤ available quantity
f. CalculateAllFields(currencyRate) → SubCost, DiscountAmount, NetAmount, TotalAfterDiscount, VatAmount, GrandTotal
g. AddTaxesFromInvoice() → copies ReturnInvoiceDetailTax records from original detail taxesValidate at least one detail line has ToReturnQuantity > 0
CalculateHeaderTotals() → aggregate TotalNetAmount, TotalAfterDiscount, GrandTotal
Generate sequence code → "RI-2026-0001"
SetAsDraft() → Status = Draft
INSERT ReturnInvoiceHeader, ReturnInvoiceDetail(s), ReturnInvoiceDetailTax(es), ReturnInvoiceTracking(s)
SaveChanges → Database transaction committed (Draft returned)
User triggers Post request with ReturnInvoiceHeader Id
PostReturnInvoiceCommandHandler.Handle()
Fetch ReturnInvoiceHeader with all details, taxes, and tracking
Validate preconditions (Status = Draft, date check, vendor payable account, period open)
Build InternalAddPostedStockOutCommand (mapped via AutoMapper from ReturnInvoiceHeader/Detail/Tracking)
Call Inventory service AddPostedStockOut() → returns StockOutId, StockOutCode, StockOutJournalId, StockOutJournalCode
- Inventory creates StockOutHeader, StockOutDetail, StockOutTracking, decreases ItemStockDetail
Build purchase return journal payload (Vendor debit line, Stock/GIT credit line, Tax lines grouped by account)
Call Accounting service AddJournal() → returns InvoiceJournalId, InvoiceJournalCode
- Accounting creates JournalEntry and JournalEntryLines
Call SetJournalData() on ReturnInvoiceHeader → stores StockOut and Journal IDs/Codes
SetAsPosted() → Status = Posted, PostedDate = now
SaveChanges → All references persisted
Return ReturnInvoiceId → Frontend receives posted return invoice
Database Transaction Isolation
Add Phase (Draft)
- Single DB transaction: INSERT header, details, detail taxes, tracking → COMMIT
Post Phase
- Semaphore lock acquired before processing (prevents duplicate posts)
- Call Inventory.AddPostedStockOut (external) → returns StockOutId/Code and StockOut Journal references
- Call Accounting.AddJournal (external) → returns InvoiceJournalId/Code
- Update ReturnInvoiceHeader with all references, set Status = Posted
- COMMIT
- External services (Inventory, Accounting) run their own transactions and return identifiers persisted by the Return Invoice service
Error Scenarios
Add Phase Validation Failures
- Return Date Before Invoice Date → "Return invoice date must be on or after the original invoice date"
- Invalid Period → "Invoice date must be in an open financial period"
- No Valid Details → "At least one return detail with quantity greater than zero is required"
- Quantity Exceeds Available → "Return quantity exceeds available quantity in stock"
- Original Invoice Not Found → "Referenced invoice header not found"
- Original Invoice Detail Not Found → "Invoice detail not found on the referenced invoice"
Post Phase Validation Failures
- Invoice Not Draft → "Invoice must be in draft status to post"
- Return Date Before Original Invoice Date → "Return invoice date must be on or after the original invoice date"
- 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"
- Stock Out Fails → Inventory service error propagated (stock out not created, header not updated)
Key Differences: Purchase Invoice vs. Return Invoice
| Aspect | Purchase Invoice | Return Invoice |
|---|---|---|
| Direction | Goods IN from vendor | Goods OUT back to vendor |
| Payload content | Full item, cost, tax, and warehouse data | Only InvoiceHeaderId, InvoiceDetailId, ToReturnQuantity |
| Item/cost data source | User input | Copied automatically from original invoice |
| Balance table | PurchaseInvoiceBalance (payment installments) | None |
| Inventory operation | StockIn (goods arrive at warehouse) | StockOut (goods leave warehouse) |
| Inventory quantity | AvailableQuantity += quantity | AvailableQuantity −= quantity |
| GL — Inventory account | Debit (asset increases) | Credit (asset decreases) |
| GL — Vendor Payable | Credit (liability increases — we owe more) | Debit (liability decreases — we owe less) |
| GL — VAT Input | Debit (recoverable VAT increases) | Credit (recoverable VAT decreases) |
| Code prefix | PI- | RI- |
End of Walkthrough
This example demonstrates a typical purchase return invoice with:
- ✅ Single storable item (NoTracking) — partial return (200 of 600 units)
- ✅ References original purchase invoice PI-2026-0001
- ✅ No discount on line
- ✅ VAT not included in cost (calculated separately at 15%)
- ✅ Inventory stock-out on post (200 units removed from warehouse)
- ✅ Purchase return journal created (Vendor Payable debit, Inventory + VAT Input credit)
Document Version: 1.0
Example Date: 2026-02-05
Currency: Saudi Riyal (SAR)
Total Return Amount: 2,300.00 SAR (including 15% VAT on 200 units @ 10 SAR)