Skip to content

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 NameValueSourceNotes
Id[Auto-Generated GUID]Systeme.g., a1b2c3d4-1111-2222-3333-444455556666
Code"RSI-2026-0001"Sequence ServiceScreen = ReturnSalesInvoice, Module = Sales
ReturnInvoiceDate2026-01-30 10:00:00PayloadTransaction date
Description"Customer return - wrong size"PayloadOptional notes
Status"Draft"SetAsDraft()TransactionStatus enum
WarehouseId48PayloadReceiving warehouse
WarehouseName"Default Warehouse 1"PayloadDisplay name
SalesInvoiceHeaderId3fa85f64-5717-4562-b3fc-2c963f66afa6PayloadFK to original invoice
CustomerId433Copied from SalesInvoiceHeaderCustomer identifier
CustomerName"dubai "Copied from SalesInvoiceHeaderCustomer display name
CurrencyId4Copied from SalesInvoiceHeaderTransaction currency
CurrencyName"Saudi Riyal"Copied from SalesInvoiceHeaderCurrency display name
CurrencyRate1.0Copied from SalesInvoiceHeaderExchange rate
PaymentTermId38Copied from SalesInvoiceHeaderPayment schedule template
PricePolicyId42Copied from SalesInvoiceHeaderActive price policy
SalesManIdNULLCopied from SalesInvoiceHeaderNo salesman assigned
SalesManNameNULLCopied from SalesInvoiceHeaderNo salesman name
ReferenceNULLCopied from SalesInvoiceHeaderOriginal invoice reference
FinancialYearPeriodId[Period ID]GetCurrentPeriodId()e.g., 25 for Jan 2026 period
TotalDiscount0CalculateHeaderTotals()Sum of line discounts
LocalTotalDiscount0× CurrencyRateBase currency discount
TotalNetAmount1000CalculateHeaderTotals()Sum of (Price × ReturnQty)
LocalTotalNetAmount1000× CurrencyRateBase currency total net
TotalAfterDiscount1000CalculateHeaderTotals()Net - Discount
LocalTotalAfterDiscount1000× CurrencyRateBase currency total
LocalServiceTotalAfterDiscount0CalculateHeaderTotals()Service items subtotal
GrandTotal1150CalculateHeaderTotals()Total + VAT
LocalGrandTotal1150× CurrencyRateBase currency grand total
StockInIdNULLDraft stateSet on post
StockInCodeNULLDraft stateSet on post
InvoiceJournalIdNULLDraft stateSet on post
InvoiceJournalCodeNULLDraft stateSet on post
StockInJournalIdNULLDraft stateSet on post
StockInJournalCodeNULLDraft stateSet on post
ZatcaStatus"NotSend"SetZatcaStatusAsNotSend()Saudi E-Invoicing status
ZatcaErrorMessageNULLDefaultNo ZATCA error
ZatcaWarningMessageNULLDefaultNo ZATCA warning
ZatcaLastTryDate[Default]DefaultNo ZATCA attempt yet
EtaUploadStatus"Pending"SetEtaStatusAsPending()ETA status
EtaErrorMessageNULLDefaultNo ETA error
PrintCount0DefaultNot printed
POSSessionIdNULLNot POSPOS session ref
ExternalIdentifierNULLPayload (null)External system ref
ExternalCodeNULLPayload (null)External return code
CreatedDate[Current Timestamp]AuditAuto-set
CreatedBy[Current User ID]SecurityFrom ISecurityService
TenantId[Tenant GUID]Multi-tenancyFrom ISecurityService
IsDeletedfalseDefaultSoft 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 NameValueSourceNotes
Id[Auto-Generated GUID]Systeme.g., b2c3d4e5-5555-6666-7777-888899990000
ReturnSalesInvoiceHeaderId[Return Header GUID above]FKParent return invoice
SalesInvoiceDetailId7a93c8e2-1234-5678-9abc-def012345678PayloadLink to original detail line
ItemId422Copied from SalesInvoiceDetailInventory item
ItemCode"IDEF_00004"Copied from SalesInvoiceDetailItem code
ItemName"وشاح"Copied from SalesInvoiceDetailItem name
ItemVariantId543Copied from SalesInvoiceDetailItem variant
ItemVariantCode"0000"Copied from SalesInvoiceDetailVariant code
ItemVariantNameEn"default category for inventory items"Copied from SalesInvoiceDetailVariant name EN
ItemVariantNameAr"-"Copied from SalesInvoiceDetailVariant name AR
CategoryId109Copied from SalesInvoiceDetailItem category
ItemCategoryNameEn"default category for inventory items"Copied from SalesInvoiceDetailCategory name EN
ItemCategoryNameAr"الفئة الافتراضية للاصناف المخزنية"Copied from SalesInvoiceDetailCategory name AR
CategoryType"Storable"Copied from SalesInvoiceDetailCategoryType enum
IsServiceItemfalseCopied from SalesInvoiceDetailNot a service
IsProductItemtrueCopied from SalesInvoiceDetailStorable item
UomId79415be3-345f-4d51-87ac-84ec87e7a0b0Copied from SalesInvoiceDetailUnit of measure
UOMCode"G8roW"Copied from SalesInvoiceDetailUOM code
UOMNameEn"BaseUOM"Copied from SalesInvoiceDetailUOM name EN
UOMNameAr"وحدة القياس الأساسية"Copied from SalesInvoiceDetailUOM name AR
ToReturnQuantity1.0PayloadQuantity being returned
OriginalQuantity1.0Copied from SalesInvoiceDetailOriginal sale quantity
TransactionRemainQuantity1.0GetItemStockDetailsBySource()Available for return at time of creation
Price1000.00Copied from SalesInvoiceDetailOriginal unit price (SAR)
SubPrice1000.00SetSubPrice()OrginSubPrice / OriginalQty × ReturnQty
Cost99.64Copied from SalesInvoiceDetailUnit cost (COGS)
SubCost99.64SetSubCost()ReturnQty × Cost
DiscountPercentage0Copied from SalesInvoiceDetailInherited discount
DiscountAmount0CalculateDiscountAmount()SubPrice × DiscountPercentage%
LocalDiscountAmount0× CurrencyRateBase currency discount
NetAmount1000.00CalculateNetAmount()SubPrice - DiscountAmount
LocalNetAmount1000.00× CurrencyRateBase currency net
TotalAfterDiscount1000.00CalculateTotalAfterDiscount()= NetAmount
LocalTotalAfterDiscount1000.00× CurrencyRateBase currency total
IsVatIncludedfalseCopied from SalesInvoiceDetailVAT not in price
VatAmount150.00CalculateTotalVatAmount()1000 × 15%
LocalVatAmount150.00× CurrencyRateBase currency VAT
GrandTotal1150.00CalculateGrandTotal()TotalAfterDiscount + VatAmount
LocalGrandTotal1150.00× CurrencyRateBase currency grand total
BarCode"" (empty)Copied from SalesInvoiceDetailNo barcode
BarCodeIdNULLCopied from SalesInvoiceDetailNo barcode
TrackingType"NoTracking"Copied from SalesInvoiceDetailTracking method
HasExpiryDatefalseCopied from SalesInvoiceDetailNo expiry
InvoiceEntryMode"Manual"Copied from SalesInvoiceDetailEntry mode
Description"(IDEF_00004) وشاح--"Copied from SalesInvoiceDetailLine description
Notes"" (empty)Copied from SalesInvoiceDetailLine notes
CreatedDate[Current Timestamp]AuditAuto-set
CreatedBy[Current User ID]SecurityFrom ISecurityService
TenantId[Tenant GUID]Multi-tenancyFrom ISecurityService

