Write a totals driver hook
Updated
A totals driver is the calculation that runs because a register moved,
not because a particular document posted. Most registers need none:
registerEngineType: "Standard" is enough. Write a driver when the rule
must follow every document that touches the register (cost write-off,
first-event VAT) or when the engine itself is special (FIFO lots).
Note
The class is
public partial class <Name>TotalDriver. Do not declare the base — the platform generates it frombaseClassName. Do not editCore/TotalDrivers/; those*.engine.csfiles are read-only copies of the kernel engines.
How to#
1. The driver object — TotalDrivers/<Name>/<Name>.driver.json,
kind: "TotalDriver". Set baseClassName to a kernel engine
(DefaultTotalDriver, FifoTotalDriver, FifoStockTotalDriver,
StockBaseTotalDriver, StockQtyBaseTotalDriver, MoneyTotalDriver,
CashlessMoneyTotalDriver, MarginTotalDriver,
CurrencyExchangeTotalDriver) and baseEngine: "Standard". isKernel is
false — you are not shipping a Core engine.
2. The hook — the platform seeds <Name>TotalDriver.script.json +
.cs beside it (scriptType: "TotalDriver"). Override only the hooks you
need and call base:
| Hook | When |
|---|---|
ValidateTransactions |
The platform hands the whole document movement set. Inspect, do not write yet. |
EndDocument |
Movements are already stored. The one place after write — Costing and UA first-event accrue here. |
CalculateOutcomes / CalculateIncomes |
Split / value a movement (FIFO family). |
CalculatePartialAmount |
Partial lot cost. Exists from FifoTotalDriver down, not on DefaultTotalDriver. |
public partial class CostingIssueTotalDriver
{
public override void ValidateTransactions(
TransactionPairCollection transactionPairs, TransactionCollection transactions)
{
base.ValidateTransactions(transactionPairs, transactions);
// fold this register's movements by item…
}
public override void EndDocument(DateTime transactionDate, Guid docId)
{
base.EndDocument(transactionDate, docId);
// write-off after the stock movements exist
}
}
3. Bind it on the register: totalDriverMetaId in
Registers/<Name>/<Name>.object.json. That engine replaces the
register's own. Do not hang a second driver beside it — wrap this
script with Chain of Command (next(),
forgotten next is ZOCOC003).
4. Bump modelVersion in the same change.
An information register has no driver.
A kernel engine (StockBaseTotalDriver, …) is wrapped with next(), not
a partial class of the same name.
Warning
A driver sees coordinates (
TransactionBase). It does not see dynamic analytics at all. Declare a cut the hook must read as a physicaldimension. If you bind it only as an analytic,IsCoordinateNulldrops every movement, the hook is a no-op, and the log is silent — movements stored, tax or cost not accrued. That is whyUaVatFirstEventmadeLegalEntitya dimension.
Costing lives in the driver on Stock so every future issue document
cannot forget the write-off. Ukraine first-event VAT is the same shape:
the rule is the increment of the base, not a posting on each document.
See Create a register and
Accumulation register.
What's not there yet#
You cannot retarget an already-issued FIFO lot from a script and "just
change the amount". A quantity-zero, amount-only movement poisons both
FIFO and AVG. Late price fixes go to P&L or through a
StockAdjustment that carries quantity. Core/ copies are for reading
signatures, not for editing.