ZuloOne
English
Sign in
Menu

01 · Organization — legal entities and org structure

Updated

Layer Base(1). Depends on Common — country, currency, address. It describes who owns operations (legal entities) and where they happen (the division hierarchy).

← General specification

Purpose#

Legal entities, with their country and functional currency, and the hierarchy of divisions/sites. Organization has no documents and no registers — only dictionaries, classifiers, and the default tenant setting that almost the whole workspace reads.

A legal entity's country triggers installation of the country tax package, but this model does not install it. See LegalEntity for why.

What is created (object map)#

Kind Name Purpose
Model Organization module model, layer Base(1)
Dictionary LegalEntity legal entity — the owner of operations
Dictionary Division division/site (hierarchical)
Dictionary DivisionType classifier of site roles
Dictionary OrganizationSettings tenant settings (singleton): default legal entity
Analytic LegalEntity, Division dimensions for GL/TaxLedger/Payroll registers
EDT RefLegalEntity, RefDivision, RefDivisionType, DivisionParentRef reference types
Menu menu.json the Organization tab
— reused — Country, Currency, Address (Common) not created here

Total: 4 dictionaries, 2 analytics, 4 EDTs, 1 test data package, and 3 integration tests. No documents, no registers, and no services.

model.json#

Layer Base(1). The only dependency is Common (minVersion 1.10.0). Organization does not depend on Tax, Inventory, or any application model — the dependency goes strictly in the opposite direction.

Number sequences#

LegalEntitySeq, DivisionSeq, DivisionTypeSeq (each dictionary has its own, starting at 1000), OrganizationSettingsSeq (starts at 1, the only record).

EDT#

Reference EDTs: RefLegalEntity, RefDivision, RefDivisionType. Separately, DivisionParentRef (also a reference to Division, but time-aware), which types the system ParentId of the hierarchy.

Dictionaries#

LegalEntity#

