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).
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: ifCurrencyis empty, fills it fromIRecordDefaults.SeedAsync("LegalEntity")(the tenant default fromCommonSettings/AccountingSettings, see Common).OnBeforeSaveAsync: rejects the record ifCountryorCurrencyis empty (Страна и валюта обязательны).
The default currency is not taken from the country. An earlier version of this document described filling
CurrencyfromCountryCurrency.IsPrimaryof the selectedCountry— the code does not do that. The default is tenant-wide (one for the whole system, fromCommonSettings.DefaultCurrency) and is filled only once, if the field is empty when the record is created; changingCountryon 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
OnAfterSaveAsyncreadingCountry["TaxLocalizationPack"]and callingITaxPackInstaller—Countryhas no such field, and there is no such code inLegalEntityEventHandler. The reason is architectural:Taxdepends onOrganization, soOrganizationcannot callTax; that edge would close a cycle. Installation is done by the CoC extensionLegalEntityTaxPackEventHandler(Inventory, layer 2,[ExtensionOf("LegalEntity")]) onOnAfterInsertAsync/OnAfterUpdateAsync: it takesCountry.CodeISO2and callsITaxPackInstaller.InstallForCountryAsync. A code comment says explicitly that a package installation failure is not a reason to reject saving the legal entity.TenantSetup.ApplyOrgAsyncrepeats the same call during initial tenant setup (see the callout below). TheTaxPackInstalleritself does not store a package in a per-country field — it scansDataPackages/index.jsonof all models and applies packages whosewhen:"country"matches the ISO2 code (for example,Common/cities-SAis 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: ifLegalEntityis empty, fills it fromIRecordDefaults.SeedAsync("Division"), ultimately fromOrganizationSettings.DefaultLegalEntity(see below) — the field remains required, but the user gets a starting point.OnBeforeSaveAsync: rejects a record with emptyLegalEntity(Подразделение должно принадлежать юрлицу).
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 rowCode="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.
Menu#
The Organization group → Dictionaries (LegalEntity, Division, DivisionType) ·
Settings (OrganizationSettings).
Links with other modules#
- Outgoing: Common — the only dependency (
Country,Currency,Address); at runtime (not throughmodel.json) —CommonSettings/AccountingSettingsfor the default currency throughRecordDefaults. - Incoming: almost the whole workspace.
LegalEntityandOrganizationSettings.DefaultLegalEntity— see the lists in LegalEntity and OrganizationSettings above.Division— Inventory (Store), HR (Employeeand 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 |
LegalEntity — Legal entities
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 |