ZuloOne
English
Sign in
Menu

📊 ABC/XYZ for the developer

Updated

← Developer wiki

The classification is the Sales business layer, not the platform kernel and not a country package. There is no if (abc) in Core. The letters A/B/C live in the Sales/DataPackages/abc-profiles.json seed, not in C#.

User: how to press Recalculate. Why the business wants it: ABC/XYZ. The IR engine: the information register.

Objects (Sales 1.40.0)#

Name Kind
AbcProfile scheme dictionary. Empty ItemGroup (Sales 1.52.0) means the whole item list
AbcClassBand class band (Profile+Axis+ClassCode uniqueness in OnBeforeSave)
AbcClassification IR, dimensions Profile + Subject (Guid)
IAbcClassifier RecalcAsync / RecalcWithScoresAsync / RecalcAllEnabledAsync
RecalcAbc dictionary command on the profile
RecalcAbcClassifications 0 2 * * * job, the same RecalcAsync over every live profile

The service is named AbcClassifier, not AbcClassification: otherwise the service class would collide with the IR_AbcClassification POCO.

Sales depends on Costing ≥ 1.2.0 so that a profile can measure InventoryValue. There is no cycle: Costing does not know Sales.

The dictionary has no table parts — the bands are not a table part; they are a child dictionary (like the LoyaltyTier ladder).

Recalculation#

RecalcAsync reads:

  • Revenue — QueryMovementsAsync + AnalyticSetService.ExpandAsync (as TaxReturnService does);
  • Stock — QueryBalancesAsync, the Item dimension;
  • InventoryValue — balances + the Item analytic.

It writes IInformationRegisterService.SetAsync("AbcClassification", asOf, …). ManualAbc / ManualXyz on the slice: the class is not overwritten.

Non-empty ItemGroup and Subject = Item: after the scores are built, only items in that group (Item.ItemGroup) remain. Pareto and XYZ are computed over that sample; the share denominator is the group total. Slice rows for the same profile and date whose item is outside the group are deleted (QueryRecords for period = asOf, then DeleteAsync) — otherwise after narrowing the group the journal would keep showing the old item list. An empty group does not remove rows: a zero score did not enter Pareto before, and a manual class must survive recalculation. A Customer profile ignores the field.

Public DTOs are not allowed in a service signature — the I<Name> contract is assembled earlier than the entities (see the comment in TaxReturnService). The test input is IReadOnlyDictionary<Guid, decimal>.

Test: AbcClassificationTest (group Бизнес-слой.Сервисы).

Phone#

The CachedAbc projection is assembled in MobileSyncService next to CachedCampaign, on the same pull for the SalesAgent role. The source is SliceLast("AbcClassification") for each live AbcProfile whose Subject is Item or Customer. Fields: SubjectId, SubjectKind, ProfileId, ProfileName, AbcClass, XyzClass. There is no separate method in IAbcClassifier: the class already lives in the journal.

The client finds the projection by AbcClass + SubjectId and labels the line itself. One profile — A/X. Several profiles — profile name and letters, sorted alphabetically by name. The store and the manager do not receive this projection.

Replenishment#

The service is AbcPolicy, the register is AbcSuggestion, and the cell dictionary is AbcPolicyCell. The service name does not match the register: otherwise the class would collide with the IR_AbcSuggestion POCO, as already happened with AbcClassifier.

AbcStockMode zero = Unspecified. The enum is non-nullable, so an empty cell is zero and does not write a suggestion row. Keep computes coverage; Order writes 0 to the order quantity.

Demand is read from SalesRealization lines (Issued and Shipped), BaseQuantity, or Quantity when it is 0. Negative Stock movements cannot be used: moving between cells is also a negative movement, so demand would be inflated. Balance is QueryBalancesAsync over Stock, summing Qty.

A repeat calculation for the same date deletes rows of this profile for which the cell no longer exists (QueryRecords for period = asOf, then DeleteAsync). A different slice date is not touched.

A Customer profile is an exception, not an empty journal: a customer has no stock balance. BuildEnabledItemProfilesAsync therefore checks Subject = Item and IsDisabled and calls the same BuildAsync as the button. After RecalcAllEnabledAsync, the RecalcAbcClassifications job (Sales 1.53.2) calls it for the same date: the classes are already in the slice when the suggestion is written.

Public BuildWithDemandAsync accepts BCL dictionaries, like RecalcWithScoresAsync. The contract does not see model types.

Test: AbcPolicyTest.

What not to do#

  • Do not put ABC in Localization.* and do not write classes onto Item.
  • Do not compute prices and do not move Stock from this service — only read registers and write AbcSuggestion.
  • Purchase draft — IAbcPolicy.CreatePurchaseDraftsAsync, command CreateAbcPurchaseDrafts on AbcProfile. The supplier, cells, units and UnitPrice come from the last PurchaseOrder line in subtype Received (by DocumentDate, and by MetaId on ties). Do not fill ExpectedReceipt: an empty date does not promise ATP. No received line or an empty cell — skip the item; do not substitute the price from DefaultPurchasePrice. A repeat looks for a draft with the same AbcProfile (extension field PurchaseOrder.Sales), supplier, cell and legal entity, and does not create a second one. The abc-replenish widget does not write the order.
  • Do not add an SPA/Flutter band designer: the phone shows the class slice; bands and cells are edited in the office.
  • Do not put a second classifier in the job: the button and cron call the same RecalcAsync. The suggestion in the same job is the same BuildAsync as the BuildAbcPolicy button, and only for an item profile.

Same topic for other readers: Analyst · User

Where next#

Was this page helpful?