🗺️ Overview and the map of modules
Updated
The ZuloOne business layer is 14 models. A model is a workspace folder with
its own model.json, its own version and explicitly declared dependencies. A
model sees the layers below it and extends their objects additively — adding
fields and attaching handlers on top — but never edits what belongs to another model.
A new country or an industry-specific solution is a new model on the Customization
layer, not a patch to the kernel.
Below is what each model contains and which questions its registers answer.
The full object specifications are in
docs/reference/business-layer/.
What the business layer is made of#
Four object kinds appear throughout the business layer:
- A dictionary — something that exists in its own right and is referenced: an item, a customer, a store, a tax rate. It lives long and changes rarely.
- A document — an event of economic life, with a date: an order, a realization, a payment, an accrual. A document has subtypes — states. The transition between them is made by a command (a button). The subtype change runs the postings; there is no state dropdown on the card. See commands and subtypes.
- A register — a derived table of movements. Nobody enters rows by hand and nobody copies rows into it: movements are created by posting documents and rebuilt when a document is reposted. A balance is computed from the movements.
- A service — shared logic called from more than one place
(
GetService<IPricingService>()). The bodies of handlers and commands are thin orchestration on top of services.
The map of models#
The layers: Base(1) is the foundation, Solution(2) the applied contours,
Customization(3) the country packages. A model sees only what is below it and
what it declared in its dependencies.
| Module | Layer | Depends on | Holds | More |
|---|---|---|---|---|
| Common | Base | — | 11 dictionaries, 3 services | 00-Common |
| Organization | Base | Common | 4 dictionaries | 01-Organization |
| Accounting | Base | Common, Organization | 4 dict., 1 document, the GL register |
02-Accounting · the ledger |
| Tax | Base | Accounting, Common, Organization | 20 dict., 4 documents, TaxLedger |
03-Tax |
| Inventory | Solution | Accounting, Common, Organization, Tax | 15 dict., 6 documents, Stock |
04-Inventory · stores |
| Purchasing | Solution | Accounting, Common, Inventory, Organization, Tax | 1 dict., 4 documents, Payable |
05-Purchasing |
| Sales | Solution | Accounting, Common, Inventory, Organization, Tax, Costing | 10 dict., 9 documents, 6 registers | 07-Sales · the process |
| Production | Solution | Common, Costing, Inventory | 6 dict., 1 document, WorkInProgress |
06-Production · the process |
| HR | Solution | Accounting, Common, Organization | 4 dict., 5 documents, 3 registers | 08-HR · the timesheet |
| CRM | Solution | Inventory, Sales | 3 dict., 3 documents, LoyaltyPoints |
09-CRM |
| Costing | Solution | Inventory, Purchasing | 2 dict., 2 registers | 10-Costing · the process |
| LocalizationSaudiArabia | Customization | Inventory, Sales, Tax, Organization | 2 dict., VatPayable, 8 services |
21 KSA · the package · ZATCA |
| LocalizationUkraine | Customization | Organization, Inventory, Sales, Tax, Purchasing, HR | 6 dict., 8 registers | Ukraine |
Two things a reader of older material will expect and not find: store, zone and cell live as dictionaries inside Inventory — there is no separate warehouse application — and the field agent is a role of the mobile client, not a model of its own.
The modules one by one#
Common — the shared building blocks#
Everything that other models reference and that belongs to no single contour.
Dictionaries (11): Country, Region, City, Address, Currency,
UnitOfMeasure, UnitClass, PaymentTerm, DeliveryTerm (Incoterms),
Gender, CommonSettings.
Services: UnitConverter — converting quantities between units;
PaymentDueService — the due date out of the payment terms;
TradeProfileService — a counterparty's trade profile (customer / supplier /
both).
A country's currencies are stored in the CountryCurrency link table rather than in one field: a country may have several currencies. Unit conversion works through a class plus a
ratio (and packaging on the item itself) rather than pairwise rules — see
stores.
Organization — who exactly keeps the books#
Dictionaries (4): LegalEntity, Division, DivisionType,
OrganizationSettings.
A legal entity is not decoration: it is a dimension in the tax ledger, in the general ledger and in every Ukrainian register. The tax regime, the registrations and the connections to the tax authority are all attached to it. A document without a legal entity cannot be posted.
Accounting — the chart of accounts and the ledger#
Dictionaries (4): ChartOfAccounts, FiscalYear, FiscalPeriod,
AccountingSettings.
Document: JournalEntry — a manual posting.
The GL register — double entry; ΣDebit = ΣCredit is checked on the
document. Three pairs of resources: Debit/Credit (financial),
TaxDebit/TaxCredit (tax) and ManagementDebit/ManagementCredit (management) —
one fact, three views of accounting, with no duplicated documents.
Services: GeneralLedgerService (the mechanics of a ledger leg — accounts by
code, period by date, the three books), FiscalPeriodService (deriving the
period from the document's date).
Every business document writes its own ledger legs, the way a 1C document writes its own accounting-register records: the sale posts revenue and cost of sales, the receipt posts inventory and payables, payroll posts the expense. The legs are the document's own movements, so cancelling the document takes them out of the book. Who writes the general ledger lists every leg.
The period is taken from the document's date, not from "today". Otherwise a backdated document drifts away from the subledger across periods, and it is only visible at the close. Separately from the fiscal periods there is a posting boundary — one tenant-wide stop date, in the Administrator menu.
The model provides the sales order print form. It extends another model's document; it is not a separate object.
Tax — tax as an engine, not a rate in a field#
The largest model by dictionary count: 20. Tax here is computed by rules held
as data, so a new country does not require an if (country) branch.
The core: Tax, TaxCode, TaxRate, TaxCategory, TaxDirection,
TaxJurisdiction.
Rate determination: TaxRule + TaxRuleCondition + TaxRuleAction,
TaxProfile + TaxProfileAttribute, TaxMapping.
Registrations and exemptions: TaxRegistration (dated),
TaxExemptionReason.
Reporting: TaxPeriod, TaxSubmission, TaxReportMapping, TaxAuthority,
TaxAuthorityConnection, plus TaxSettings.
Documents (4): TaxCalculation, TaxDocument, TaxPayment, TaxReturn.
The TaxLedger register — the base and the amount of tax broken down by
code, direction and legal entity, plus RecoverableAmount. The return is
assembled from it.
The return is assembled by the Build return button on a TaxPeriod row
(BuildTaxReturn → ITaxReturnService.BuildFromPeriodAsync): the period window
is copied to TaxReturn, and the amounts and lines are computed from the ledger.
A closed tax or financial period refuses with a message. There is no need to
create a return by hand and type OutputTax / InputTax: before the button
existed, that was the only way in, because the service had no entry point of its own.
Services: TaxService (the calculation), TaxReturnService,
TaxAuthoritySubmitService, TaxPackInstaller, EInvoiceRelease.
A trap that cost time: tax determination requires a context. Rules, profiles and mappings are skipped silently when no context is passed — and then a credit note is reversed at a different rate from the one its own invoice was issued at.
Inventory — items, stores and prices#
Master data (14): Item, ItemGroup, ItemUnit, Brand,
Store / StoreZone / StoreCell / StoreCellType, Customer,
CustomerContact, CustomerOutlet, Supplier, PriceType,
InventorySettings.
Documents (6): PutAwayTask, PickTask, StockTransfer, GoodsIssue,
StockAdjustment, StockCount (a stocktake).
The Stock register — quantity broken down by cell and item, single
entry, Qty only. There is no value here: that lives in Costing.
Services: PricingService (a price with its provenance),
StockAvailabilityService (may this be written off), ItemQuantityConverter,
StoreCellService, ItemClassification, TenantSetup.
Counterparties sit here for a reason: customers and suppliers are master
data, and in the menu they are placed where they are used (Sales and
Purchasing). PriceTypeHistory is the link table of price type history.
The balance is per cell, and that matters: goods cannot be teleported, movements are paired (receiving → storage → picking), and a write-off comes strictly out of the picking cell.
Purchasing — goods in and money to the supplier#
Documents (4): PurchaseOrder (Draft → Ordered → Received),
PurchaseReturn, PurchaseCreditNote (reverses input VAT), VendorPayment.
The Payable register — the debt to suppliers, broken down by supplier.
Service: PurchaseReceiptService.
It has almost no dictionaries of its own — one PurchasingSettings: the supplier
and the items come from Inventory. SupplierInvoice books only the difference
from what the receipt already accrued — process map §8.
Sales — the largest model of the contour#
Dictionaries (10): SalesContract, SalesPerson, DeliveryRoute,
DeliveryRouteStop, DeliverySchedule, Driver, Vehicle, AbcProfile,
AbcClassBand, SalesSettings.
Documents (9): SalesOrder (Draft → Submitted → Confirmed),
SalesRealization (Reserved … Issued), CustomerPayment, SalesReturn,
SalesCreditNote, SalesDebitNote, VisitRoute, AgentVisit, DeliveryTrip.
Registers (6):
Receivable— what customers owe: it arises when a sales realization is released and is settled by a payment; the slice is customer and contract. The mirror ofPayable.ReservedStock— the quantity held by confirmed orders. Free = Stock − Reserved; it is released by rolling theConfirmedsubtype back.Revenue— recognised revenue by item, customer and contract.AbcClassification— the ABC/XYZ slice by profile and subject; a manual classification (ManualAbc/ManualXyz) survives a recalculation.AgentGpsLog— an information register: a point on an agent's track.MobileCommandReceipt— the idempotency information register for mobile commands: for each key it holds the action, the status and the stored answer; a repeated key returns the same answer rather than a second document.
Services: SalesLinePricing, SalesContractService,
SalesFulfillmentService, DeliveryService, GrossMarginService,
AbcClassifier.
A contract belongs to a trading outlet (Outlet is mandatory, and the
customer is copied from the outlet), and it is the contract that carries the
currency, the settlement kind, the payment term, the Incoterms, the price type,
the limit and the date window. The contract's prices are the SalesContractPrice
and SalesContractQtyBreak link tables. The ABC/XYZ recalculation is a job,
RecalcAbcClassifications.
Production — bills of materials and output#
Dictionaries (6): BillOfMaterials, BomComponent, Routing,
RoutingOperation, WorkCenter, ProductionSettings.
Document: ProductionOrder (Released → Finished).
The WorkInProgress register — the quantity of the product in progress,
not its value: FIFO has already left the store when the raw material was written
off.
Services: BomService, RoutingService.
HR — people, the timesheet and payroll#
Dictionaries (4): Employee, Position, TimeOff, HRSettings.
Documents (5): TimeSheet, PayrollAccrual, PayrollPayment,
SocialInsuranceAccrual, SocialInsurancePayment.
Registers (3): Payroll (accrued), PayrollLiability (Amount +
Withheld, negatives forbidden), SocialInsurance (EmployeeContribution /
EmployerContribution).
Services: PayrollCalculationService, PayrollVoidService,
SocialInsuranceService, TimeSheetService.
The Withheld resource on PayrollLiability is the contract for any
withholding from pay: the Ukrainian personal income tax (ПДФО) and military levy (військовий збір) write into exactly
that one, and the localization introduces no withholding mechanism of its own.
Voiding an accrual is a separate command (VoidPayrollAccrual), not deleting the
document.
CRM — the lead, the quotation and loyalty#
Dictionaries (3): LoyaltyTier, LoyaltyCampaign, CRMSettings.
Documents (3): SalesLead (New → Qualified → Quoted), SalesQuotation
(Draft → Issued → Converted), LoyaltyRedemption.
The LoyaltyPoints register — points by customer, negatives forbidden:
the engine will not let more be redeemed than was accrued.
Services: LeadService, QuotationService, LoyaltyCampaignService.
CRM is built on top of the order axis additively and does not replace it: the lead and the quotation are optional, and an order stands on its own. More — the lead, the quotation, loyalty.
Costing — what it cost#
Dictionaries (1): CostingSettings (the standard cost is a price type: CostingSettings.StandardPriceType).
Registers (2): ItemCostFifo — FIFO lots by item (Quantity + Amount);
InventoryValue — the quantity and the value of stock.
Services: StandardCostService, SurplusCostingService.
The split from Inventory is deliberate: Stock answers "how much is in stock", InventoryValue "worth how much". The sales margin is computed in Sales
(GrossMarginService), not here.
LocalizationSaudiArabia — Saudi law and services#
VAT and the e-invoice are built today; the package does not end there.
Dictionaries (2): LocalizationSaudiArabiaSettings, FatooraStubDelivery.
The VatPayable register — VAT payable.
Services (8): SaudiEInvoice, ZatcaQr (the TLV QR), ZatcaXades (the
signature), ZatcaCsr, ZatcaOnboarding, ZatcaComplianceSamples,
FatooraSigner, EInvoiceRelease. Job: ApplyFatooraOutcomes.
The model carries the print forms for the realization and the credit and debit
notes — those are extensions of Sales documents, not objects of its own. The E
and O categories with their VATEX codes live on the
TaxCategory.LocalizationSaudiArabia extension. A standard invoice cannot be
handed to the customer before the Cleared status. GOSI is computed by the HR
kernel at the stand's rates. Mudad, Qiwa, Nafath, SADAD and Saudi banks are not
in the model. More — the package ·
ZATCA.
LocalizationUkraine — Ukrainian law and services#
VAT (ПДВ), the regimes, single tax (єдиний податок) and payroll withholdings are built today; the package does not end at tax.
Dictionaries (6): LocalizationUkraineSettings, UaRegimeChange,
UaSingleTaxLimit, UaBankConnection, UaLandPlot, UaTaxFilingExport.
Registers (8) — more than any other localization has, and each answers its
own question:
UaVatFirstEvent— the accumulations on a contract (Shipped,Accrued,Paid,EpAccrued) from which the first event on the output side is derived (ПКУ 187.1). The key is the legal entity / customer / contract dimensions rather than analytics: a totals driver sees coordinates only.UaVatPayable— the liability itself, by the first event.UaPurchaseFirstEvent— the mirror on the input side (Received,PaidOut,Credited), Tax Code of Ukraine (ПКУ) 198.2.UaVatCredit— the right to credit on an input first event.UaPayrollLevy— personal income tax (ПДФО) and military levy (військовий збір) broken down by employee:TaxLedgeris sliced by direction, legal entity and tax code — enough to pay the budget, and not enough to show a withholding per person.UaBankStatement— imported Autoclient bank-statement entries (an information register, not a payment document).UaFopEsv— the social insurance contribution (ЄСВ) accrued by a sole proprietor (ФОП) "for self", by legal entity.UaVatRefundClaim— the declared amount of VAT budget refund.
Services: UaFirstEvent, UaTaxFiling, UaPayrollLevies, UaEInvoice,
UaBank.
Jobs: ApplyUaRegimeChanges, PullUaTaxInvoiceReceipts,
PullUaBankStatements.
Social insurance (ЄСВ) requires no separate code: it is the social insurance contour in HR. The
tax invoice (податкова накладна) goes to the Unified Register of Tax Invoices
(ЄРПН) through an operator: the TaxDocument envelope is ours, and the XML and
qualified electronic signatures (КЕП) stay with M.E.Doc. The MakeDoc adapter is in the package, but there is no
live exchange in CI. The PrivatBank statement uses the pb-statements channel.
There is no Diia or State Tax Service (ДПС) cabinet. More — Ukraine.
Every register in one list#
Which question each of them answers. Register movements are derived: they are not entered by hand, and they are rebuilt when a document is reposted.
A breakdown has two kinds. For an analyst, the question is: "which fields can I use in a report?" A dimension is the register's own
column. An analytic is a dynamic breakdown alongside; the set of analytics
grows without touching the register itself. For a developer the difference
matters more: code that computes balances during posting sees dimensions only. If you slice the register only by analytics, that code matches zero rows — silently, with no error. That is why the UaVatFirstEvent key is dimensions (legal entity ×
customer × contract), without analytics that duplicate those names. The outlet
slice lives on UaVatPayable, where the driver writes it. Details —
the developer wiki.
| Register | Module | Dimensions | Analytics | Resources | Answers the question |
|---|---|---|---|---|---|
Stock |
Inventory | Cell, Item | — | Qty |
how much is in stock, and where |
ReservedStock |
Sales | — | Cell, Item | Qty |
how much is held by orders |
InventoryValue |
Costing | — | Item | Qty, Value |
worth how much |
ItemCostFifo |
Costing | Item | — | Quantity, Amount |
at what price to write off |
WorkInProgress |
Production | — | Cell, Item | Qty |
how much is in progress |
Receivable |
Sales | — | Customer, SalesContract | Amount |
how much is owed to us |
Payable |
Purchasing | — | Supplier | Amount |
how much we owe |
Revenue |
Sales | — | Customer, Item, SalesContract | Amount |
how much revenue is recognised |
GL |
Accounting | — | Account, FiscalPeriod, LegalEntity | 3 Dr/Cr pairs | what is in the ledger |
TaxLedger |
Tax | — | TaxCode, Direction, LegalEntity | TaxBase, TaxAmount, RecoverableAmount |
what goes into the return |
Payroll |
HR | — | Division, Employee | Amount |
how much is accrued |
PayrollLiability |
HR | — | Employee | Amount, Withheld |
how much is payable and how much withheld |
SocialInsurance |
HR | — | Division, Employee | employee / employer contribution | social insurance |
LoyaltyPoints |
CRM | Customer | — | Points |
how many points |
AbcClassification |
Sales | Profile, Subject | — | class, share, Cv |
which ABC/XYZ class the item or customer has |
AgentGpsLog |
Sales | Agent | — | coordinates, speed, battery | where the agent has been |
MobileCommandReceipt |
Sales | Key | — | status, answer | is this a replay |
VatPayable |
KSA | — | Customer, CustomerOutlet, SalesContract | Amount |
VAT payable |
UaVatFirstEvent |
Ukraine | LegalEntity, Customer, SalesContract | — | Shipped, Accrued, Paid, EpAccrued |
has the first event occurred |
UaVatPayable |
Ukraine | — | Customer, CustomerOutlet, SalesContract | Amount |
the VAT (ПДВ) liability |
UaPurchaseFirstEvent |
Ukraine | LegalEntity, Supplier | — | Received, PaidOut, Credited |
the first event on the input side |
UaVatCredit |
Ukraine | LegalEntity, Supplier | — | Amount |
the tax credit |
UaPayrollLevy |
Ukraine | Employee, LegalEntity, TaxCode | — | Base, Amount |
personal income tax (ПДФО) and military levy (ВЗ) per person |
UaBankStatement |
Ukraine | LegalEntity, BankRef | — | Amount, TranType, CounterpartyEdrpou |
what came in or went out on the bank statement |
UaFopEsv |
Ukraine | LegalEntity | — | Amount |
the social insurance contribution (ЄСВ) accrued by a sole proprietor (ФОП) "for self" |
UaVatRefundClaim |
Ukraine | LegalEntity | — | Amount |
the declared amount of VAT budget refund |
ReservedStock is sliced by the same coordinates as Stock (cell + item) — and
that is precisely why the subtraction "free = Stock − Reserved" means anything at
all.
A negative on Stock is allowed by the engine (allowNegativeBalance: true) — the refusal lives in the events and commands
(IStockAvailabilityService), not in the engine. LoyaltyPoints and
PayrollLiability, by contrast, forbid a negative at the register level.
Three numbers that show how it is built#
The free balance is computed per cell, not per store. Stock and
ReservedStock are sliced identically (cell + item) — which is why the
subtraction means something, and why it does not sum across the store:
There are 200 pcs in the store: 100 in picking cell
A, 100 in storage cellB. An order for 40 has not been picked yet: the reservation is inB, so free inBis 60 and free inAis 100. Picking more than sixty fromBis not possible, even though another hundred pieces are inA. After picking is confirmed, the 40 move toAtogether with the reservation.
The first event is shifted by a maximum, not a sum. A prepayment of ₴120 at a
rate of 20% closes a base of ₴100; a subsequent shipment of ₴100 accrues nothing,
because max(Shipped, Paid) has not grown. The walk-through with numbers and by
regime — Ukraine.
One fact, three pairs in the ledger. A sale writes into GL simultaneously
Debit/Credit (the financial view), TaxDebit/TaxCredit (the tax one) and
ManagementDebit/ManagementCredit (the management one). These are not three
postings and not three documents: the amounts in the pairs diverge where the
recognition rules themselves diverge, and a report picks the pair it needs.
What's not there yet#
Limits a consultant may look for in setup and not find:
- Lots, serial numbers and expiry dates as a stock contour — not in the product; a barcode on the print form exists.
- A live M.E.Doc / State Tax Service (ДПС) cabinet — the M.E.Doc adapter is in the package; this is REST on the accountant's PC plus an agent, not a cloud. How to seat it without pushing to the State Tax Service (ДПС) — M.E.Doc. There is no State Tax Service (ДПС) cabinet.
- Diia — not in the delivery. Sign-in is platform identity, not a country package. There is no Diia signing or sharing channel. Why it is separate.
- Opportunities in CRM — the lead and the quotation exist; there is no opportunity object.
How the processes connect (the end-to-end flow)#
Purchase → Receipt into the store → (Production: raw material out → output) → Order → Realization
│ │ │ │
▼ ▼ ▼ ▼
Payable Stock WorkInProgress ReservedStock,
ItemCostFifo ItemCostFifo then Stock,
InventoryValue Revenue, Receivable,
TaxLedger
every document above writes its own legs into GL
- Master data (items, counterparties, stores) is supplied by Inventory/Common.
- Movements go through documents, which move registers in pairs and by the scripts' rules. From the UI a document is posted by a command (a change of subtype) — not a state dropdown; see commands and subtypes.
- Cost is carried over onto the output and the shipment (FIFO), and tax is computed by the tax engine at the points of sale and purchase.
- The ledger is filled by the documents themselves: each posts its own legs, and loses them when it is cancelled.
The invariants an analyst should know#
- Goods cannot be "teleported" — only paired movements between cells.
- More cannot be written off than is in the cell: the refusal comes from
IStockAvailabilityService, not from the register engine. - Register movements are derived: not entered by hand, not copied, regenerated by reposting.
- A posting's date is the document's date, not "today". Otherwise a backdated document ends up in a different period.
- A subtype is frozen once saved: it can only be moved by a command, and only along a permitted edge.
- The tax integration is universal (
TaxAuthorityConnection), and a country is connected by a localization rather than by rewriting the kernel. - In the kernel (Core / Sales / Tax / Pricing)
if (country == …)is forbidden: a new law is a seed and rules held as data in the country's package.
The menu ribbon#
Every ribbon tab is a module. Its columns are: Documents,
Dictionaries, Totals, Settings. A menu item appears where users need it, not necessarily where the object's metadata lives. A user command (an import, a processing run)
can be placed as a targetType: UserCommand item — a click runs the command
rather than opening a page.
| Tab | What is here |
|---|---|
| Common | countries, addresses, currencies, units of measure, payment and delivery terms |
| Organization | legal entities and divisions |
| Accounting | the chart of accounts, the journal, the general ledger, fiscal periods |
| Tax | kinds/codes/rates, rules, calculations, returns, payments |
| Inventory | items, stores and cells, put-away/picking, balances, write-offs, stocktakes |
| Production | bills of materials, routings, work centres, production orders |
| Costing | the value of stock, standard cost |
| Sales | customers, trading outlets, contracts, orders, realizations, payments, returns, routes and visits, ABC profiles |
| Purchasing | suppliers, purchase orders, payments, payables |
| HR | employees, timesheets, payroll, social insurance |
| CRM | leads, quotations, loyalty tiers and points |
| Saudi Arabia | VAT payable, ZATCA settings |
| Ukraine | regimes, single-tax limit, declaration export |
Price list rows, bill-of-materials components and tax rule conditions are not shown in the menu: they open from the parent dictionary.
Where next#
How each contour runs — the process map. How Global Core and a country package split — product direction.
The contours in detail (for the analyst): stores, sales and purchasing, the ledger, prices, costing, production, the timesheet, ABC/XYZ, loyalty, KSA, Ukraine.
Where to click (for the user): getting started, working with data, the form designer, payroll, the mobile app.
How it works inside (for the developer): the architecture, information registers, Chain of Command, the forms system, what scripts can do, the posting boundary.
The object specifications —
docs/reference/business-layer/.