Table 3: ReturnSalesInvoiceTracking (INSERT)

Operation: INSERT new record (one per detail line, copied from original SalesInvoiceTracking)

Column NameValueSourceNotes
Id[Auto-Generated GUID]SystemTracking record ID
ReturnSalesInvoiceDetailId[Return Detail GUID above]FKParent return detail line
BatchNoNULLCopied from SalesInvoiceTrackingNo batch for NoTracking
Quantity1.0From original trackingMirrors original
HasExpiryDatefalseCopied from SalesInvoiceTrackingNo expiry tracking
ExpireDateNULLCopied from SalesInvoiceTrackingNo expiry date
SerialIdNULLCopied from SalesInvoiceTrackingNo serial for NoTracking
TrackingType"NoTracking"Copied from SalesInvoiceTrackingTracking type enum
CreatedDate[Current Timestamp]AuditTimestamp
CreatedBy[Current User ID]SecurityUser who created
TenantId[Tenant GUID]Multi-tenancyTenant 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 NameValueSourceNotes
Id[Auto-Generated GUID]SystemTax record ID
ReturnSalesInvoiceDetailId[Return Detail GUID above]FKParent return detail
TaxId460Copied from SalesInvoiceDetailTaxVAT/Tax definition
VatPercentage15.0Copied from SalesInvoiceDetailTax15% VAT
VatAmount150.00CalculateVatAmount()TotalAfterDiscount × 15%
LocalVatAmount150.00CalculateLocalVatAmount()VatAmount × CurrencyRate
TaxEffect"Add"Copied from SalesInvoiceDetailTaxTaxCalculationEffect enum
TaxValueType[Type]Copied from SalesInvoiceDetailTaxPercentage or fixed
CreatedDate[Current Timestamp]AuditAuto-set
TenantId[Tenant GUID]Multi-tenancyTenant isolation

