Register — accumulation register
Updated
What it is#
A balances/turnovers view by accounting breakdowns. Each transactional posting writes a movement for a resource (resources) with concrete breakdown values. Breakdowns are almost always dynamic analytics from the global catalog (analytics; see Analytic), not physical dimensions: create physical dimensions only when you need to read raw movements by this breakdown. The totals driver sees only coordinates, not movement analytics; QueryMovementsAsync filters by dimension, but not by analytic. isDoubleEntry: false (the overwhelming majority of registers, including store and turnover registers — receipt and issue are the same one-sided posting, just with different signs) means the movement is written with transactions.Add. true means the movement is a transfer of a resource between two sets of breakdowns inside one register, and both sides must post atomically as one posting (transactionPairs.Add(outcome, income)); otherwise the whole movement is rejected (inter-store transfer, money register with two personal accounts). Do not set true only because a movement reduces the balance — a write-off/issue remains one-sided, isDoubleEntry: false (see zuloone-new-register; the live example is Stock, which people tried to make double-entry and then rolled back). totalDriverMetaId binds the totals calculation algorithm (driver; see below) — Standard/FIFO/Money/custom; registerEngineType: "Information" is a separate information-register engine (not accumulation, an IR_ table, slice of latest/earliest values by dimensions), see zuloone-new-register §9.
JSON envelope#
<Name>.object.json, "kind": "Register":
{
"kind": "Register",
"object": {
"caption": { "en": "Stock", "ru": "Остатки" },
"registerEngineType": "Standard",
"totalDriverMetaId": "<GUID TotalDriver>",
"isDoubleEntry": false,
"allowNegativeBalance": true,
"useBalanceTable": true,
"metaId": "<GUID Register>", "name": "Stock", "modelId": "<GUID Model>"
},
"dimensions": [
{
"registerMetaId": "<GUID Register>",
"fieldName": "Item",
"edtMetaId": "<GUID EDT Reference>",
"isOperational": true, "isInformational": false, "isPivotDimension": false,
"displayOrder": 1,
"caption": { "en": "Item", "ru": "Товар" },
"metaId": "<GUID Dim>", "name": "Item", "modelId": "<GUID Model>"
}
],
"resources": [
{
"registerMetaId": "<GUID Register>",
"fieldName": "Qty",
"edtMetaId": "<GUID EDT Qty>",
"isOperational": true, "displayOrder": 1,
"caption": { "en": "Qty", "ru": "Кол-во" },
"metaId": "<GUID Res>", "name": "Qty", "modelId": "<GUID Model>"
}
]
}
Dimensions are either isOperational (participate in posting from the script) or isInformational (filled by the platform for reporting). Reusable dimensions are declared once as Analytic and attached by edtMetaId.
C# base#
Totals driver: public partial class <Name>TotalDriver — overrides calculation hooks; the class is generated and is not declared by hand. Important: the driver sees only transactions (TransactionBase — coordinates, with no access to movement analytics). If a hook must match by analytic, the analytic must get into the transaction itself at write time, not be read by the driver after the fact.
Events / Chain of Command#
A driver wrapper (not the calculation itself) is like a command and tx: a generated class; [Replace] swallows the whole chain, and a missing next in someone else's wrapper is a compilation error (see wiki/developer/chain-of-command.md). Movements are written from a document transaction script (transactions.Add(new <Register> {...}); double entry uses transactionPairs.Add(minus, plus)), not from dictionary/document events. Writing from an event to another table (including a register table) inside OnAfter*Insert escalates to a distributed transaction, which is unsupported on Linux.
Example#
Stock in Inventory — single-entry, dimensions Cell+Item, resource Qty, allowNegativeBalance: true (there is no "external counterparty" basket; the balance is always factual).