Appearance
Return Sales Invoice Complete Walkthrough - Example-Based Documentation
Overview
This document demonstrates the complete lifecycle of a return sales invoice using a real payload example, showing exactly which tables and columns are affected during both Add (Draft) and Post phases.
A return sales invoice is always linked to an existing (posted) sales invoice. Most header and detail data is inherited from the original invoice — the payload only specifies which lines to return and the quantities.
Example Payload
json
{
"returnInvoiceDate": "2026-01-30T10:00:00",
"salesInvoiceHeaderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"description": "Customer return - wrong size",
"warehouseId": 48,
"warehouseName": "Default Warehouse 1",
"returnSalesInvoiceDetails": [
{
"salesInvoiceDetailId": "7a93c8e2-1234-5678-9abc-def012345678",
"toReturnQuantity": 1
}
]
}Original Invoice Reference: SI-2026-0001
- Item: IDEF_00004 (وشاح), Qty 1, Price 1000 SAR, VAT 15% = 150, Grand Total 1150 SAR
- Item Cost (WAC): 99.64 SAR
Phase 1: Add Return Sales Invoice (Draft)
Handler: AddReturnSalesInvoiceCommandHandlerAction: Creates draft return invoice; no inventory or GL impact
Pre-Processing Steps
1. Resolve FinancialYearPeriodId for ReturnInvoiceDate (2026-01-30)
2. Fetch original SalesInvoiceHeader (with Details, Tracking, Taxes)
3. Call IInventoryPublicApi.GetItemStockDetailsBySource()
→ Verify available return quantity from stock (StockOut records)
4. Filter details: only keep items where ToReturnQuantity > 0
5. Copy detail data from original invoice line (price, cost, UOM, tracking, taxes)
6. Calculate all amounts per line
7. Calculate header totals
8. SetAsDraft(), SetZatcaStatusAsNotSend(), SetEtaStatusAsPending()Calculations (Before Saving)
Line Item Calculations (inherited price/cost from original invoice):
ToReturnQuantity × Price = SubPrice
1 × 1000 = 1000 SAR (SubPrice)
DiscountAmount (inherited 0%):
1000 × 0% = 0 SAR
NetAmount = SubPrice - DiscountAmount:
1000 - 0 = 1000 SAR
VAT Amount (TaxEffect = Add, 15%):
1000 × 15% = 150 SAR
GrandTotal:
1000 + 150 = 1150 SAR
Header Totals:
TotalNetAmount = 1000 SAR (Price × ReturnQty)
TotalDiscount = 0 SAR
TotalAfterDiscount = 1000 SAR
GrandTotal = 1150 SAR
LocalGrandTotal = 1150 SAR (CurrencyRate = 1)Table 1: ReturnSalesInvoiceHeader (INSERT)
Operation: INSERT new record
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | e.g., a1b2c3d4-1111-2222-3333-444455556666 |
| Code | "RSI-2026-0001" | Sequence Service | Screen = ReturnSalesInvoice, Module = Sales |
| ReturnInvoiceDate | 2026-01-30 10:00:00 | Payload | Transaction date |
| Description | "Customer return - wrong size" | Payload | Optional notes |
| Status | "Draft" | SetAsDraft() | TransactionStatus enum |
| WarehouseId | 48 | Payload | Receiving warehouse |
| WarehouseName | "Default Warehouse 1" | Payload | Display name |
| SalesInvoiceHeaderId | 3fa85f64-5717-4562-b3fc-2c963f66afa6 | Payload | FK to original invoice |
| CustomerId | 433 | Copied from SalesInvoiceHeader | Customer identifier |
| CustomerName | "dubai " | Copied from SalesInvoiceHeader | Customer display name |
| CurrencyId | 4 | Copied from SalesInvoiceHeader | Transaction currency |
| CurrencyName | "Saudi Riyal" | Copied from SalesInvoiceHeader | Currency display name |
| CurrencyRate | 1.0 | Copied from SalesInvoiceHeader | Exchange rate |
| PaymentTermId | 38 | Copied from SalesInvoiceHeader | Payment schedule template |
| PricePolicyId | 42 | Copied from SalesInvoiceHeader | Active price policy |
| SalesManId | NULL | Copied from SalesInvoiceHeader | No salesman assigned |
| SalesManName | NULL | Copied from SalesInvoiceHeader | No salesman name |
| Reference | NULL | Copied from SalesInvoiceHeader | Original invoice reference |
| FinancialYearPeriodId | [Period ID] | GetCurrentPeriodId() | e.g., 25 for Jan 2026 period |
| TotalDiscount | 0 | CalculateHeaderTotals() | Sum of line discounts |
| LocalTotalDiscount | 0 | × CurrencyRate | Base currency discount |
| TotalNetAmount | 1000 | CalculateHeaderTotals() | Sum of (Price × ReturnQty) |
| LocalTotalNetAmount | 1000 | × CurrencyRate | Base currency total net |
| TotalAfterDiscount | 1000 | CalculateHeaderTotals() | Net - Discount |
| LocalTotalAfterDiscount | 1000 | × CurrencyRate | Base currency total |
| LocalServiceTotalAfterDiscount | 0 | CalculateHeaderTotals() | Service items subtotal |
| GrandTotal | 1150 | CalculateHeaderTotals() | Total + VAT |
| LocalGrandTotal | 1150 | × CurrencyRate | Base currency grand total |
| StockInId | NULL | Draft state | Set on post |
| StockInCode | NULL | Draft state | Set on post |
| InvoiceJournalId | NULL | Draft state | Set on post |
| InvoiceJournalCode | NULL | Draft state | Set on post |
| StockInJournalId | NULL | Draft state | Set on post |
| StockInJournalCode | NULL | Draft state | Set on post |
| ZatcaStatus | "NotSend" | SetZatcaStatusAsNotSend() | Saudi E-Invoicing status |
| ZatcaErrorMessage | NULL | Default | No ZATCA error |
| ZatcaWarningMessage | NULL | Default | No ZATCA warning |
| ZatcaLastTryDate | [Default] | Default | No ZATCA attempt yet |
| EtaUploadStatus | "Pending" | SetEtaStatusAsPending() | ETA status |
| EtaErrorMessage | NULL | Default | No ETA error |
| PrintCount | 0 | Default | Not printed |
| POSSessionId | NULL | Not POS | POS session ref |
| ExternalIdentifier | NULL | Payload (null) | External system ref |
| ExternalCode | NULL | Payload (null) | External return code |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
| CreatedBy | [Current User ID] | Security | From ISecurityService |
| TenantId | [Tenant GUID] | Multi-tenancy | From ISecurityService |
| IsDeleted | false | Default | Soft delete flag |
Table 2: ReturnSalesInvoiceDetail (INSERT)
Operation: INSERT new record (one per line item)
Note: All item metadata (name, code, prices, UOM, tracking) is copied from the corresponding SalesInvoiceDetail via SetDetailData().
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | e.g., b2c3d4e5-5555-6666-7777-888899990000 |
| ReturnSalesInvoiceHeaderId | [Return Header GUID above] | FK | Parent return invoice |
| SalesInvoiceDetailId | 7a93c8e2-1234-5678-9abc-def012345678 | Payload | Link to original detail line |
| ItemId | 422 | Copied from SalesInvoiceDetail | Inventory item |
| ItemCode | "IDEF_00004" | Copied from SalesInvoiceDetail | Item code |
| ItemName | "وشاح" | Copied from SalesInvoiceDetail | Item name |
| ItemVariantId | 543 | Copied from SalesInvoiceDetail | Item variant |
| ItemVariantCode | "0000" | Copied from SalesInvoiceDetail | Variant code |
| ItemVariantNameEn | "default category for inventory items" | Copied from SalesInvoiceDetail | Variant name EN |
| ItemVariantNameAr | "-" | Copied from SalesInvoiceDetail | Variant name AR |
| CategoryId | 109 | Copied from SalesInvoiceDetail | Item category |
| ItemCategoryNameEn | "default category for inventory items" | Copied from SalesInvoiceDetail | Category name EN |
| ItemCategoryNameAr | "الفئة الافتراضية للاصناف المخزنية" | Copied from SalesInvoiceDetail | Category name AR |
| CategoryType | "Storable" | Copied from SalesInvoiceDetail | CategoryType enum |
| IsServiceItem | false | Copied from SalesInvoiceDetail | Not a service |
| IsProductItem | true | Copied from SalesInvoiceDetail | Storable item |
| UomId | 79415be3-345f-4d51-87ac-84ec87e7a0b0 | Copied from SalesInvoiceDetail | Unit of measure |
| UOMCode | "G8roW" | Copied from SalesInvoiceDetail | UOM code |
| UOMNameEn | "BaseUOM" | Copied from SalesInvoiceDetail | UOM name EN |
| UOMNameAr | "وحدة القياس الأساسية" | Copied from SalesInvoiceDetail | UOM name AR |
| ToReturnQuantity | 1.0 | Payload | Quantity being returned |
| OriginalQuantity | 1.0 | Copied from SalesInvoiceDetail | Original sale quantity |
| TransactionRemainQuantity | 1.0 | GetItemStockDetailsBySource() | Available for return at time of creation |
| Price | 1000.00 | Copied from SalesInvoiceDetail | Original unit price (SAR) |
| SubPrice | 1000.00 | SetSubPrice() | OrginSubPrice / OriginalQty × ReturnQty |
| Cost | 99.64 | Copied from SalesInvoiceDetail | Unit cost (COGS) |
| SubCost | 99.64 | SetSubCost() | ReturnQty × Cost |
| DiscountPercentage | 0 | Copied from SalesInvoiceDetail | Inherited discount |
| DiscountAmount | 0 | CalculateDiscountAmount() | SubPrice × DiscountPercentage% |
| LocalDiscountAmount | 0 | × CurrencyRate | Base currency discount |
| NetAmount | 1000.00 | CalculateNetAmount() | SubPrice - DiscountAmount |
| LocalNetAmount | 1000.00 | × CurrencyRate | Base currency net |
| TotalAfterDiscount | 1000.00 | CalculateTotalAfterDiscount() | = NetAmount |
| LocalTotalAfterDiscount | 1000.00 | × CurrencyRate | Base currency total |
| IsVatIncluded | false | Copied from SalesInvoiceDetail | VAT not in price |
| VatAmount | 150.00 | CalculateTotalVatAmount() | 1000 × 15% |
| LocalVatAmount | 150.00 | × CurrencyRate | Base currency VAT |
| GrandTotal | 1150.00 | CalculateGrandTotal() | TotalAfterDiscount + VatAmount |
| LocalGrandTotal | 1150.00 | × CurrencyRate | Base currency grand total |
| BarCode | "" (empty) | Copied from SalesInvoiceDetail | No barcode |
| BarCodeId | NULL | Copied from SalesInvoiceDetail | No barcode |
| TrackingType | "NoTracking" | Copied from SalesInvoiceDetail | Tracking method |
| HasExpiryDate | false | Copied from SalesInvoiceDetail | No expiry |
| InvoiceEntryMode | "Manual" | Copied from SalesInvoiceDetail | Entry mode |
| Description | "(IDEF_00004) وشاح--" | Copied from SalesInvoiceDetail | Line description |
| Notes | "" (empty) | Copied from SalesInvoiceDetail | Line notes |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
| CreatedBy | [Current User ID] | Security | From ISecurityService |
| TenantId | [Tenant GUID] | Multi-tenancy | From ISecurityService |
Table 3: ReturnSalesInvoiceTracking (INSERT)
Operation: INSERT new record (one per detail line, copied from original SalesInvoiceTracking)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Tracking record ID |
| ReturnSalesInvoiceDetailId | [Return Detail GUID above] | FK | Parent return detail line |
| BatchNo | NULL | Copied from SalesInvoiceTracking | No batch for NoTracking |
| Quantity | 1.0 | From original tracking | Mirrors original |
| HasExpiryDate | false | Copied from SalesInvoiceTracking | No expiry tracking |
| ExpireDate | NULL | Copied from SalesInvoiceTracking | No expiry date |
| SerialId | NULL | Copied from SalesInvoiceTracking | No serial for NoTracking |
| TrackingType | "NoTracking" | Copied from SalesInvoiceTracking | Tracking type enum |
| CreatedDate | [Current Timestamp] | Audit | Timestamp |
| CreatedBy | [Current User ID] | Security | User who created |
| TenantId | [Tenant GUID] | Multi-tenancy | Tenant isolation |
Tracking Record Behavior:
- NoTracking: Tracking record copied from original with NULL BatchNo/SerialId
- Serial: BatchNo set from original tracking SerialId, Quantity = 1 each
- Batch: BatchNo copied from original batch tracking record
- Expiry: ExpireDate copied from original batch
Table 4: ReturnSalesInvoiceDetailTax (INSERT)
Operation: INSERT new record (one per tax per detail line, copied from original SalesInvoiceDetailTax)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Tax record ID |
| ReturnSalesInvoiceDetailId | [Return Detail GUID above] | FK | Parent return detail |
| TaxId | 460 | Copied from SalesInvoiceDetailTax | VAT/Tax definition |
| VatPercentage | 15.0 | Copied from SalesInvoiceDetailTax | 15% VAT |
| VatAmount | 150.00 | CalculateVatAmount() | TotalAfterDiscount × 15% |
| LocalVatAmount | 150.00 | CalculateLocalVatAmount() | VatAmount × CurrencyRate |
| TaxEffect | "Add" | Copied from SalesInvoiceDetailTax | TaxCalculationEffect enum |
| TaxValueType | [Type] | Copied from SalesInvoiceDetailTax | Percentage or fixed |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
| TenantId | [Tenant GUID] | Multi-tenancy | Tenant isolation |
Summary of Add Phase (Draft)
Tables Affected:
- ✅ ReturnSalesInvoiceHeader - 1 INSERT
- ✅ ReturnSalesInvoiceDetail - 1 INSERT
- ✅ ReturnSalesInvoiceTracking - 1 INSERT (copied from original tracking)
- ✅ ReturnSalesInvoiceDetailTax - 1 INSERT (copied from original taxes)
Inventory Impact:
- NONE - No stock movement in draft phase
GL Impact:
- NONE - No journal entries in draft phase
Validations Performed:
✅ Return invoice date in open financial period (FinancialYearPeriodId = 25) ✅ Original SalesInvoiceHeader exists and is found ✅ Each SalesInvoiceDetailId exists in the original invoice ✅ Available return quantity check:
- Storable items:
IInventoryPublicApi.GetItemStockDetailsBySource()→ AvailableQuantity / UomFactor ≥ ToReturnQuantity - Service items:
SalesInvoiceDetail.RemainingServiceQuantity≥ ToReturnQuantity ✅ Lines with ToReturnQuantity = 0 are silently discarded
Key Difference from Sales Invoice:
- Return invoice has no balance/installment table — there is no
ReturnSalesInvoiceBalance - Return invoice has no source table — it always references one original invoice header
- The ZATCA validation requires the original
SalesInvoiceHeader.IsAccepted = truebefore sending
Phase 2: Post Return Sales Invoice
Handler: PostReturnSalesInvoiceCommandHandlerAction: Finalizes return invoice, creates inventory and GL transactions
Prerequisites Check
Before posting, system validates:
- ✅ Invoice Status = Draft
- ✅
accountingInternalComm.CanAddPostedJournal()— period allows posted journals - ✅ Customer has receivable account configured (
Customer.AccountingInfo?.ReceivableAccountIdORCustomer.Category?.ReceivableAccountId) - ✅ Customer has sales return account configured (
Customer.AccountingInfo?.SalesReturnAccountIdORCustomer.Category?.SalesReturnAccountId) - ✅ Accounting module is accessible to the current user (
sideMenuService.GetUserSubDomainModules) - ✅
inventoryInternalComm.CanPostStockIn()— inventory module allows stock in posting - ✅ Module access checks:
HasModuleAccess(Inventory),HasModuleAccess(Finance)
Table 1: ReturnSalesInvoiceHeader (UPDATE)
Operation: UPDATE existing record
| Column Name | Before (Draft) | After (Posted) | Change Description |
|---|---|---|---|
| Status | "Draft" | "Posted" | Status changed via SetAsPosted() |
| InvoiceJournalId | NULL | [Journal GUID] | Return sales journal entry ID |
| InvoiceJournalCode | NULL | "JE-2026-0125" | Journal entry code |
| StockInId | NULL | [StockIn GUID] | Reference to stock in transaction |
| StockInCode | NULL | "SI-2026-0001" | Stock in sequential code |
| StockInJournalId | NULL | [Journal GUID] | Inventory/COGS journal entry ID |
| StockInJournalCode | NULL | "JE-2026-0126" | Inventory journal code |
| UpdatedDate | [Original] | 2026-01-30 10:05:00 | Last update timestamp |
| UpdatedBy | [Original] | [Current User ID] | Updated by user |
All other columns remain unchanged
Table 2: StockInHeader (INSERT - Inventory Module)
Operation: INSERT new record Created By: IInventoryPublicApi.AddPostedStockIn()
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | e.g., c3d4e5f6-2222-3333-4444-555566667777 |
| Code | "SI-2026-0001" | Sequence Service | Stock in sequential code (not same as sales invoice) |
| TransactionDate | 2026-01-30 10:00:00 | ReturnInvoice.ReturnInvoiceDate | Transaction date |
| TransactionType | "StockIn" | Fixed | Inventory increase |
| WarehouseId | 48 | ReturnInvoice.WarehouseId | Receiving warehouse |
| WarehouseName | "Default Warehouse 1" | ReturnInvoice.WarehouseName | Display name |
| SourceDocumentType | "SalesReturnInvoice" | Fixed | Source document enum |
| SourceDocumentId | [Return Invoice Header GUID] | ReturnInvoice.Id | Reference to return invoice |
| SourceDocumentCode | "RSI-2026-0001" | ReturnInvoice.Code | Return invoice code |
| Status | "Posted" | Fixed | Always posted |
| TotalCost | 99.64 | Sum of detail costs | Total inventory value added |
| JournalId | [Journal GUID] | Created journal | Inventory journal ID |
| JournalCode | "JE-2026-0126" | Created journal | Inventory journal code |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Posting timestamp |
| CreatedBy | [Current User ID] | Security | Posting user |
| TenantId | [Tenant GUID] | Multi-tenancy | Tenant isolation |
Table 3: StockInDetail (INSERT - Inventory Module)
Operation: INSERT new record (one per inventory line) Created By: IInventoryPublicApi.AddPostedStockIn()
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | e.g., d4e5f6a7-3333-4444-5555-666677778888 |
| StockInHeaderId | [StockIn Header GUID] | FK | Parent stock in |
| ItemId | 422 | ReturnDetail.ItemId | Inventory item |
| ItemCode | "IDEF_00004" | ReturnDetail.ItemCode | Item code |
| ItemName | "وشاح" | ReturnDetail.ItemName | Item name |
| ItemVariantId | 543 | ReturnDetail.ItemVariantId | Item variant |
| ItemVariantCode | "0000" | ReturnDetail.ItemVariantCode | Variant code |
| UomId | 79415be3-345f-4d51-87ac-84ec87e7a0b0 | ReturnDetail.UomId | Unit of measure |
| UOMCode | "G8roW" | ReturnDetail.UOMCode | UOM code |
| Quantity | 1.0 | ReturnDetail.ToReturnQuantity | Quantity returned to stock |
| UnitCost | 99.64 | ReturnDetail.Cost | Cost per unit (original) |
| LineCost | 99.64 | Quantity × UnitCost | Total line inventory value |
| TrackingType | "NoTracking" | ReturnDetail.TrackingType | Tracking method |
| HasExpiryDate | false | ReturnDetail.HasExpiryDate | Expiry flag |
| SourceDetailId | [Return Detail GUID] | ReturnDetail.Id | Link to return invoice line |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Posting timestamp |
| CreatedBy | [Current User ID] | Security | Posting user |
| TenantId | [Tenant GUID] | Multi-tenancy | Tenant isolation |
Table 4: StockInTracking (INSERT)
Operation: INSERT new record Created By: IInventoryPublicApi.AddPostedStockIn()
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Tracking record ID |
| StockInDetailId | [StockIn Detail GUID] | FK | Parent stock in detail |
| BatchNo | NULL | N/A | No batch number for NoTracking |
| Quantity | 1.0 | StockInDetail.Quantity | Quantity returned |
| ExpireDate | NULL | N/A | No expiry date |
| UnitCost | 99.64 | Original cost | Cost per unit |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Timestamp |
| CreatedBy | [Current User ID] | Security | Posting user |
| TenantId | [Tenant GUID] | Multi-tenancy | Tenant isolation |
Table 5: ItemStockDetail (UPDATE - Inventory Module)
Operation: UPDATE existing record Mechanism: Increases AvailableQuantity (items returned to stock)
Before Post:
sql
ItemId: 422
ItemVariantId: 543
WarehouseId: 48
TrackingType: NoTracking
HasExpiryDate: false
Quantity: 10.0
AvailableQuantity: 9.0 -- After the original sale posted (was 10, sold 1)
TotalCost: 896.72 -- After original sale (996.36 - 99.64)
UnitCost: 99.64 -- Weighted Average Cost (unchanged)After Post:
sql
ItemId: 422
ItemVariantId: 543
WarehouseId: 48
TrackingType: NoTracking
HasExpiryDate: false
Quantity: 10.0 -- Unchanged (historical record)
AvailableQuantity: 10.0 -- Increased by 1 (item returned)
TotalCost: 996.36 -- += ReturnQty × UnitCost (896.72 + 99.64)
UnitCost: 99.64 -- Recalculated WAC (unchanged in this case)Update Logic:
sql
UPDATE ItemStockDetail
SET AvailableQuantity = AvailableQuantity + 1.0,
TotalCost = TotalCost + 99.64
WHERE ItemVariantId = 543
AND WarehouseId = 48
AND TrackingType = 'NoTracking'
AND HasExpiryDate = falseTable 6: JournalEntry (INSERT #1 - Sales Return Journal)
Operation: INSERT new record Created By: IAccountingPublicApi.AddJournal()Purpose: Reverse the accounts receivable and record the sales return
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | e.g., e5f6a7b8-4444-5555-6666-777788889999 |
| Code | "JE-2026-0125" | Sequence Service | Journal entry code |
| JournalDate | 2026-01-30 10:00:00 | ReturnInvoice.ReturnInvoiceDate | Transaction date |
| SourceDocument | "SalesReturnInvoice" | Fixed | SourceDocument enum |
| SourceDocumentId | [Return Invoice Header GUID] | ReturnInvoice.Id | Link to return invoice |
| SourceDocumentCode | "RSI-2026-0001" | ReturnInvoice.Code | Return invoice reference |
| RefrenceNumber | "RSI-2026-0001" | ReturnInvoice.Code | Reference |
| Type | "Sales" | Fixed | JournalEntryType enum |
| Status | "Posted" | Fixed | Always posted |
| Description | "Customer return - wrong size" | ReturnInvoice.Description | Journal description |
| TotalDebit | 1150.00 | Sum of debits | Total debits |
| TotalCredit | 1150.00 | Sum of credits | Total credits (balanced) |
| FinancialYearPeriodId | 25 | ReturnInvoice.FinancialYearPeriodId | Accounting period |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Posting timestamp |
| CreatedBy | [Current User ID] | Security | Posting user |
| TenantId | [Tenant GUID] | Multi-tenancy | Tenant isolation |
Table 7: JournalEntryLine (INSERT Multiple - Sales Return Journal)
Operation: INSERT 3 records Created By: IAccountingPublicApi.AddJournal()
Line 1: Accounts Receivable (Credit)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Line 1 ID |
| JournalEntryId | [Journal GUID above] | FK | Parent journal |
| AccountId | 1010 | Customer.ReceivableAccountId | AR account |
| AccountCode | "1010" | Account master | Account code |
| AccountName | "Accounts Receivable" | Account master | Account name |
| AccountNameAr | "العملاء" | Account master | Arabic name |
| DebitAmount | 0.00 | N/A | No debit |
| CreditAmount | 1150.00 | ReturnInvoice.LocalGrandTotal | Reduces AR — customer owes less |
| LineDescription | "Customer return - wrong size" | ReturnInvoice.Description | Line description |
| CurrencyId | 4 | Base Currency | Saudi Riyal |
| CurrencyRate | 1.0 | Always 1 for base | Base currency rate |
| CostCenterId | NULL | N/A | No cost center for AR |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Posting timestamp |
Line 2: Sales Return Account (Debit)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Line 2 ID |
| JournalEntryId | [Journal GUID above] | FK | Parent journal |
| AccountId | 4020 | Customer.SalesReturnAccountId | Sales return account |
| AccountCode | "4020" | Account master | Account code |
| AccountName | "Sales Returns" | Account master | Account name |
| AccountNameAr | "مردودات المبيعات" | Account master | Arabic name |
| DebitAmount | 1000.00 | ReturnInvoice.LocalTotalAfterDiscount | Sales revenue reversal |
| CreditAmount | 0.00 | N/A | No credit |
| LineDescription | "Customer return - wrong size" | ReturnInvoice.Description | Line description |
| CurrencyId | 4 | Base Currency | Saudi Riyal |
| CurrencyRate | 1.0 | Always 1 | Base currency rate |
| CostCenterId | NULL | N/A | No cost center on return account |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Posting timestamp |
Line 3: VAT Output (Debit — reversed from original)
Note: Generated by AddVatLines(). For each distinct tax × TaxEffect combination:
TaxEffect = Add→DebitAmount = VatAmount, CreditAmount = 0(VAT Output reversed)TaxEffect = Deduct→CreditAmount = VatAmount, DebitAmount = 0
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Line 3 ID |
| JournalEntryId | [Journal GUID above] | FK | Parent journal |
| AccountId | 2030 | Tax.AccountId (from generalSettingsInternalComm) | VAT payable account |
| AccountCode | "2030" | Account master | Account code |
| AccountName | "VAT Output" | Account master | Account name |
| AccountNameAr | "ضريبة المخرجات" | Account master | Arabic name |
| DebitAmount | 150.00 | ReturnDetailTax.LocalVatAmount (TaxEffect=Add) | VAT liability reversed |
| CreditAmount | 0.00 | N/A | No credit |
| LineDescription | "Total VAT" | Localized string | VAT line description |
| CurrencyId | 4 | Base Currency | Saudi Riyal |
| CurrencyRate | 1.0 | Always 1 | Base currency rate |
| CostCenterId | NULL | N/A | No cost center |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Posting timestamp |
Sales Return Journal Summary:
JE-2026-0125 - Sales Return Journal
DR 4020 Sales Returns 1,000.00
DR 2030 VAT Output 150.00
CR 1010 Accounts Receivable 1,150.00
--------- ---------
Total 1,150.00 1,150.00Table 8: JournalEntry (INSERT #2 - Inventory Journal)
Operation: INSERT new record Created By: IInventoryPublicApi.AddPostedStockIn() (Inventory Module) Purpose: Reverse the COGS — items are back in inventory
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | e.g., f6a7b8c9-5555-6666-7777-888899990000 |
| Code | "JE-2026-0126" | Sequence Service | Journal entry code |
| JournalDate | 2026-01-30 10:00:00 | ReturnInvoice.ReturnInvoiceDate | Transaction date |
| SourceDocument | "StockIn" | Fixed | SourceDocument enum |
| SourceDocumentId | [StockIn Header GUID] | StockIn.Id | Link to stock in |
| SourceDocumentCode | "SI-2026-0001" | StockIn.Code | Stock in reference |
| RefrenceNumber | "SI-2026-0001" | StockIn.Code | Reference |
| Type | "Inventory" | Fixed | JournalEntryType enum |
| Status | "Posted" | Fixed | Always posted |
| Description | "Inventory in for RSI-2026-0001" | Generated | Inventory description |
| TotalDebit | 99.64 | Sum of debits | Total debits |
| TotalCredit | 99.64 | Sum of credits | Total credits (balanced) |
| FinancialYearPeriodId | 25 | ReturnInvoice.FinancialYearPeriodId | Accounting period |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Posting timestamp |
| CreatedBy | [Current User ID] | Security | Posting user |
| TenantId | [Tenant GUID] | Multi-tenancy | Tenant isolation |
Table 9: JournalEntryLine (INSERT Multiple - Inventory Journal)
Operation: INSERT 2 records Created By: Inventory Module
Line 1: Inventory Asset (Debit)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Line 1 ID |
| JournalEntryId | [Inventory Journal GUID] | FK | Parent journal |
| AccountId | 1030 | InventorySettings.InventoryAssetAccountId | Inventory asset account |
| AccountCode | "1030" | Account master | Account code |
| AccountName | "Inventory" | Account master | Account name |
| AccountNameAr | "المخزون" | Account master | Arabic name |
| DebitAmount | 99.64 | StockInDetail.LineCost | Inventory asset increased |
| CreditAmount | 0.00 | N/A | No credit |
| LineDescription | "Stock in - وشاح" | Generated | Item name |
| CurrencyId | 4 | Base Currency | Saudi Riyal |
| CurrencyRate | 1.0 | Always 1 | Base currency rate |
| CostCenterId | NULL | N/A | No cost center for asset |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Posting timestamp |
Line 2: Cost of Goods Sold (Credit)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Line 2 ID |
| JournalEntryId | [Inventory Journal GUID] | FK | Parent journal |
| AccountId | 5010 | InventorySettings.CostOfGoodsSoldAccountId | COGS expense account |
| AccountCode | "5010" | Account master | Account code |
| AccountName | "Cost of Goods Sold" | Account master | Account name |
| AccountNameAr | "تكلفة البضاعة المباعة" | Account master | Arabic name |
| DebitAmount | 0.00 | N/A | No debit |
| CreditAmount | 99.64 | StockInDetail.LineCost | COGS expense reversed |
| LineDescription | "COGS reversal - وشاح" | Generated | Item name |
| CurrencyId | 4 | Base Currency | Saudi Riyal |
| CurrencyRate | 1.0 | Always 1 | Base currency rate |
| CostCenterId | NULL | N/A | No cost center |
| CreatedDate | 2026-01-30 10:05:00 | Audit | Posting timestamp |
Inventory Journal Summary:
JE-2026-0126 - Inventory Journal
DR 1030 Inventory 99.64
CR 5010 Cost of Goods Sold 99.64
------ ------
Total 99.64 99.64Table 10: CustomerFinancial (UPDATE)
Operation: UPDATE existing record
Before Post (after original sale was posted):
sql
CustomerId: 433
CustomerName: "dubai"
TotalSales: 1150.00
TotalReturns: 0.00
OutstandingBalance: 1150.00
CreditLimit: 0.00
AvailableCredit: -1150.00After Post:
sql
CustomerId: 433
CustomerName: "dubai"
TotalSales: 1150.00 -- Unchanged (TotalSales tracks gross sales)
TotalReturns: 1150.00 -- += ReturnInvoice.LocalGrandTotal
OutstandingBalance: 0.00 -- -= ReturnInvoice.LocalGrandTotal
CreditLimit: 0.00 -- Unchanged
AvailableCredit: 0.00 -- CreditLimit - OutstandingBalanceUpdate SQL:
sql
UPDATE CustomerFinancial
SET TotalReturns = TotalReturns + 1150.00,
OutstandingBalance = OutstandingBalance - 1150.00,
AvailableCredit = CreditLimit - (OutstandingBalance - 1150.00)
WHERE CustomerId = 433Table 11: SalesMan (NO UPDATE)
Operation: NONE - No salesman assigned
Reason: SalesManId is NULL (inherited from original invoice which had no salesman)
When updated:
- If SalesManId is provided,
salesman.AddReturnInvoice(returnSalesInvoice)reduces the salesman's TotalSales and outstanding balance
Table 12: SalesInvoiceDetail.RemainingServiceQuantity (NO UPDATE)
Operation: NONE - No service items in this example
When updated:
- For service line items (
IsServiceItem = true),UpdateServiceRemainingQuantity()restores theSalesInvoiceDetail.RemainingServiceQuantityby adding back the returned quantity
Summary of Post Phase
Tables Affected:
- ✅ ReturnSalesInvoiceHeader - 1 UPDATE (status, journal IDs, stock in IDs)
- ✅ StockInHeader - 1 INSERT (inventory return transaction)
- ✅ StockInDetail - 1 INSERT (line detail)
- ✅ StockInTracking - 1 INSERT (tracking record)
- ✅ ItemStockDetail - 1 UPDATE (increased AvailableQuantity)
- ✅ JournalEntry - 2 INSERTS (Sales Return + Inventory journals)
- ✅ JournalEntryLine - 5 INSERTS (3 sales return + 2 inventory)
- ✅ CustomerFinancial - 1 UPDATE (TotalReturns +, OutstandingBalance -)
- ❌ SalesMan - 0 updates (no salesman)
- ❌ SalesInvoiceDetail.RemainingServiceQuantity - 0 updates (no service items)
Inventory Impact:
Item: IDEF_00004 (وشاح)
Warehouse: Default Warehouse 1 (48)
Before: Available Qty = 9 (after original sale)
After: Available Qty = 10 (item returned)
Cost Reversed: 99.64 SARGL Impact (Combined Journals):
Chart of Accounts Impact:
Account 4020 (Sales Returns) DR 1,000.00
Account 2030 (VAT Output) DR 150.00
Account 1010 (Accounts Receivable) CR 1,150.00
Account 1030 (Inventory) DR 99.64
Account 5010 (COGS) CR 99.64
Balance Sheet Impact:
Assets:
Accounts Receivable -1,150.00
Inventory +99.64
Net Assets -1,050.36
Liabilities:
VAT Payable -150.00 (DR to VAT Output reduces liability)
Income Statement Impact:
Revenue:
Sales Returns (contra) +1,000.00 (increases returns = reduces net revenue)
Expenses:
Cost of Goods Sold -99.64 (COGS reversed = reduces expense)
Gross Profit Impact: -900.36 (mirrors the original sale's gross profit)Complete Transaction Flow Summary
Step-by-Step Execution
1. User submits Add Return Sales Invoice request
↓
2. AddReturnSalesInvoiceCommandHandler.Handle()
↓
3. Resolve FinancialYearPeriodId = 25 for 2026-01-30
↓
4. Fetch SalesInvoiceHeader (3fa85f64...) with all details and tracking
↓
5. Call IInventoryPublicApi.GetItemStockDetailsBySource():
- SourceDetailIds = [7a93c8e2-...]
- SourceDocumentType = SalesInvoice
→ Returns: AvailableQuantity = 1, UomFactor = 1
↓
6. Filter details: ToReturnQuantity = 1 > 0 → keep
Validate: AvailableQuantity (1) ≥ ToReturnQuantity (1) ✓
SetTransactionRemainQuantity(1)
↓
7. SetDetailData() → copy all fields from SalesInvoiceDetail:
- Price = 1000, Cost = 99.64, Tracking, Taxes, etc.
↓
8. CalculateAllFields(CurrencyRate=1):
- SubCost = 1 × 99.64 = 99.64
- SubPrice = (1000/1) × 1 = 1000
- DiscountAmount = 1000 × 0% = 0
- NetAmount = 1000 - 0 = 1000
- TotalAfterDiscount = 1000
- VatAmount = 1000 × 15% = 150
- GrandTotal = 1000 + 150 = 1150
↓
9. CalculateHeaderTotals():
- TotalNetAmount = 1000
- TotalAfterDiscount = 1000
- GrandTotal = 1150
↓
10. SetAsDraft(), SetZatcaStatusAsNotSend(), SetEtaStatusAsPending()
↓
11. Generate sequence code → "RSI-2026-0001"
↓
12. INSERT ReturnSalesInvoiceHeader (Status = Draft)
INSERT ReturnSalesInvoiceDetail (1 line, with SalesInvoiceDetailId)
INSERT ReturnSalesInvoiceTracking (copied from original)
INSERT ReturnSalesInvoiceDetailTax (copied from original)
↓
13. SaveChanges → Database transaction committed
↓
14. Check ZatcaSendingOption: IsOnSaveTrigger? → (skip if not configured)
↓
15. Return ReturnInvoiceId + Code → Frontend receives draft return invoice
--- DRAFT PHASE COMPLETE ---
16. User clicks "Post" button
↓
17. PostReturnSalesInvoiceCommandHandler.Handle() [SemaphoreSlim lock acquired]
↓
18. Check module access: Accounting ✓, Inventory ✓, Finance ✓
↓
19. Fetch ReturnSalesInvoiceHeader with all includes
Validate Status = Draft ✓
↓
20. Validate preconditions (Accounting):
- CanAddPostedJournal ✓
- receivableAccountId = 1010 (from Customer.AccountingInfo) ✓
- returnAccountId = 4020 (from Customer.AccountingInfo) ✓
- Accounting module accessible ✓
↓
21. Create Sales Return Journal (JE-2026-0125):
CR 1010 Accounts Receivable 1,150
DR 4020 Sales Returns 1,000
DR 2030 VAT Output 150
↓
22. Check hasStorableItems = true (CategoryType = Storable)
Call IInventoryPublicApi.AddPostedStockIn():
- Create StockInHeader (stock in sequential code)
- Create StockInDetail (Qty 1, Cost 99.64)
- Update ItemStockDetail (AvailableQty 9 → 10)
- Create Inventory Journal (JE-2026-0126):
DR 1030 Inventory 99.64
CR 5010 COGS 99.64
- Return StockInId, StockInCode, JournalId, JournalCode
↓
23. Update ReturnSalesInvoiceHeader:
- InvoiceJournalId = [GUID], InvoiceJournalCode = "JE-2026-0125"
- StockInId = [GUID], StockInCode = "SI-2026-0001"
- StockInJournalId = [GUID], StockInJournalCode = "JE-2026-0126"
- Status = Posted
↓
24. ApplyReturnSalesInvoiceToCustomerCredit():
- CustomerFinancial.TotalReturns += 1150
- CustomerFinancial.OutstandingBalance -= 1150
↓
25. UpdateServiceRemainingQuantity() — no service items, no-op
↓
26. SaveChanges → All transactions committed
↓
27. SemaphoreSlim released
↓
28. CreatePostedReturnInvoiceEventAsync():
→ Publish ErpReturnSalesInvoicePostedIntegrationEvent (webhook)
↓
29. Check ZatcaSendingOption: IsOnPostTrigger or IsOnPrintTrigger? → process ZATCA if configured
↓
30. Return ReturnInvoiceId → Frontend receives posted return invoice
--- POST PHASE COMPLETE ---Database Transaction Isolation
Add Phase (Draft)
sql
BEGIN TRANSACTION
INSERT INTO Sales.ReturnSalesInvoiceHeader VALUES (...)
INSERT INTO Sales.ReturnSalesInvoiceDetail VALUES (...)
INSERT INTO Sales.ReturnSalesInvoiceTracking VALUES (...)
INSERT INTO Sales.ReturnSalesInvoiceDetailTax VALUES (...)
COMMIT TRANSACTIONDuration: ~100ms (single database transaction, includes API call to Inventory for stock availability check)
Post Phase
sql
BEGIN TRANSACTION -- Main Transaction
-- Step 1: Create Sales Return Journal
EXEC Accounting.AddJournal
INSERT INTO JournalEntry VALUES (...) -- SourceDocument = SalesReturnInvoice
INSERT INTO JournalEntryLine VALUES (...) -- 3 lines (CR AR, DR SalesReturn, DR VAT)
-- Step 2: Create Stock In (nested, SaveToContext = false)
EXEC Inventory.AddPostedStockIn
INSERT INTO StockInHeader VALUES (...)
INSERT INTO StockInDetail VALUES (...)
UPDATE ItemStockDetail SET AvailableQuantity += 1, TotalCost += 99.64
-- Create Inventory Journal
INSERT INTO JournalEntry VALUES (...) -- SourceDocument = StockIn
INSERT INTO JournalEntryLine VALUES (...) -- 2 lines (DR Inventory, CR COGS)
-- Step 3: Update Return Invoice Header
UPDATE Sales.ReturnSalesInvoiceHeader
SET Status = 'Posted',
InvoiceJournalId = @JournalId,
InvoiceJournalCode = 'JE-2026-0125',
StockInId = @StockInId,
StockInCode = 'SI-2026-0001',
StockInJournalId = @StockInJournalId,
StockInJournalCode = 'JE-2026-0126'
-- Step 4: Update Customer
UPDATE CustomerFinancial
SET TotalReturns += 1150,
OutstandingBalance -= 1150
COMMIT TRANSACTIONDuration: ~200-500ms (multiple module integrations)
Key Differences vs. Sales Invoice
| Aspect | Sales Invoice | Return Sales Invoice |
|---|---|---|
| Payload | Full item data provided | Only SalesInvoiceDetailId + ToReturnQuantity |
| Data source | User input | Copied from original SalesInvoiceDetail |
| Balance table | ✅ SalesInvoiceBalance (installments) | ❌ None |
| Inventory | StockOut (decrease) | StockIn (increase) |
| AR journal line | DR Accounts Receivable | CR Accounts Receivable |
| Revenue journal line | CR Sales Revenue | DR Sales Return account |
| VAT journal line | CR VAT Output | DR VAT Output |
| Inventory journal | DR COGS / CR Inventory | DR Inventory / CR COGS |
| CustomerFinancial | TotalSales ↑, OutstandingBalance ↑ | TotalReturns ↑, OutstandingBalance ↓ |
| Concurrency | No semaphore | SemaphoreSlim (1,1) on post |
| ZATCA prerequisite | None | Original invoice must be IsAccepted = true |
Error Scenarios
Add Phase Validation Failures
| Error Condition | Error Message | Resolution |
|---|---|---|
| Original invoice not found | "Sales.InvoiceHeaderNotFound" | Check SalesInvoiceHeaderId is correct |
| Detail line not in original | "Sales.NotFoundInvoiceDetailsLine" | SalesInvoiceDetailId must match a detail in the original invoice |
| Stock already fully returned (storable) | "Sales.AvailableQuantityLessThanToReturnQuantity" | Check remaining returnable quantity via inventory |
| Service quantity exhausted | "Sales.AvailableServiceQuantityLessThanToReturnQuantity" | SalesInvoiceDetail.RemainingServiceQuantity is 0 |
| Source detail not found in inventory | "Sales.SourceDetailNotFound" | Stock out record for this detail may be missing |
| Invalid period | (from GetCurrentPeriodQuery) | Change return invoice date or open period |
Post Phase Validation Failures
| Error Condition | Error Message | Resolution |
|---|---|---|
| Invoice not draft | "Sales.ReturnInvoiceAlreadyPosted" | Cannot post already-posted return invoice |
| Return invoice not found | "Sales.ReturnInvoiceNotFound" | Check invoice ID |
| Customer receivable account missing | "Sales.ConfigureCustomerAccount" | Configure customer receivable account |
| Customer sales return account missing | "Sales.ConfigureCustomerAccount" | Configure customer sales return GL account |
| Accounting module not accessible | "Sales.AccountingModuleNotAccessible" | User must have Accounting module permission |
| Cannot post stock in | "Purchase.CannotPostStockIn" | Check inventory settings allow stock in posting |
| Period closed | (from CanAddPostedJournal) | Reopen period or change return invoice date |
| Tax not found | "Sales.TaxNotFound" | Verify tax definition is active and has GL account |
| Invalid tax effect | "Sales.InvalidTaxEffect" | Tax must have TaxEffect of Add or Deduct |
End of Walkthrough
This example demonstrates a typical return sales invoice with:
- ✅ Single storable item (no tracking)
- ✅ Full quantity return (returning all 1 unit)
- ✅ No discount
- ✅ VAT not included in price
- ✅ Data fully inherited from original sales invoice
- ✅ Weighted average cost method for COGS reversal
Document Version: 1.0 Example Date: 2026-01-30 Currency: Saudi Riyal (SAR) Return Amount: 1,150.00 SAR (including 15% VAT) Original Invoice: SI-2026-0001 (2026-01-28)