Summary of Add Phase (Draft)

Tables Affected:

  1. ReturnSalesInvoiceHeader - 1 INSERT
  2. ReturnSalesInvoiceDetail - 1 INSERT
  3. ReturnSalesInvoiceTracking - 1 INSERT (copied from original tracking)
  4. 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 = true before sending

Phase 2: Post Return Sales Invoice

Handler: PostReturnSalesInvoiceCommandHandlerAction: Finalizes return invoice, creates inventory and GL transactions

Prerequisites Check

Before posting, system validates:

  1. ✅ Invoice Status = Draft
  2. accountingInternalComm.CanAddPostedJournal() — period allows posted journals
  3. ✅ Customer has receivable account configured (Customer.AccountingInfo?.ReceivableAccountId OR Customer.Category?.ReceivableAccountId)
  4. ✅ Customer has sales return account configured (Customer.AccountingInfo?.SalesReturnAccountId OR Customer.Category?.SalesReturnAccountId)
  5. ✅ Accounting module is accessible to the current user (sideMenuService.GetUserSubDomainModules)
  6. inventoryInternalComm.CanPostStockIn() — inventory module allows stock in posting
  7. ✅ Module access checks: HasModuleAccess(Inventory), HasModuleAccess(Finance)

Table 1: ReturnSalesInvoiceHeader (UPDATE)

Operation: UPDATE existing record

Column NameBefore (Draft)After (Posted)Change Description
Status"Draft""Posted"Status changed via SetAsPosted()
InvoiceJournalIdNULL[Journal GUID]Return sales journal entry ID
InvoiceJournalCodeNULL"JE-2026-0125"Journal entry code
StockInIdNULL[StockIn GUID]Reference to stock in transaction
StockInCodeNULL"SI-2026-0001"Stock in sequential code
StockInJournalIdNULL[Journal GUID]Inventory/COGS journal entry ID
StockInJournalCodeNULL"JE-2026-0126"Inventory journal code
UpdatedDate[Original]2026-01-30 10:05:00Last 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 NameValueSourceNotes
Id[Auto-Generated GUID]Systeme.g., c3d4e5f6-2222-3333-4444-555566667777
Code"SI-2026-0001"Sequence ServiceStock in sequential code (not same as sales invoice)
TransactionDate2026-01-30 10:00:00ReturnInvoice.ReturnInvoiceDateTransaction date
TransactionType"StockIn"FixedInventory increase
WarehouseId48ReturnInvoice.WarehouseIdReceiving warehouse
WarehouseName"Default Warehouse 1"ReturnInvoice.WarehouseNameDisplay name
SourceDocumentType"SalesReturnInvoice"FixedSource document enum
SourceDocumentId[Return Invoice Header GUID]ReturnInvoice.IdReference to return invoice
SourceDocumentCode"RSI-2026-0001"ReturnInvoice.CodeReturn invoice code
Status"Posted"FixedAlways posted
TotalCost99.64Sum of detail costsTotal inventory value added
JournalId[Journal GUID]Created journalInventory journal ID
JournalCode"JE-2026-0126"Created journalInventory journal code
CreatedDate2026-01-30 10:05:00AuditPosting timestamp
CreatedBy[Current User ID]SecurityPosting user
TenantId[Tenant GUID]Multi-tenancyTenant isolation