isLogged: true (the only audited dictionary in the model). ID (system), Name (String 160, required), RegistrationNumber (String 32, required, unique — the legal entity's business identity), Country (RefCountry, required), Currency (RefCurrency, required), LegalAddress (RefAddress, optional), TaxRegistrationNumber (String 64, indexed, optional).

TaxRegistrationNumber is the legal entity's reserve tax number: dated registration by periods is kept by Tax · TaxRegistration (PartyType=LegalEntity), but while no active TaxRegistration row is found for the document date, a ZATCA invoice (21-SaudiArabia) and the Ukrainian export (22-Ukraine) read this field directly as the seller's TIN/VAT number.

LegalEntityEventHandler event:

  • OnBeforeCreateAsync: if Currency is empty, fills it from IRecordDefaults.SeedAsync("LegalEntity") (the tenant default from CommonSettings/AccountingSettings, see Common).
  • OnBeforeSaveAsync: rejects the record if Country or Currency is empty (Страна и валюта обязательны).

The default currency is not taken from the country. An earlier version of this document described filling Currency from CountryCurrency.IsPrimary of the selected Country — the code does not do that. The default is tenant-wide (one for the whole system, from CommonSettings.DefaultCurrency) and is filled only once, if the field is empty when the record is created; changing Country on an already filled record does not recalculate the currency.

Tax package installation does not live in this handler. An earlier version of the document showed OnAfterSaveAsync reading Country["TaxLocalizationPack"] and calling ITaxPackInstaller — Country has no such field, and there is no such code in LegalEntityEventHandler. The reason is architectural: Tax depends on Organization, so Organization cannot call Tax; that edge would close a cycle. Installation is done by the CoC extension LegalEntityTaxPackEventHandler (Inventory, layer 2, [ExtensionOf("LegalEntity")]) on OnAfterInsertAsync/OnAfterUpdateAsync: it takes Country.CodeISO2 and calls ITaxPackInstaller.InstallForCountryAsync. A code comment says explicitly that a package installation failure is not a reason to reject saving the legal entity. TenantSetup.ApplyOrgAsync repeats the same call during initial tenant setup (see the callout below). The TaxPackInstaller itself does not store a package in a per-country field — it scans DataPackages/index.json of all models and applies packages whose when:"country" matches the ISO2 code (for example, Common/cities-SA is installed by the same mechanism even though it is not tax-related; the interface name is historical and wider than taxes).

Extensions of LegalEntity by other models (additive fields, layer Solution): 21-SaudiArabia adds CommercialRegistration (CRN, separate from VAT); 22-Ukraine adds UaTaxRegime, DpsRegionCode, DpsOfficeCode, IsLegalPerson (optional Boolean — empty means an individual entrepreneur, not "legal person by default") and attaches UaLegalEntityRegimeEventHandler (CoC), which synchronises TaxMapping when the regime changes.

Referenced by: Division.LegalEntity, Accounting.JournalEntry (+ dimension GL-LegalEntity), Tax.TaxRegistration, TaxAuthorityConnection, TaxPeriod, TaxSubmission, TaxCalculation, TaxDocument, TaxPayment, TaxReturn (+ dimension TaxLedger-LegalEntity), Sales.SalesContract, CustomerPayment, SalesCreditNote, SalesDebitNote, SalesRealization, Purchasing.PurchaseOrder, PurchaseCreditNote, VendorPayment.

Division#

Hierarchical (isHierarchical: true → the server adds system ParentId of type DivisionParentRef), isLogged: false — unlike LegalEntity, changes are not audited. ID, Name (String 128, required), LegalEntity (RefLegalEntity, required), DivisionType (RefDivisionType, required).

DivisionEventHandler event:

  • OnBeforeCreateAsync: if LegalEntity is empty, fills it from IRecordDefaults.SeedAsync("Division"), ultimately from OrganizationSettings.DefaultLegalEntity (see below) — the field remains required, but the user gets a starting point.
  • OnBeforeSaveAsync: rejects a record with empty LegalEntity (Подразделение должно принадлежать юрлицу).

Referenced by: Inventory.Store.Division, HR.Employee.Division, PayrollAccrual, SocialInsuranceAccrual, SocialInsurancePayment, TimeSheet (all carry Division), registers Payroll (dimension Payroll-Division) and SocialInsurance.

DivisionType#

isCached: true. ID, Code (String 16, required, unique), Name (String 128, required, translatable). Classifier of the site role — it determines which operations are available to a node (sales only from a sales point, a store holds balances, and so on), but the consuming model (Inventory.Store) places the role check, not DivisionType itself.

The base data package seeds 4 rows: OFFICE/Office, PLANT/Plant, WAREHOUSE/Warehouse, SALESPOINT/Sales point.

The list is not closed by the four seeded rows. TenantSetup.ApplyOrgAsync (Inventory, see the callout below) finds or creates a fifth row Code="MAIN" (Headquarters) during initial tenant setup — for the head division of a new company.

OrganizationSettings#

Singleton (isSingleton, isCached) — one record per tenant. ID, ApplicationName (String 128, optional — tenant application name), Logo (Image, optional), DefaultLegalEntity (RefLegalEntity, optional), DefaultLegalEntityCode (String 128, hidden on the form — denormalised ID shadow of DefaultLegalEntity).

The code and reference are synchronised with the same one-way pattern as CommonSettings.DefaultCurrency/DefaultCurrencyCode (Common): saving a non-empty reference stamps the code; loading with an empty reference but a filled code resolves the reference back by searching by ID. An empty reference on save does not erase the code.

This is the centre of gravity of the whole document. DefaultLegalEntity is not a local module setting. It is the tenant default that Common.RecordDefaults.SeedAsync (see Common · Services) places into the LegalEntity field when a new record is created everywhere that such a field exists and is empty: Division (this model), Sales.SalesContract, CustomerPayment, SalesCreditNote, SalesDebitNote, SalesRealization, Purchasing.PurchaseOrder, PurchaseCreditNote, VendorPayment, Accounting.JournalEntry, Tax.TaxAuthorityConnection, TaxPeriod, TaxSubmission, TaxCalculation, TaxDocument, TaxPayment, TaxReturn. None of these fields becomes a reference to OrganizationSettings in metadata — the connection is entirely runtime-based through the service, not through edtMetaId.

Business logic and invariants#

The functional currency is fixed when the legal entity is created. The whole general ledger and reporting are kept in it; operations in another currency are revalued in Accounting by a separate exchange-rate-difference posting entry. The code does not separately check currency changes on an active legal entity, but replaying posting history is not allowed by convention — create a new legal entity for a change.

The country defines the tax jurisdiction, but does not store the package in a field. Installation happens by calling ITaxPackInstaller.InstallForCountryAsync from Inventory (CoC extension of LegalEntity + TenantSetup.ApplyOrgAsync) — see LegalEntity above for the details and the architectural separation reason.

RegistrationNumber is the legal entity's business identity, unique, and a duplicate is rejected on save.

Every site belongs to exactly one legal entity. A store (Inventory) and an employee (HR) inherit the legal entity transitively through their required Division — neither Store nor Employee has a separate LegalEntity field. This gives one common "legal entity/division" breakdown in all registers (GL, TaxLedger, Payroll).

DefaultLegalEntityCode survives loss of the reference. Once stamped, the code remains even if the user clears DefaultLegalEntity; on the next load the reference is resolved back by code. Export/import with changed MetaIds does not break the link for exactly this reason.

ApplyOrgAsync — registering a company and opening a branch in one call#

Organization has no separate "company registration" document — the scenario is fully executed by ITenantSetup.ApplyOrgAsync(legalEntityName, registrationNumber, countryIso2, currencyCode) from Inventory, using existing Country/Currency. It is idempotent: a repeated call with the same RegistrationNumber does not create anything again.

Call LegalEntity Division (type MAIN) Store CommonSettings.TradeProfile Tax package
before scenario 0 0 0 not set —
1st ApplyOrgAsync("ООО Ромашка", "REG-1", "SA", "SAR") created created (type MAIN, auto-created if absent) created (simple, +StoreZone "Main") BuySell (does not downgrade existing Full) VAT-SA installed
2nd ApplyOrgAsync with the same REG-1 not created again (found by RegistrationNumber) not created again not created again unchanged not reinstalled

EnsureBuySellAsync does not downgrade a tenant that has already selected Full — an explicit test comment says: "restarting organization setup must not silently turn off production" (Common · TradeProfileService).

Analytics#

LegalEntity (RefLegalEntity) and Division (RefDivision) are dimensions of registers in other models. They are not interchangeable: GL (Accounting) and TaxLedger (Tax) are sliced by the LegalEntity analytic, while Payroll (HR) is sliced by Division, not LegalEntity.

The Organization group → Dictionaries (LegalEntity, Division, DivisionType) · Settings (OrganizationSettings).

  • Outgoing: Common — the only dependency (Country, Currency, Address); at runtime (not through model.json) — CommonSettings/AccountingSettings for the default currency through RecordDefaults.
  • Incoming: almost the whole workspace. LegalEntity and OrganizationSettings.DefaultLegalEntity — see the lists in LegalEntity and OrganizationSettings above. Division — Inventory (Store), HR (Employee and accrual documents). Inventory performs tax package installation by the legal entity country (CoC extension + TenantSetup), not Organization itself — see the explanation in LegalEntity.

Objects#

Generated from workspace metadata by node tools/gen-reference.mjs; edit the metadata, not this block.

Dictionaries#

Division — Divisions

Organizational units / sites hierarchy; each belongs to exactly one legal entity

Field Caption Type Required
DivisionType Division type → DivisionType Yes
ID Code String(50) Yes
LegalEntity Legal entity → LegalEntity Yes
Name Name String(128) Yes
ParentId Parent → Division No

DivisionType — Division types

Classifier of division roles: Office / Plant / Warehouse / SalesPoint

Field Caption Type Required
Code Code String(16) Yes
ID Code String(50) Yes
Name Name String(128) Yes

Taxable and reporting unit; owns operations, has country and functional currency

Field Caption Type Required
Country Country → Country Yes
Currency Currency → Currency Yes
ID Code String(50) Yes
LegalAddress Legal address → Address No
Name Name String(160) Yes
RegistrationNumber Reg. number String(32) Yes
TaxRegistrationNumber Tax registration number String(64) No

OrganizationSettings — Organization settings

Field Caption Type Required
ApplicationName Application name String(128) No
DefaultLegalEntity Default legal entity → LegalEntity No
DefaultLegalEntityCode Default legal entity code String(128) No
ID ID String(50) Yes
Logo Application logo Image No
Was this page helpful?