Skip to main content
Version: Latest

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)

TablePurpose
Saas.InventoryAccountCategoryTemplatesCode, Name/FName, Description/FDescription, BusinessActivityId FK, AccountingChartTemplateId FK, Version, IsDefault
Saas.InventoryAccountCategoryTemplateAccountsTemplateId FK, DestinationField (one of the 13 confirmed real fields, table below), MappingKey (nullable, informational cross-reference only), AccountCode, IsRequired, Notes/FNotesstores AccountCode, never AccountId
Saas.TenantInventoryAccountCategoryTemplateApplyLogsCentral 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).

FieldPaired toggleClassificationWhy
InventoryIdRuntimeActiveRead today in Purchase Invoice / Purchase Return / Stock Opening Balance GL posting
PurchaseDiscountIdPostPurchaseDiscountRuntimeActiveSame resolvers
PurchaseFreeQuantityIdPostPurchaseFreeQuantityDiscountRuntimeActiveSame
RevenueIdFuturePostingReadyOnly ever fetched via the SalesRevenue MappingKey — no sales GL posting exists yet
SalesReturnIdPostSalesReturnFuturePostingReadyNo sales-return posting exists yet
SalesDiscountIdPostSalesDiscountFuturePostingReadyNo MappingKey/posting reads it
SalesFreeQuantityIdPostSalesFreeQuantityDiscountFuturePostingReadySame
SalesPromotionDiscountIdPostSalesPromotionDiscountFuturePostingReadySame
SalesCouponDiscountIdPostSalesCouponDiscountFuturePostingReadySame
CommissionExpenseIdPostCommissionPerInvoiceFuturePostingReadySame
CostIdFuturePostingReadyFetched by the same resolvers, but never actually consumed in journal-entry creation today
PurchaseReturnIdPostPurchaseReturnFuturePostingReadySame "fetched but not consumed" status as CostId
ClearingReceiveIdPostClearingReceiveFuturePostingReadyNo 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 rowsExplicit target supplied?ModeBehavior
0NoCreateDefaultNew 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)
1NoUpdateSingleExistingForce=false fills blank fields only; Force=true overwrites supported fields
2+NoAmbiguousTargetNothing written — reports a warning to re-call with an explicit target
anyYesUpdateExplicitTargetVerified 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).