Table 3: StockInDetail (INSERT - Inventory Module)

Operation: INSERT new record (one per inventory line) Created By: IInventoryPublicApi.AddPostedStockIn()

Column NameValueSourceNotes
Id[Auto-Generated GUID]Systeme.g., d4e5f6a7-3333-4444-5555-666677778888
StockInHeaderId[StockIn Header GUID]FKParent stock in
ItemId422ReturnDetail.ItemIdInventory item
ItemCode"IDEF_00004"ReturnDetail.ItemCodeItem code
ItemName"وشاح"ReturnDetail.ItemNameItem name
ItemVariantId543ReturnDetail.ItemVariantIdItem variant
ItemVariantCode"0000"ReturnDetail.ItemVariantCodeVariant code
UomId79415be3-345f-4d51-87ac-84ec87e7a0b0ReturnDetail.UomIdUnit of measure
UOMCode"G8roW"ReturnDetail.UOMCodeUOM code
Quantity1.0ReturnDetail.ToReturnQuantityQuantity returned to stock
UnitCost99.64ReturnDetail.CostCost per unit (original)
LineCost99.64Quantity × UnitCostTotal line inventory value
TrackingType"NoTracking"ReturnDetail.TrackingTypeTracking method
HasExpiryDatefalseReturnDetail.HasExpiryDateExpiry flag
SourceDetailId[Return Detail GUID]ReturnDetail.IdLink to return invoice line
CreatedDate2026-01-30 10:05:00AuditPosting timestamp
CreatedBy[Current User ID]SecurityPosting user
TenantId[Tenant GUID]Multi-tenancyTenant isolation

Table 4: StockInTracking (INSERT)

Operation: INSERT new record Created By: IInventoryPublicApi.AddPostedStockIn()

Column NameValueSourceNotes
Id[Auto-Generated GUID]SystemTracking record ID
StockInDetailId[StockIn Detail GUID]FKParent stock in detail
BatchNoNULLN/ANo batch number for NoTracking
Quantity1.0StockInDetail.QuantityQuantity returned
ExpireDateNULLN/ANo expiry date
UnitCost99.64Original costCost per unit
CreatedDate2026-01-30 10:05:00AuditTimestamp
CreatedBy[Current User ID]SecurityPosting user
TenantId[Tenant GUID]Multi-tenancyTenant 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 = false

Table 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 NameValueSourceNotes
Id[Auto-Generated GUID]Systeme.g., e5f6a7b8-4444-5555-6666-777788889999
Code"JE-2026-0125"Sequence ServiceJournal entry code
JournalDate2026-01-30 10:00:00ReturnInvoice.ReturnInvoiceDateTransaction date
SourceDocument"SalesReturnInvoice"FixedSourceDocument enum
SourceDocumentId[Return Invoice Header GUID]ReturnInvoice.IdLink to return invoice
SourceDocumentCode"RSI-2026-0001"ReturnInvoice.CodeReturn invoice reference
RefrenceNumber"RSI-2026-0001"ReturnInvoice.CodeReference
Type"Sales"FixedJournalEntryType enum
Status"Posted"FixedAlways posted
Description"Customer return - wrong size"ReturnInvoice.DescriptionJournal description
TotalDebit1150.00Sum of debitsTotal debits
TotalCredit1150.00Sum of creditsTotal credits (balanced)
FinancialYearPeriodId25ReturnInvoice.FinancialYearPeriodIdAccounting period
CreatedDate2026-01-30 10:05:00AuditPosting timestamp
CreatedBy[Current User ID]SecurityPosting user
TenantId[Tenant GUID]Multi-tenancyTenant isolation

Table 7: JournalEntryLine (INSERT Multiple - Sales Return Journal)

Operation: INSERT 3 records Created By: IAccountingPublicApi.AddJournal()

Line 1: Accounts Receivable (Credit)

