ZuloOne
English
Sign in
Menu

References in module settings

Updated

← Developer wiki

On a settings card, the reference is selected from a dictionary. The code string remains the column that the calculation already reads, and it is stamped on save if the reference is filled. An empty reference does not erase the string: on UPDATE the dictionary hydrates only the touched fields, and an empty Guid means “the field did not arrive”.

On load, if the reference is empty but the code is written, the reference is substituted. This is how the old row UAH opens with the currency card.

Singleton On the form Stamp Who reads the stamp
CommonSettings DefaultCurrency DefaultCurrencyCode TenantSetup.StatusAsync
AccountingSettings DefaultCurrency DefaultCurrencyCode accounting card
OrganizationSettings DefaultLegalEntity DefaultLegalEntityCode = LegalEntity.ID new record with a legal-entity field
ProductionSettings DefaultOutputLocation DefaultOutputLocationCode = StoreCell.ID new production order card: an empty output cell copies the reference (code is the fallback key if the reference is empty)
TaxSettings DefaultTax DefaultTaxCode ITaxService.ResolveDefaultTaxCodeAsync
PurchasingSettings, SalesSettings DefaultPaymentTerm DefaultPaymentTermDays payment due date for an order and invoice while no own term is selected
LocalizationUkraineSettings ExemptVat, SingleTax*, IncomeTax, MilitaryLevy paired *Code VAT (ПДВ), single tax (ЄП), personal income tax (ПДФО), military levy (ВЗ)

The costing method is not a dictionary. The calculation knows two values; a third word in the string would be read as FIFO. On the form this is CostingSettings.ValuationMethod (CostingMethodKind: 0 not set, 1 FIFO, 2 average). Saving writes CostingMethod = FIFO or AVG. Zero does not erase the string. The CostingValuation driver still compares the string with AVG; empty and FIFO mean batches by age.

Accounting accounts use the same technique, separately: account profile.

Codes that have no dictionary row remain text: tax authority form number (TaxInvoiceFormCode, J1201010), declaration type, income code for 4DF, VAT number, ISO2 on TaxAuthority.CountryCode and TaxJurisdiction.CountryCode (two letters go into the submission file).

A new card opens already with these values. Record creation calls the value service: empty currency, legal entity, output cell, and payment due date are taken from the singleton of their own module. Purchasing reads the term from purchasing settings; sale, customer, and contract read it from sales settings. The customer’s or contract’s term overrides the module term on save if the user did not change it manually.

Start dates (EffectiveFrom, DateFrom, HireDate, CountDate, DeliveryDate, TaxPointDate, PeriodFrom) on a new record are today. Period end, actual date, arrival and departure marks, and fiscal-year start remain empty. A timesheet sets the first and last day of the month itself.

The module may be absent#

Settings are read by table name across six models, and each tenant has its own model composition. No module means no settings table for it, and a query to it fails (42P01 on Postgres, “Invalid object name” on SQL Server).

Therefore RecordDefaults first asks metadata (IMetadataService.GetAllDictionariesAsync()) and only then reads the table. If the object is absent from metadata, the tenant does not have the module, and that is a normal answer: there will simply be no default, and the caller already treats an empty reference as “nothing to substitute”. An object that IS present in metadata must be readable: if there is no table for it, the stand is broken and must fail loudly.

Why this matters more than it sounds: one service reads all of them on OnBeforeCreate, so an exception from one foreign module would drop POST /api/data/<X>/new entirely — the card would not open for ANY dictionary. A stand without Production is the usual way to hit it.

try/catch does not work here, and this is not a style question. In Postgres, a failed statement dooms the WHOLE transaction: every next command in it returns 25P02: current transaction is aborted until the block ends. Catching does not reset the transaction — it only moves the cause away from the failure point. Swallow the missing-table error and the clear “no ProductionSettings table” becomes 25P02 on an order subtype change three calls later, where people investigate postings. The same mechanism is recorded in Sales/Tests/SalesOutputTaxTest.cs.

One row is an engine rule, not a drawing instruction#

isSingleton decides what the UI opens: a record card instead of a list. That is not enough: nothing in the UI stops a script, a test, or a POST /api/data/<X> from inserting a second row, and the dictionary has no database constraint behind the flag.

ITaxService.ResolveDefaultTaxCodeAsync reads with FirstOrDefault() under a comment asserting “there is exactly one row”. A second row would make an arbitrary row the stand-wide default tax. Every document with no tax contour of its own would silently pick that code up — including models that never read tax settings, and including tests that assert zero tax because no contour is configured.

The engine therefore refuses the second row itself: a save that would create one is cancelled before any model handler runs. Two consequences for business-layer code:

  • Do not re-implement the check in a handler. A duplicate guard in <X>EventHandler.OnBeforeSaveAsync is unreachable — next(...) cancels first — and will quietly rot.
  • A reader still needs a deterministic order. Stands provisioned before the engine check already hold extra rows, and an upgrade does not collapse them. ResolveDefaultTaxCodeAsync orders by CreatedDateTime, then MetaId, so the oldest row wins — the one the settings form opens. Without the ORDER BY, whichever row the database returned first would win, which is to say a random one.

Count rows with SQL, not with the dictionary manager, when the answer has to be authoritative: these dictionaries are isCached, and the cache survives a test case’s rollback. A read-through can honestly return zero where the table holds a row — which is one way the extra rows got written in the first place.

Tests: CommonSettingsCurrencyTest, TaxSettingsRefTest, TaxSettingsSingletonTest, ProductionOutputCellTest.

Where next#

Was this page helpful?