Inventory Account Category Templates
Phase 5B.3 (2026-07-16). Mapping resolution
can only fill an existing InventoryAccountCategory row — a tenant with zero rows was stuck. This
phase closes that gap: a SaaS-managed InventoryAccountCategoryTemplate catalog (one per
BusinessActivity + AccountingChartTemplate pair) plus a safe apply that creates or updates exactly
one tenant dbo.InventoryAccountCategory row.
Schema (SaaS DB)
| Table | Purpose |
|---|---|
Saas.InventoryAccountCategoryTemplates | Code, Name/FName, Description/FDescription, BusinessActivityId FK, AccountingChartTemplateId FK, Version, IsDefault |
Saas.InventoryAccountCategoryTemplateAccounts | TemplateId FK, DestinationField (one of the 13 confirmed real fields, table below), MappingKey (nullable, informational cross-reference only), AccountCode, IsRequired, Notes/FNotes — stores AccountCode, never AccountId |
Saas.TenantInventoryAccountCategoryTemplateApplyLogs | Central apply log |
Unique filtered index on the template table: (BusinessActivityId, AccountingChartTemplateId, IsDefault) WHERE IsDefault=1 AND Is_Deleted=0 — a 3-column shape (unlike
Cost Center Templates' 2-column shape), because one BusinessActivity
can legitimately pair with more than one chart template over time (e.g. the original _Basic_COA chart
and the newer _Saudi_USStyle_COA_v1 chart each get their own linked inventory-category template).
InventoryAccountCategoryTemplate/…TemplateAccount used Name/FName/Description/FDescription/
Notes/FNotes from inception — it is the entity the naming-convention cleanup
later aligned everything else to, not one that needed correcting itself.
The 13 confirmed real InventoryAccountCategory fields
This classification is code-side only — never a database column — exposed via
InventoryAccountCategoryTemplateAccountDto.UsageStatus, and confirmed directly against the real GL
posting resolvers (GeneralLedgerUtilityService.GetPurchaseAccountsAsync/GetInventoryAccountsAsync).
| Field | Paired toggle | Classification | Why |
|---|---|---|---|
InventoryId | — | RuntimeActive | Read today in Purchase Invoice / Purchase Return / Stock Opening Balance GL posting |
PurchaseDiscountId | PostPurchaseDiscount | RuntimeActive | Same resolvers |
PurchaseFreeQuantityId | PostPurchaseFreeQuantityDiscount | RuntimeActive | Same |
RevenueId | — | FuturePostingReady | Only ever fetched via the SalesRevenue MappingKey — no sales GL posting exists yet |
SalesReturnId | PostSalesReturn | FuturePostingReady | No sales-return posting exists yet |
SalesDiscountId | PostSalesDiscount | FuturePostingReady | No MappingKey/posting reads it |
SalesFreeQuantityId | PostSalesFreeQuantityDiscount | FuturePostingReady | Same |
SalesPromotionDiscountId | PostSalesPromotionDiscount | FuturePostingReady | Same |
SalesCouponDiscountId | PostSalesCouponDiscount | FuturePostingReady | Same |
CommissionExpenseId | PostCommissionPerInvoice | FuturePostingReady | Same |
CostId | — | FuturePostingReady | Fetched by the same resolvers, but never actually consumed in journal-entry creation today |
PurchaseReturnId | PostPurchaseReturn | FuturePostingReady | Same "fetched but not consumed" status as CostId |
ClearingReceiveId | PostClearingReceive | FuturePostingReady | No MappingKey/posting code reads it |
Only 3 of the 13 fields are consumed by any GL posting today (all three in Purchase Invoice/Return/
Stock-Opening-Balance flows). The remaining 10 are configured — a template can assign a real account
code to them — but no sales posting (GenerateForSalesInvoiceAsync/GenerateForSalesReturnInvoiceAsync),
no COGS posting, and no stock issue/receive/count/adjustment posting exists in the codebase yet that
would consume them. This is a deliberate "configure ahead of the feature" posture, not a bug — but it
means applying a template today does not by itself enable any new GL behavior beyond purchasing.
Target-row resolution
| Existing rows | Explicit target supplied? | Mode | Behavior |
|---|---|---|---|
| 0 | No | CreateDefault | New row created (Name="Default"/FName="Default", all 4 ApplyForAll* flags true) unless a required field's AccountCode doesn't resolve — then nothing is created at all (all-or-nothing) |
| 1 | No | UpdateSingleExisting | Force=false fills blank fields only; Force=true overwrites supported fields |
| 2+ | No | AmbiguousTarget | Nothing written — reports a warning to re-call with an explicit target |
| any | Yes | UpdateExplicitTarget | Verified to belong to the tenant, then updated like the single-existing case |
Writing an XxxId field with a paired PostXxx toggle also sets that toggle true. Never creates
dbo.Accounts/dbo.CostCenters; never updates FinancialSettings; never deletes a row; never
creates more than one row. Re-running with Force=false is idempotent by construction.
API
InventoryAccountCategoryTemplatesController, route api/Saas/InventoryAccountCategoryTemplates — full
CRUD/Copy/Activate/Deactivate on templates and their account rows, all under
Permissions.InventoryAccountCategoryTemplates.{ViewAll,View,Create,Edit,Delete,Activate,Deactivate,Apply}.
POST .../{id}/ApplyToTenant/Preview (.Apply)
POST .../{id}/ApplyToTenant (.Apply)
POST api/internal/inventory-account-category-templates/apply (Shumoul.Api, internal key)
Governed by the same Features.AccountManagement entitlement as
chart accounts/mappings.
Seed data
5 templates (one per activity's _Basic_COA chart), e.g. RestaurantCafe_Default_Inventory_Accounting,
each IsDefault=true, 13 account rows each. Phase 5D later added a second template per activity
({Activity}_Saudi_USStyle_Inventory_Accounting_v1, linked to the newer chart) — the original,
_Basic_COA-linked template is unaffected, still available.
What was deliberately not added
No automatic tenant provisioning — an explicit admin action (or, since Phase 6C, the
Tenant Starter Kit) must call Apply. No "default inventory
category" design for the genuine multi-category-tenant case (AmbiguousTarget is reported, never
guessed). Live end-to-end apply through a running internal endpoint was not exercised this phase — only
mocked-repository unit tests cover the orchestration logic (the same category of limitation every
sibling apply flow in this initiative documents).