Column NameValueSourceNotes
Id[Auto-Generated GUID]SystemLine 1 ID
JournalEntryId[Journal GUID above]FKParent journal
AccountId1010Customer.ReceivableAccountIdAR account
AccountCode"1010"Account masterAccount code
AccountName"Accounts Receivable"Account masterAccount name
AccountNameAr"العملاء"Account masterArabic name
DebitAmount0.00N/ANo debit
CreditAmount1150.00ReturnInvoice.LocalGrandTotalReduces AR — customer owes less
LineDescription"Customer return - wrong size"ReturnInvoice.DescriptionLine description
CurrencyId4Base CurrencySaudi Riyal
CurrencyRate1.0Always 1 for baseBase currency rate
CostCenterIdNULLN/ANo cost center for AR
CreatedDate2026-01-30 10:05:00AuditPosting timestamp

Line 2: Sales Return Account (Debit)

Column NameValueSourceNotes
Id[Auto-Generated GUID]SystemLine 2 ID
JournalEntryId[Journal GUID above]FKParent journal
AccountId4020Customer.SalesReturnAccountIdSales return account
AccountCode"4020"Account masterAccount code
AccountName"Sales Returns"Account masterAccount name
AccountNameAr"مردودات المبيعات"Account masterArabic name
DebitAmount1000.00ReturnInvoice.LocalTotalAfterDiscountSales revenue reversal
CreditAmount0.00N/ANo credit
LineDescription"Customer return - wrong size"ReturnInvoice.DescriptionLine description
CurrencyId4Base CurrencySaudi Riyal
CurrencyRate1.0Always 1Base currency rate
CostCenterIdNULLN/ANo cost center on return account
CreatedDate2026-01-30 10:05:00AuditPosting timestamp

Line 3: VAT Output (Debit — reversed from original)

Note: Generated by AddVatLines(). For each distinct tax × TaxEffect combination:

  • TaxEffect = AddDebitAmount = VatAmount, CreditAmount = 0 (VAT Output reversed)
  • TaxEffect = DeductCreditAmount = VatAmount, DebitAmount = 0
Column NameValueSourceNotes
Id[Auto-Generated GUID]SystemLine 3 ID
JournalEntryId[Journal GUID above]FKParent journal
AccountId2030Tax.AccountId (from generalSettingsInternalComm)VAT payable account
AccountCode"2030"Account masterAccount code
AccountName"VAT Output"Account masterAccount name
AccountNameAr"ضريبة المخرجات"Account masterArabic name
DebitAmount150.00ReturnDetailTax.LocalVatAmount (TaxEffect=Add)VAT liability reversed
CreditAmount0.00N/ANo credit
LineDescription"Total VAT"Localized stringVAT line description
CurrencyId4Base CurrencySaudi Riyal
CurrencyRate1.0Always 1Base currency rate
CostCenterIdNULLN/ANo cost center
CreatedDate2026-01-30 10:05:00AuditPosting 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.00

Table 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 NameValueSourceNotes
Id[Auto-Generated GUID]Systeme.g., f6a7b8c9-5555-6666-7777-888899990000
Code"JE-2026-0126"Sequence ServiceJournal entry code
JournalDate2026-01-30 10:00:00ReturnInvoice.ReturnInvoiceDateTransaction date
SourceDocument"StockIn"FixedSourceDocument enum
SourceDocumentId[StockIn Header GUID]StockIn.IdLink to stock in
SourceDocumentCode"SI-2026-0001"StockIn.CodeStock in reference
RefrenceNumber"SI-2026-0001"StockIn.CodeReference
Type"Inventory"FixedJournalEntryType enum
Status"Posted"FixedAlways posted
Description"Inventory in for RSI-2026-0001"GeneratedInventory description
TotalDebit99.64Sum of debitsTotal debits
TotalCredit99.64Sum of creditsTotal credits (balanced)
FinancialYearPeriodId25ReturnInvoice.FinancialYearPeriodIdAccounting period
CreatedDate2026-01-30 10:05:00AuditPosting timestamp
CreatedBy[Current User ID]SecurityPosting user
TenantId[Tenant GUID]Multi-tenancyTenant isolation

Table 9: JournalEntryLine (INSERT Multiple - Inventory Journal)

Operation: INSERT 2 records Created By: Inventory Module

