📊 ABC/XYZ for the developer
Updated
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(asTaxReturnServicedoes);Stock—QueryBalancesAsync, theItemdimension;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 ontoItem. - Do not compute prices and do not move
Stockfrom this service — only read registers and writeAbcSuggestion. - Purchase draft —
IAbcPolicy.CreatePurchaseDraftsAsync, commandCreateAbcPurchaseDraftsonAbcProfile. The supplier, cells, units andUnitPricecome from the lastPurchaseOrderline in subtypeReceived(byDocumentDate, and byMetaIdon ties). Do not fillExpectedReceipt: an empty date does not promise ATP. No received line or an empty cell — skip the item; do not substitute the price fromDefaultPurchasePrice. A repeat looks for a draft with the sameAbcProfile(extension fieldPurchaseOrder.Sales), supplier, cell and legal entity, and does not create a second one. Theabc-replenishwidget 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 sameBuildAsyncas theBuildAbcPolicybutton, and only for an item profile.
Same topic for other readers: Analyst · User