Line 1: Inventory Asset (Debit)

Column NameValueSourceNotes
Id[Auto-Generated GUID]SystemLine 1 ID
JournalEntryId[Inventory Journal GUID]FKParent journal
AccountId1030InventorySettings.InventoryAssetAccountIdInventory asset account
AccountCode"1030"Account masterAccount code
AccountName"Inventory"Account masterAccount name
AccountNameAr"المخزون"Account masterArabic name
DebitAmount99.64StockInDetail.LineCostInventory asset increased
CreditAmount0.00N/ANo credit
LineDescription"Stock in - وشاح"GeneratedItem name
CurrencyId4Base CurrencySaudi Riyal
CurrencyRate1.0Always 1Base currency rate
CostCenterIdNULLN/ANo cost center for asset
CreatedDate2026-01-30 10:05:00AuditPosting timestamp

Line 2: Cost of Goods Sold (Credit)

Column NameValueSourceNotes
Id[Auto-Generated GUID]SystemLine 2 ID
JournalEntryId[Inventory Journal GUID]FKParent journal
AccountId5010InventorySettings.CostOfGoodsSoldAccountIdCOGS expense account
AccountCode"5010"Account masterAccount code
AccountName"Cost of Goods Sold"Account masterAccount name
AccountNameAr"تكلفة البضاعة المباعة"Account masterArabic name
DebitAmount0.00N/ANo debit
CreditAmount99.64StockInDetail.LineCostCOGS expense reversed
LineDescription"COGS reversal - وشاح"GeneratedItem name
CurrencyId4Base CurrencySaudi Riyal
CurrencyRate1.0Always 1Base currency rate
CostCenterIdNULLN/ANo cost center
CreatedDate2026-01-30 10:05:00AuditPosting 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.64

Table 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.00

After 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 - OutstandingBalance

Update SQL:

sql
UPDATE CustomerFinancial
SET TotalReturns = TotalReturns + 1150.00,
    OutstandingBalance = OutstandingBalance - 1150.00,
    AvailableCredit = CreditLimit - (OutstandingBalance - 1150.00)
WHERE CustomerId = 433

Table 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 the SalesInvoiceDetail.RemainingServiceQuantity by adding back the returned quantity

Summary of Post Phase

Tables Affected:

  1. ReturnSalesInvoiceHeader - 1 UPDATE (status, journal IDs, stock in IDs)
  2. StockInHeader - 1 INSERT (inventory return transaction)
  3. StockInDetail - 1 INSERT (line detail)
  4. StockInTracking - 1 INSERT (tracking record)
  5. ItemStockDetail - 1 UPDATE (increased AvailableQuantity)
  6. JournalEntry - 2 INSERTS (Sales Return + Inventory journals)
  7. JournalEntryLine - 5 INSERTS (3 sales return + 2 inventory)
  8. CustomerFinancial - 1 UPDATE (TotalReturns +, OutstandingBalance -)
  9. SalesMan - 0 updates (no salesman)
  10. 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 SAR

GL 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 TRANSACTION

Duration: ~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 TRANSACTION

Duration: ~200-500ms (multiple module integrations)


Key Differences vs. Sales Invoice

AspectSales InvoiceReturn Sales Invoice
PayloadFull item data providedOnly SalesInvoiceDetailId + ToReturnQuantity
Data sourceUser inputCopied from original SalesInvoiceDetail
Balance table✅ SalesInvoiceBalance (installments)❌ None
InventoryStockOut (decrease)StockIn (increase)
AR journal lineDR Accounts ReceivableCR Accounts Receivable
Revenue journal lineCR Sales RevenueDR Sales Return account
VAT journal lineCR VAT OutputDR VAT Output
Inventory journalDR COGS / CR InventoryDR Inventory / CR COGS
CustomerFinancialTotalSales ↑, OutstandingBalance ↑TotalReturns ↑, OutstandingBalance ↓
ConcurrencyNo semaphoreSemaphoreSlim (1,1) on post
ZATCA prerequisiteNoneOriginal invoice must be IsAccepted = true

Error Scenarios

Add Phase Validation Failures

Error ConditionError MessageResolution
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 ConditionError MessageResolution
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)

Internal Documentation — Microtec