03 · Tax — universal tax engine
Updated
Layer Base(1). Depends on Common (minVersion 1.10.0),
Organization, and Accounting. This
is the universal MECHANISM for tax determination and calculation; tax LAW (rates,
codes, rules, reporting codes) is data in country packages
(localizations), not core logic.
Purpose#
Determines the applicable tax for an operation, calculates the base and amount
(several taxes per document, input/output, recoverability), keeps the tax ledger,
assembles returns, and creates tax documents for electronic exchange with a tax
authority. The engine knows no country. All specifics — rates, codes, rules,
reporting codes, submission channel — come from the Saudi Arabia
/ Ukraine package. Determination and calculation are always at
document/direction level, not per line: TaxService accepts one aggregated
base (taxBase) per call, not a line collection.
What is created (object map)#
| Kind | Name | Purpose |
|---|---|---|
| Model | Tax |
module model, layer Base(1) |
| Dictionary (master) | Tax, TaxAuthority, TaxJurisdiction, TaxCategory, TaxCode, TaxRate, TaxExemptionReason |
tax, authority, jurisdiction, rate and its category |
| Dictionary (determination) | TaxRegistration, TaxProfile, TaxProfileAttribute, TaxMapping, TaxRule, TaxRuleCondition, TaxRuleAction |
who/what is taxable and by which rule |
| Dictionary (reporting/integration) | TaxPeriod, TaxReportMapping, TaxAuthorityConnection, TaxSubmission |
period, return boxes, authority connection, submission log |
| Dictionary (classifier) | TaxDirection |
INPUT/OUTPUT/WITHHOLDING/OTHER — tax-ledger dimension |
| Dictionary (settings) | TaxSettings |
singleton — default tax/code |
| TablePart | TaxCalculationLines, TaxPaymentLines, TaxReturnLines |
document lines |
| Document | TaxCalculation, TaxPayment, TaxReturn, TaxDocument |
calculation, payment, return, e-invoice envelope |
| Register | TaxLedger |
tax ledger (source of returns) |
| Analytic (owned here) | TaxCode, TaxDirection |
TaxLedger dimensions |
| — reused — | analytic LegalEntity (Organization) |
TaxLedger dimension, not owned here |
| Service | TaxService, TaxReturnService, TaxAuthoritySubmitService, TaxPackInstaller, TaxDocumentOutcome, EInvoiceRelease |
determination/calculation, return, mock submission, package installation, outcome stamping, release extension point |
| Command | BuildTaxReturn, FileTaxReturn, FinalizeTaxCalculation, PayTax |
document transitions |
| EDT | TaxRateValue (scalar) + Ref EDT for every dictionary, TaxRuleOperatorEdt |
types |
| Enum | TaxRuleOperator (11 values), TaxConnectionEnvironment, TaxConnectionStatus |
closed sets |
| Menu | menu.json |
the Tax tab |
Total: 1 model, 20 dictionaries, 3 table parts, 4 documents, 1 register, 2 analytics, 6 services, 4 commands, 3 enums, and 2 data packages (universal only, with no country-specific rates).
There are no LinkTables, no
TaxType, noTaxPackInstallation, and no separate "rule engine" service in the model. There are no link tables for lineage/adjustments, noTaxTypedictionary, noTaxPackInstallationdictionary, and no servicesITaxRuleEngine,ITaxCalculationService,ITaxLedgerService,ITaxReportingService, orITaxAuthorityIntegration. TheTaxCalculation↔source-document lineage is ordinary fieldsSourceDocumentType/SourceDocumentIdon TaxDocument plus the platform document-link graph (see TaxCalculation). Determination and calculation live entirely inTaxService(see Services). Installed packages are not tracked in a separate table by decision, not by omission: the platform already keeps that record —IDataPackageServicereportspack.Applied, andITaxPackInstaller.InstallForCountryAsyncskips an already-applied pack on exactly that flag. A business-layer table would be a second source of truth for "is the Saudi pack installed". Versions are not tracked by either.
model.json#
Layer Base(1), version 1.24.0. Dependencies: Common (minVersion 1.10.0),
Organization (minVersion 1.0.0), Accounting (minVersion 1.0.0). Tax does
not depend on Sales, Purchasing, or Inventory — the dependency goes strictly in
the opposite direction (see Links with other modules).
Number sequences#
A separate sequence for every document: TaxCalculationSeq, TaxPaymentSeq,
TaxReturnSeq, TaxDocumentSeq, TaxSubmissionSeq.
EDT#
Scalar: TaxRateValue Decimal(9,6) — owned here. Money comes from
Accounting. Reference: one Ref EDT for every dictionary
in the model. Enumerable: TaxRuleOperatorEdt (a wrapper over
TaxRuleOperator).
Dictionaries#
Classifier TaxDirection is a small Code/Name dictionary used as an analytic
of TaxLedger. Recoverability is not a separate dictionary: the
amount recoverable is the RecoverableAmount resource on the
TaxCalculation line and on the register movement itself.
TaxAuthority#
Code, Name, CountryCode, IsActive. The handler is a template; there is
no custom validation. Referenced by: TaxJurisdiction, Tax.
TaxJurisdiction#
Code, Name, CountryCode, Level (Int — hierarchy depth). Template handler.
Referenced by: Tax, optionally TaxRule (rule scope narrowing).
Tax#
Code, Name, Authority→TaxAuthority (required),
Jurisdiction→TaxJurisdiction (required), EffectiveFrom
(required)/EffectiveTo. Template handler. Referenced by:
TaxCategory, TaxCode, TaxRate,
TaxPeriod, TaxRuleAction.
TaxCategory#
Tax→(required), Code, Treatment (String: STANDARD/ZERO_RATED/
EXEMPT/OUT_OF_SCOPE/REVERSE_CHARGE). Template handler — the Treatment
string is read by TaxService.IsZeroTreatmentAsync (the zero-rate shortcut),
by TaxService.IsSelfAssessedAsync (the reverse-charge mirror) and by
TaxCalculationGLTx (which pair of accounts the leg uses).
Referenced by: TaxCode, TaxCalculationLines.TaxCategory.
TaxCode#
Code, Name, Tax→(required), TaxCategory→(required — part of the rate
resolution key, not decoration), TaxRate→ (historical/compatibility, not read
by live calculation), EffectiveFrom/EffectiveTo. Template handler.
Referenced by: TaxRule/TaxRuleAction/TaxMapping,
TaxCalculationLines.TaxCode, analytic TaxCode on TaxLedger.
TaxRate#
Tax→(required), Code, Rate (TaxRateValue), EffectiveFrom (required)/
EffectiveTo. Real validation is below.
TaxRate.OnBeforeSaveAsync/resolution: FindOverlappingRateAsync(taxId, from, to) rejects overlapping windows within one Tax (category is not part of
the overlap key — different categories of one tax share one date scale through
TaxCode, not through a column on TaxRate). Adjacent non-overlapping windows
are allowed. The rate is resolved again for every calculation through
ResolveRateAsync; TaxCode.TaxRate is never read directly.
Rate resolution is the chain TaxCode → (Tax + TaxCategory) → TaxRate.
TaxCategory is part of the lookup key, not an ornament. Treatment ∈ {ZERO_RATED, EXEMPT, OUT_OF_SCOPE} returns 0% without calling TaxRate at all;
REVERSE_CHARGE is intentionally excluded from that list — a rate is still
needed for the buyer's self-assessment.
Referenced by: TaxCode (see TaxCode above). The rate itself is
not read by external objects — only through the code.
TaxExemptionReason#
Tax→(required), Code, Text. Real validation: Code is unique within one
Tax.
TaxRegistration#
PartyType (String — polymorphic, no FK: Tax is a Base-layer model and cannot
reference Customer/Supplier from a higher layer), PartyId (Guid),
Country/Jurisdiction, RegistrationNumber, EffectiveFrom/EffectiveTo.
Validation: FindOverlappingRegistrationAsync (same PartyType+PartyId+
Jurisdiction) — on save and inside ResolveRegistrationNumberAsync,
unconditionally (there is no IsDisabled escape hatch). If there is no row
for the document date, Saudi Arabia/Ukraine
read the fallback LegalEntity.TaxRegistrationNumber.
TaxProfile#
PartyType+PartyId (polymorphic), EffectiveFrom/EffectiveTo, IsDisabled
(Boolean, default false). Validation: FindOverlappingProfileAsync —
skipped completely when IsDisabled=true (allows superseded profiles to
coexist).
TaxProfileAttribute#
TaxProfile→, AttributeCode, AttributeValue. Validation: AttributeCode is
unique within the profile. Read by TaxService as buyer.profile.{code}/
seller.profile.{code}/supplier.profile.{code} — see
How the rate is determined.
TaxMapping#
SourceType+SourceId (polymorphic: Item/ItemGroup/Customer/Supplier/
LegalEntity), TaxCode→, Priority, EffectiveFrom/EffectiveTo,
IsDisabled. Validation: FindOverlappingMappingAsync (same SourceType+
SourceId) — also skipped when IsDisabled=true. ResolveMappingAsync priority
order: Item > ItemGroup > Customer > Supplier > LegalEntity.
Referenced by: Inventory — assigning a tax code to an
item/item group without binding Inventory itself to a country.
TaxRule#
Tax determination rule — data, not code. Default action TaxCode→, Priority
(Int), optional Jurisdiction→/Tax→ (scope narrowing),
EffectiveFrom/EffectiveTo, IsDisabled. Validation on save: none — pure
template (see the callout below).
TaxRuleCondition#
TaxRule→, ConditionGroup (Int — AND inside the group, OR between groups),
Path (dot path, seller.id), Operator→TaxRuleOperator, Value.
Validation: none — pure template; an empty condition set means the rule
always matches.
TaxRuleAction#
TaxRule→, Tax→ (supports several taxes on one transaction), TaxCode→,
RateOverride (Decimal — 0 means "no override"; this field cannot express
exactly 0%, use TaxCategory.Treatment for that). Real validation exists.
TaxRuleOperator (11 values, without And/Or — boolean structure is entirely via
ConditionGroup): Eq, Neq, In, NotIn, Gt, Gte, Lt, Lte,
Between, Exists, NotExists.
Rule match order: Priority ascending → EffectiveFrom descending → number
of conditions descending (the more specific rule wins).
TaxRule/TaxRuleConditioncan be saved as anything — there is no write error. UnlikeTaxRate/TaxPeriod/TaxMapping/TaxProfile/TaxRegistration/TaxExemptionReason/TaxProfileAttribute/TaxRuleAction, whose handlers really reject bad data,TaxRuleandTaxRuleConditionare pure templates: a rule with no conditions silently becomes catch-all, and a typo inPath(seller.Idinstead ofseller.id) saves without any warning and silently never matches on read. This asymmetry is not documented in code as an intentional decision — treat it as an open data-integrity hole.
TaxPeriod#
LegalEntity→(required), Tax→(required), Code (required, not unique —
the natural key is the date range), FromDate/ToDate (ToDate ≥ FromDate). A
window overlap for the same LegalEntity+Tax pair is rejected
unconditionally — unlike TaxMapping/TaxProfile, there is no IsDisabled
escape hatch here.
TaxReportMapping#
Mapping from direction/category to a return box; read by
TaxReturnService.ResolveBoxesAsync (Services).
TaxSettings#
Singleton (queried with "1 = 1", like other workspace singleton dictionaries).
DefaultTax/DefaultTaxCode — the handler synchronises them both ways on save
(changing one recalculates/checks the other).
TaxAuthorityConnection#
Universal authority-connection setup — one record per legal entity × authority.
Fields: see TaxAuthorityConnection in Objects below.
| Field | Business note |
|---|---|
LegalEntity |
Seeded from RecordDefaults on create. |
Environment |
Uses the authority environment scale Sandbox(0), Simulation(1), Production(2). |
Status |
Uses the onboarding scale NotOnboarded(0), Onboarded(1), Active(2). |
CredentialRef |
Stores only the secret reference; the secret itself is not in this record. |
IsMockReject |
Switches the mock submission service to the rejected outcome for tests and demos. |
The handler is a pure template, without any checks. Allowed environments and the specific protocol/endpoint are the country package's responsibility (Saudi Arabia, Ukraine); submission inside Tax is a mock (see TaxAuthoritySubmitService).
TaxSubmission#
Append-only log; only LegalEntity is seeded on create, and there is no save
validation at all. Written by TaxAuthoritySubmitService.
Menu anomaly (
NEEDS_VERIFICATION):TaxSubmissionis a dictionary by kind (no subtypes, no posting), but inmenu.jsonit is attached toDocuments, notDictionaries. Do not change it as an error without confirmation — perhaps this is intentional for visibility next toTaxDocument.
Documents#
TaxCalculation#
Header: LegalEntity, Currency, TaxPointDate (tax-event date — not
necessarily the source document date), DeterminationReason (free text — the
only fallback link to the source document when the document graph has not yet
been written; see the callout below). Subtypes: Draft → Finalized. Table part
TaxCalculationLines: TaxCode, TaxCategory, Direction→TaxDirection,
TaxBase, RateValue, RateOverridden (Boolean), TaxAmount,
RecoverableAmount.
The source-document link is soft, not an FK and not a LinkTable. The main path is the platform document-link graph (
IDocumentManager.AddLinkAsync/GetDocumentFamilyAsync), which the calling handler writes immediately afterCreateCalculationAsync. The fallback path (when the graph is not written) is text parsing ofDeterminationReasonby prefix ("Sales invoice ","Purchase order "), used by Purchasing'sTaxCalculationPayableSeamEventHandler. This is fragile (depends on exact prefix match), but it is the only real mechanism — there is no link table for it.
OnBeforePostAsync is the only cancellable check in the model, per line:
the rate is re-resolved (ResolveRateAsync) — no rate active on TaxPointDate
→ refusal (нет действующей ставки); the rate was not explicitly overridden and
does not match the date-resolved rate → refusal (не действовала на);
|TaxBase × RateValue − TaxAmount| > 0.01 → refusal (не сходится). All three
message substrings are asserted verbatim by integration tests.
Posting (Finalized) writes 1:1 (without aggregation) into
TaxLedger: .An(TaxCode).An(TaxDirection).An(LegalEntity).Res(TaxBase).Res(TaxAmount).Res(RecoverableAmount).
TaxCalculationGLTx writes the VAT leg into GL on the same subtype.
Reverse charge: one calculation, two lines
An INPUT line whose code carries Treatment=REVERSE_CHARGE gets a mirror
OUTPUT line in the same calculation: same code, same base, same rate, same
amount. The supplier — abroad, or domestically relieved — charged no tax; the
buyer declares that tax himself and deducts it in the same breath, so a fully
recovering buyer pays nothing and both halves still appear on the return.
Both halves are written by TaxService.WriteCalculationAsync. They are in one
document on purpose: they are one tax event, they must post and unpost together,
and a credit note reversing the purchase has to find them in one place —
CreateReversalFromCalculationAsync keeps each source line's own direction,
so the deduction and the liability are each undone on their own side.
Consequences you can rely on:
- The return needs no special case.
NetPayable = outputTax − inputTaxalready nets a full recovery to zero, and both halves stay visible per direction (this is what fills box 9 of the Saudi form). - GL posts the pair among the tax accounts — Dr
VatReceivableAccountCode/ CrVatPayableAccountCode, touching neither receivables nor payables. The supplier is not credited with VAT he never charged. - A partly recoverable reverse-charge code is refused. The declared half is
owed in full whatever the deduction is, so the remainder is a real cost — and
the only place that capitalises non-recoverable tax is the purchase receipt,
which under a reverse charge saw no tax to capitalise.
NonRecoverablePct > 0on such a code throws instead of posting the difference somewhere plausible. - Only the buyer's side. On an
OUTPUTline a reverse-charge code means the opposite — the seller shifts the tax to his buyer and owes nothing — and that is not implemented: the engine still computes the seller's tax at the code's rate. There is no caller for it today, and changing it would silently alter the books of anyone who has such a code on a sale. - An
INPUTreverse-charge line with no mirror still posts its deduction:TaxCalculationGLTxfalls back to the ordinary input entry rather than dropping it.
A calculation lives and dies with its source document. The source
(SalesRealization, PurchaseOrder, SalesCreditNote, SalesDebitNote,
PurchaseCreditNote) spawns it after posting and links it as a child. Its
handlers call ITaxCalculationLifecycle and ITaxService:
| Source event | Call | Effect on the calculation |
|---|---|---|
OnBeforeUnpostAsync — a step back, a re-post after an edit, the first step of a delete |
WithdrawCalculationsAsync(sourceId) |
Finalized → Draft: its TaxLedger, GL and Payable-seam movements are reversed with the source's own |
OnAfterPostAsync |
CreateCalculationAsync / CreateReversalAsync |
a Draft calculation with the same DeterminationReason is re-determined in place — same document, fresh lines; a Finalized one is left alone (the after-post event can run twice in one posting) |
OnBeforeDeleteAsync |
DeleteCalculationsOfAsync(sourceId, reason) |
the calculation is deleted |
Example: an invoice 4 × 25 at 15% is issued — the calculation holds tax 15. The
invoice goes back to Draft — the calculation is Draft, TaxLedger and the
VAT leg are 0. The quantity becomes 6 and the invoice is issued again — the
same calculation is Finalized with tax 22.5. A leftover Finalized
calculation at the old tax 15 would block that re-issue from creating anything.
The one hard stop is a filed TaxReturn whose period covers the
calculation's TaxPointDate for the same legal entity: withdrawing would make
the books disagree with what was reported. The source's unpost or delete is then
refused with the period and the return number; the correction is a credit or
debit note.
OnBeforeDeleteAsync finds the calculation by its DeterminationReason, not by
the link: by the time the delete event fires, the platform has already removed
the document's links.
TaxCalculationLifecycle is a separate service with two dependencies on
purpose. It runs inside the source's posting transaction, before the engine sets
its savepoint; resolving the heavy TaxService there broke that transaction
("Cannot issue SAVE TRANSACTION when there is no active transaction").
TaxPayment#
LegalEntity, lines Amount (validation: > 0, at least one line when moving
to Paid). Writes no movement to any Tax register — GetTransactions is
only into the book: TaxPaymentTx writes the payment's own GL leg (Dr VAT
payable / Cr cash, books FIN,TAX) — a separate tax register would duplicate the
ledger and the book with a third contour. The Paid payment is filed once with
the tax-authority mock (TaxAuthoritySubmitService.SubmitPaymentOnceAsync).
TaxReturn#
Built from TaxLedger, not from source documents. Header:
LegalEntity, Tax, period window, subtypes Draft → Filed. Table part
TaxReturnLines is built by TaxReturnService.ResolveBoxesAsync using
TaxReportMapping. Also writes no movements — it is a
pure turnover snapshot at build time, not a posting source.
OnAfterPostAsync when transitioning to Filed calls
ITaxAuthoritySubmitService.SubmitReturnAsync inside a bare try{}catch{} — a
submission failure is fully invisible (no log, no user error). This is justified
by an explicit code comment: a mock failure must not roll back a return filing
that already happened.
TaxDocument#
Envelope for e-invoice/submission to a government system. Header: LegalEntity,
SourceDocumentType/SourceDocumentId (the same polymorphic soft link as in
TaxCalculation), EInvoiceKind
(INVOICE/CREDIT_NOTE/DEBIT_NOTE/ADJUSTMENT), InvoiceType (country
string — for example Standard/Simplified for KSA, an M.E.Doc form code for
Ukraine), Uuid, InvoiceHash, PreviousInvoiceHash, InvoiceCounter,
Payload, ExternalId, Authority, RejectionReason. Subtypes: Draft →
Issued → Cleared/Reported/Rejected (the last three are target readonly
subtypes; editing after them is blocked).
Writes no movements — TaxDocumentTransactions is a fully commented template.
Write order matters.
TaxDocumentOutcome.ApplyAsync(Services) writesAuthority/RejectionReasonthroughUpdateDocumentAsyncbeforeSetSubtypeAsynctransitions to the target readonly subtype — the reverse order would make the readonly-subtype guard silently reject writing the fields.
Example 1 — output VAT on a sales invoice#
Legal entity ACME KSA, customer X, base 100, code resolves to STD
(Treatment=STANDARD), rate on invoice date is 0.15.
| Field | Before (Draft) |
After (Issued + TaxCalculation Finalized) |
|---|---|---|
SalesRealization.TaxRateApplied |
0.00 | 0.15 (stamped by Sales OnBeforePostAsync) |
TaxCalculation |
— | TaxBase=100.00, RateValue=0.15, TaxAmount=15.00 (100×0.15, AwayFromZero), Direction=OUTPUT |
TaxLedger |
— | +TaxBase 100.00 / +TaxAmount 15.00, dimensions TaxCode=STD, Direction=OUTPUT, LegalEntity=ACME KSA |
GL (TaxCalculationGLTx, output > 0) |
— | Dr ArAccountCode 15.00 / Cr VatPayableAccountCode 15.00 |
Example 2 — input VAT on a purchase#
Supplier Y, legal entity ACME KSA, base 1000, rate on receipt date is 0.15,
NonRecoverablePct=0 (default — fully recoverable).
| Field | Before | After (Received + TaxCalculation Finalized) |
|---|---|---|
PurchaseOrder.NonRecoverableVat |
0.00 | 0.00 |
TaxCalculation |
— | TaxBase=1000.00, RateValue=0.15, TaxAmount=150.00, RecoverableAmount=150.00, Direction=INPUT |
TaxLedger |
— | +TaxBase 1000.00 / +TaxAmount 150.00 / +RecoverableAmount 150.00 |
GL (inputRecoverable > 0) |
— | Dr VatReceivableAccountCode 150.00 / Cr PayableAccountCode 150.00 |
Example 2a — a reverse-charge import#
Same legal entity, a service bought from a non-resident supplier, base 500,
code RC-SA (Treatment=REVERSE_CHARGE), rate 0.15, NonRecoverablePct=0.
The supplier's invoice carries no tax — the figures below are entirely the
buyer's own.
| Field | Before | After (Received + TaxCalculation Finalized) |
|---|---|---|
TaxCalculation line 1 |
— | Direction=INPUT, TaxBase=500.00, RateValue=0.15, TaxAmount=75.00, RecoverableAmount=75.00 |
TaxCalculation line 2 (mirror) |
— | Direction=OUTPUT, TaxBase=500.00, RateValue=0.15, TaxAmount=75.00 |
TaxLedger |
— | two movements, one per direction, both TaxCode=RC-SA |
GL (selfAssessed > 0) |
— | Dr VatReceivableAccountCode 75.00 / Cr VatPayableAccountCode 75.00 — no payables leg |
TaxReturn for the period |
— | OutputTax +75.00, InputTax +75.00, NetPayable unchanged |
Change NonRecoverablePct on RC-SA to 50 and the same purchase throws instead
of posting: 37.50 of tax would be owed with nowhere to charge it.
Example 3 — reversal by credit note when the rate changes (open sharp edge)#
Original invoice from 2024-06-01, base 100, rate on that date is 0.15
(TaxAmount=15.00, copied to SalesRealization.TaxRateApplied). The rate
changes to 0.20 from 2025-01-01. A credit note for the same base 100 is issued
and posted on 2025-03-01.
| Field | Original invoice (2024-06-01) | Credit note (posted 2025-03-01) |
|---|---|---|
| Determination context | seller/customer/item — real | same customer ⇒ same tax code (the historical bug with context=null, which gave the wrong code, is fixed in all known call sites) |
Date passed to TaxService |
invoice.DocumentDate = 2024-06-01 |
note.DocumentDate = 2025-03-01, not the original invoice date |
| Resolved rate | 0.15 | 0.20 — active on 2025-03-01, not 0.15 from the original invoice |
TaxCalculation (reversal) |
TaxAmount = +15.00 |
TaxAmount = **−20.00** (100×0.20, minus sign — reversal) |
Net effect in TaxLedger |
+15.00 | −20.00 ⇒ balance −5.00, not zero |
The credit note's tax code is guaranteed to match the original invoice (the
context is now built correctly in all checked call sites — Sales/Purchasing
invoice and credit note). The rate is not guaranteed to match:
CreateReversalAsync always re-resolves the rate on the credit-note date, not
on the original-document date. TaxRateApplied, copied to the credit-note
header, is used only to check that there is something to reverse; it is not
substituted into the reversal amount. The code does not say whether this is an
intentional accounting decision or an open defect; treat it as
NEEDS_VERIFICATION, not ready behaviour.
Registers#
TaxLedger#
registerEngineType: Standard, isDoubleEntry: false, allowNegativeBalance: true, useBalanceTable: true. Resources: TaxBase, TaxAmount,
RecoverableAmount (all Money). Analytics (all isRequired: true):
TaxCode (owned here), TaxDirection (owned here), LegalEntity — not owned
here, declared in Organization · Analytics and
reused here the same way as Accounting · GL.
Analytics are references to sets (AnalyticSetMetaId), not physical columns:
TaxReturnService.CollectAsync reads raw movements, expands them through
AnalyticSetService.ExpandAsync, and groups them in memory — SQL cannot filter
or group by an analytic value directly.
Not every
TaxLedgerline is born from aTaxCalculationdocument. Ukraine's single tax (Єдиний податок) under the simplified system is an exception: asynchronous totals driverUaVatFirstEventTotalDriver(Ukraine) writes directly intoTaxLedgerthrough rawIRegisterMovementService, bypassingTaxCalculationandCreateCalculationAsyncentirely. For most countries (KSA) that is wrong — there everyTaxLedgerline really comes from theFinalizedposting ofTaxCalculation— but as a general model invariant the statement is false.
Services#
| Service | Key methods | Meaning |
|---|---|---|
TaxService |
CalculateTax, ResolveRateAsync, CreateCalculationAsync, CreateReversalAsync, DeterminationContext, NonRecoverableOfAsync |
determination and calculation core — see the callouts below |
TaxReturnService |
BuildAsync, BuildFromPeriodAsync, ResolveBoxesAsync, CollectAsync |
builds a return from the ledger |
TaxCalculationLifecycle |
WithdrawCalculationsAsync, DeleteCalculationsOfAsync |
a source document's calculation follows its unpost and delete; a filed period refuses — see TaxCalculation |
TaxAuthoritySubmitService |
SubmitReturnAsync, SubmitPaymentAsync, SubmitPaymentOnceAsync, SubmitDocumentAsync |
mock submission channel; writes TaxSubmission; outcome is determined by TaxAuthorityConnection.IsMockReject |
TaxPackInstaller |
InstallForCountryAsync(iso2) |
scans IDataPackageService.ListAsync() across all models, not only Tax, and looks for When:"country" by ISO2 |
TaxDocumentOutcome |
ApplyAsync, JournalAsync, ConnectionOfAsync |
single point for stamping Authority/RejectionReason and transitioning TaxDocument subtype by the outcome of any channel (KSA Fatoora, UA M.E.Doc/ЄРПН) |
EInvoiceRelease |
BuyerReleaseBlockAsync |
Tax implementation always returns null (does not block) — Chain of Command extension point for a country |
Exactly 6 services. There is no separate rule-engine interface (ITaxRuleEngine
and others; see the callout in Object map).
Why there is no separate "rule engine": determination and calculation are
fully concentrated in TaxService; ledger movements are written by the
documents themselves on posting; each country implements the tax-authority
integration contract by itself (see Links with other modules).
How the rate is determined#
Context (TaxService.DeterminationContext(...)) is required input. Canonical
keys: seller.id, buyer.id, supplier.id, item.id, item.groupId, plus
dynamic buyer.profile.{code}/seller.profile.{code}/supplier.profile.{code}
from active TaxProfile +
TaxProfileAttribute. The context is an ordinary
Dictionary<string,object?>, not a typed class: the service contract is compiled
into a separate assembly that cannot see document types declared by scripts.
Main sharp edge:
context = nulldoes not throw; it silently skips the whole machinery. If caller code passesnull(or a typo path such asseller.Idinstead ofseller.id),ResolveDeterminationAsyncskips both rules and mappings and quietly falls back toResolveDefaultTaxCodeAsync()— no log, no error. Concrete scenario: a customer is mapped inTaxMappingto code EXEMPT; if the calling handler does not passbuyer.id, the exemption is never checked and the default-code VAT is charged, with no warning anywhere.This historical bug is documented verbatim in a code comment (
SalesCreditNoteEventHandler): this is exactly how the machinery (rules, profiles, mappings) stayed unreachable; no document passed context,nullarrived here, and tests could not catch it because they passed context themselves. Status today: fixed in all four known call sites (sales invoice and credit note, purchase receipt and credit note — all now build real context before callingTaxService). The mechanism is still not structurally fixed: this is not a "context is required" check, but still a quiet default fallthrough; future caller code that forgets to build context will hit the same mine without any platform warning.
The seller's country answers before the stand does
ResolveCountryDefaultTaxCodeAsync(legalEntity, taxPoint) is the step between
TaxMapping and TaxSettings.DefaultTaxCode. It walks LegalEntity →
Country → ISO-2 → TaxJurisdiction → Tax → TaxCategory whose Treatment
is STANDARD → TaxCode, and answers only when exactly one such code also
has a rate in force on the tax point. Anything else returns null and the
stand default applies exactly as before.
The step exists because one TaxSettings row cannot serve a stand that carries
two countries: a Saudi invoice computed with a Ukrainian default code posts,
balances, and reaches the return — where the VAT-SA box layout, which maps
only S/Z/E, comes out empty.
| Deliberate choice | Why |
|---|---|
| The seller is a parameter, not a context key | Both callers take legalEntity as required while the context is optional. Reading seller.id would enable the step only for the callers that already know the most about determination. seller.id stays as a fallback for a caller that passes a context but no entity. |
| Exactly one, never "the first match" | Ukraine's jurisdiction carries VAT, the single tax, personal income tax and the military levy — more than eight standard codes. Which one applies depends on the entity's regime, and that question belongs to TaxMapping. |
| A code without a rate in force is not an answer | The step runs before a path that still works without this answer. Handing back a code the engine cannot price would turn a working calculation into a "no rate in force" refusal. The base vat package is exactly that shape: jurisdiction ZZ, code STD, no rates. |
A disabled TaxMapping still counts |
ResolveMappingAsync filters disabled rows out, so this step asks a different question — HasAnyMappingAsync, enabled or not. A disabled pin is a decision, not a blank. |
This closes the window left by PinSaudiTaxCodes (the job that writes a
TaxMapping for every Saudi legal entity). It is a job because the same work
in a LegalEntity after-insert handler sits inside the entity insert's lock
window. The job stays — the pin is what makes the decision visible as data.
Rate resolution is date-only: EffectiveFrom is required, empty EffectiveTo
means an open window, both boundaries are inclusive, and comparison is by
calendar date (not time). The window is chosen in memory, not through date SQL
literals, for SQL Server/Postgres portability. Rounding is
Math.Round(base × rate, GlobalConstants.Get<int?>("AmountScale") ?? 2, MidpointRounding.AwayFromZero) — the constant and rounding mode are shared by
TaxService and country e-invoice assembly (verified by test: 33.33 × 0.20 = 6.67, not banker's 6.66).
TaxReturnService.BuildCoreAsync sums InputTax by RecoverableAmount, not raw
TaxAmount — the non-recoverable part (TaxCode.NonRecoverablePct, default
0) does not enter the return.
Global constants (GlobalConstants/Tax.json)#
Tax has no global-constants group of its own. The only constant read by the
calculation is AmountScale (cross-model, owned by Common, not
Tax) for tax-amount rounding (see How the rate is determined).
Neither Tax.RoundingMode, nor Tax.DefaultEnvironment, nor
Tax.Submission.MaxRetries exists in code. Connection environment
(Sandbox/Simulation/Production) is an ordinary field
TaxAuthorityConnection.Environment (see TaxAuthorityConnection),
set on each connection record. There is no global constant-level default.
Business logic and invariants#
- Tax determination requires explicit context —
context = nulldoes not throw; it quietly skips rules and mappings. See How the rate is determined. - The rate is re-resolved for every calculation;
TaxCode.TaxRateis historical and is not read by live code. See TaxCode/TaxRate. TaxRule/TaxRuleConditionare not validated on save — asymmetry with other model objects. See the callout in TaxRule.TaxCalculation.OnBeforePostAsyncis the model's only cancellable check. See TaxCalculation.TaxPaymentandTaxReturnwrite no movements — see their sections in Documents.- Not every
TaxLedgerline is born fromTaxCalculation— Ukraine's single tax writes directly. See TaxLedger. - A credit note may fail to bring the balance back to zero if the rate changed between the original document and the reversal. See Example 3.
- Document lineage is a soft link, not an FK and not a LinkTable. See the callout in TaxCalculation.
Menu#
The Tax group → Dictionaries (all 20 objects from the section above) ·
Documents (TaxCalculation, TaxReturn, TaxPayment, TaxDocument,
TaxSubmission — the last one despite being a dictionary by kind; see
TaxSubmission) · Registers (TaxLedger) · Settings
(TaxSettings, TaxAuthorityConnection).
Links with other modules#
-
Outgoing: Common — only a
model.jsondependency, not used directly in calculation; Organization —LegalEntity, analyticLegalEntityon TaxLedger; Accounting —MoneyandIGeneralLedgerService:TaxCalculationGLTxandTaxPaymentTxwrite the calculation's and the payment's ownGLlegs; see Who writes the general ledger. -
Incoming — who calls Tax, and how:
Source Mechanism Sales SalesRealizationOnBeforePostAsync— guard for missing rate on invoice date, stampsTaxRateApplied;OnAfterPostAsync—CreateCalculationAsync(..., "OUTPUT", ..., TaxContextAsync(...))with real contextSales SalesCreditNoteOnAfterPostAsync—CreateReversalAsync(..., "OUTPUT", ..., taxPoint=note.DocumentDate, ctx)— see Example 3Purchasing PurchaseOrder(receipt)same guard + NonRecoverableOfAsync;OnAfterPostAsync—CreateCalculationAsync(..., "INPUT", ...)Purchasing PurchaseCreditNoteCreateReversalAsync(..., "INPUT", ..., taxPoint=note.DocumentDate, ctx)Inventory LegalEntityTaxPackEventHandler(CoC extension ofLegalEntity)ITaxPackInstaller.InstallForCountryAsync(iso)by legal entity country — best-effort, failure does not block saving the legal entity; why this is Inventory, not Tax/Organization, is explained in Organization · LegalEntityPurchasing TaxCalculationPayableSeamEventHandler[ExtensionOf("TaxCalculation")]— adds input VAT of a receipt to the supplier'sPayableSaudi Arabia SaudiVatTxreadsTaxRateApplied, callsTaxService.CalculateTaxinto its separateVatPayableregister (notTaxLedger, not GL — a parallel contour only for reporting); EInvoiceReleaseSa wrapsEInvoiceReleaseby CoC;SaudiEInvoicecallsITaxDocumentOutcome.ApplyAsyncand readsTaxCategory.Treatmentfor the ZATCA letter (S/Z/E/O)Ukraine UaVatTxwrites only the shipment base into itsUaVatFirstEventregister (without code/rate — VAT (ПДВ) recognition by the "first event" is incompatible with the "one date per document" model and is solved outside Tax);UaVatFirstEventTotalDriver— see the TaxLedger callout;UaEInvoicecalls the sameITaxDocumentOutcome.ApplyAsyncBoth localizations follow one rule: only the universal calculation
TaxCalculation/TaxPaymentis posted to GL — country contours (VatPayablein KSA,UaVatPayablein Ukraine) exist for reporting to a specific authority and are never posted to the ledger; otherwise VAT would be doubled.
Objects#
Generated from workspace metadata by node tools/gen-reference.mjs; edit the metadata, not this block.
Dictionaries#
Tax — Taxes
A tax = type + authority + jurisdiction, effective-dated
| Field | Caption | Type | Required |
|---|---|---|---|
Authority |
Authority | → TaxAuthority |
Yes |
Code |
Code | String(32) | Yes |
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ID |
Code | String(50) | Yes |
Jurisdiction |
Jurisdiction | → TaxJurisdiction |
Yes |
Name |
Name | String(128) | Yes |
TaxAuthority — Tax authorities
Government tax authority (ZATCA/HMRC/FTA/…)
| Field | Caption | Type | Required |
|---|---|---|---|
Code |
Code | String(16) | Yes |
CountryCode |
Country code | String(2) | Yes |
ID |
Code | String(50) | Yes |
IsActive |
Active | Boolean | No |
Name |
Name | String(128) | Yes |
TaxAuthorityConnection — Authority connections
| Field | Caption | Type | Required |
|---|---|---|---|
AgentTag |
Agent tag | String(100) | No |
Authority |
Authority | → TaxAuthority |
Yes |
BaseUrl |
Base URL | String(256) | Yes |
Code |
Code | String(128) | Yes |
CredentialRef |
Credential reference | String(128) | No |
Environment |
Environment | TaxConnectionEnvironmentEdt |
Yes |
ID |
Code | String(50) | Yes |
IsMockReject |
Mock reject | Boolean | No |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
MaxRetries |
Max retries | Integer | No |
Status |
Status | TaxConnectionStatusEdt |
No |
TimeoutSeconds |
Timeout, s | Integer | No |
TaxCategory — Tax categories
Treatment: STANDARD/ZERO_RATED/EXEMPT/OUT_OF_SCOPE/REVERSE_CHARGE
| Field | Caption | Type | Required |
|---|---|---|---|
Code |
Code | String(32) | Yes |
ID |
Code | String(50) | Yes |
Name |
Name | String(128) | No |
Tax |
Tax | → Tax |
Yes |
Treatment |
Treatment | String(24) | Yes |
TaxCode — Tax codes
User-facing classification: tax + category + rate, effective-dated
| Field | Caption | Type | Required |
|---|---|---|---|
Code |
Code | String(32) | Yes |
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ExemptionReason |
Exemption reason | → TaxExemptionReason |
No |
ID |
Code | String(50) | Yes |
Name |
Name | String(128) | Yes |
NonRecoverablePct |
Non-recoverable % | Decimal(9,4) | No |
Tax |
Tax | → Tax |
Yes |
TaxCategory |
Tax category | → TaxCategory |
Yes |
TaxRate |
Tax rate | → TaxRate |
No |
TaxDirection — Tax directions
Classifier: INPUT / OUTPUT / WITHHOLDING / OTHER
| Field | Caption | Type | Required |
|---|---|---|---|
Code |
Code | String(16) | Yes |
ID |
Code | String(50) | Yes |
Name |
Name | String(128) | Yes |
TaxExemptionReason — Tax exemption reasons
Why a supply is exempt or out of scope. Country letters (VATEX-SA-*) stay on the localization category extension; this catalog is the core stamp on TaxCalculation.
| Field | Caption | Type | Required |
|---|---|---|---|
Code |
Code | String(32) | Yes |
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ID |
Code | String(50) | Yes |
Name |
Name | String(256) | Yes |
Tax |
Tax | → Tax |
Yes |
TaxJurisdiction — Tax jurisdictions
Hierarchical jurisdiction (country / region / …)
| Field | Caption | Type | Required |
|---|---|---|---|
Code |
Code | String(32) | Yes |
CountryCode |
Country code | String(2) | Yes |
ID |
Code | String(50) | Yes |
Level |
Level | Integer | No |
Name |
Name | String(128) | Yes |
ParentId |
Parent | → TaxJurisdiction |
No |
TaxMapping — Tax mappings
Pins a TaxCode to Item/ItemGroup/Customer/Supplier/LegalEntity when no TaxRule matched. SourceId is a Guid; Tax has no FK to Inventory.
| Field | Caption | Type | Required |
|---|---|---|---|
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ID |
Code | String(50) | Yes |
IsDisabled |
Disabled | Boolean | No |
Priority |
Priority | Integer | Yes |
SourceId |
Source | Guid | Yes |
SourceType |
Source type | String(24) | Yes |
TaxCode |
Tax code | → TaxCode |
Yes |
TaxPeriod — Tax periods
Named return window of a legal entity and tax. Closing it refuses a new TaxReturn for those dates. Posting lock is FiscalPeriod, not this dictionary.
| Field | Caption | Type | Required |
|---|---|---|---|
Code |
Code | String(32) | Yes |
FromDate |
From | DateTime | Yes |
ID |
Code | String(50) | Yes |
IsClosed |
Closed | Boolean | No |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
Name |
Name | String(128) | No |
Tax |
Tax | → Tax |
Yes |
ToDate |
To | DateTime | Yes |
TaxProfile — Tax profiles
Dated party attributes for tax rules. PartyType+PartyId because Tax cannot reference Customer. Empty IsDisabled is live.
| Field | Caption | Type | Required |
|---|---|---|---|
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ID |
Code | String(50) | Yes |
IsDisabled |
Disabled | Boolean | No |
Name |
Name | String(128) | Yes |
PartyId |
Party | Guid | Yes |
PartyType |
Party type | String(16) | Yes |
TaxProfileAttribute — Tax profile attributes
AttributeCode+Value on a TaxProfile. Copied to buyer.profile.{code} (or seller/supplier) before rules run.
| Field | Caption | Type | Required |
|---|---|---|---|
AttributeCode |
Attribute | String(32) | Yes |
ID |
Code | String(50) | Yes |
TaxProfile |
Profile | → TaxProfile |
Yes |
Value |
Value | String(64) | Yes |
TaxRate — Tax rates
Effective-dated rate of a tax within a category (historical rate is immutable). A tax may carry several rates at once — standard, reduced, zero — told apart by TaxCategory; the date then picks the version of each.
| Field | Caption | Type | Required |
|---|---|---|---|
Code |
Code | String(32) | Yes |
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ID |
Code | String(50) | Yes |
Rate |
Rate | TaxRateValue |
Yes |
Tax |
Tax | → Tax |
Yes |
TaxCategory |
Tax category | → TaxCategory |
No |
TaxRegistration — Tax registrations
Dated VAT (or other tax) number of a party. PartyType+PartyId because Tax cannot reference Customer/Supplier. The party-card TaxRegistrationNumber is the print fallback.
| Field | Caption | Type | Required |
|---|---|---|---|
ID |
Code | String(50) | Yes |
Jurisdiction |
Jurisdiction | → TaxJurisdiction |
Yes |
PartyId |
Party | Guid | Yes |
PartyType |
Party type | String(16) | Yes |
RegistrationNumber |
Registration number | String(64) | Yes |
Tax |
Tax | → Tax |
Yes |
ValidFrom |
Valid from | DateTime | Yes |
ValidTo |
Valid to | DateTime | No |
TaxReportMapping — Tax report mappings
Which return box a tax code fills. Boxes are data, not hardcode. Directed mapping beats an undirected one.
| Field | Caption | Type | Required |
|---|---|---|---|
Direction |
Direction | → TaxDirection |
No |
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ID |
Code | String(50) | Yes |
ReturnBox |
Return box | String(32) | Yes |
ReturnType |
Return type | String(32) | Yes |
Tax |
Tax | → Tax |
Yes |
TaxCode |
Tax code | → TaxCode |
Yes |
TaxRule — Tax determination rules
| Field | Caption | Type | Required |
|---|---|---|---|
Code |
Code | String(64) | Yes |
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ID |
Code | String(50) | Yes |
IsDisabled |
Disabled | Boolean | No |
Jurisdiction |
Jurisdiction | → TaxJurisdiction |
No |
Name |
Name | String(128) | Yes |
Priority |
Priority | Integer | Yes |
Tax |
Tax | → Tax |
No |
TaxCode |
Tax code | → TaxCode |
Yes |
TaxRuleAction — Tax rule actions
| Field | Caption | Type | Required |
|---|---|---|---|
DisplayOrder |
Order | Integer | No |
ID |
Code | String(50) | Yes |
RateOverride |
Rate override | TaxRateValue |
No |
TaxCode |
Tax code | → TaxCode |
Yes |
TaxRule |
Tax rule | → TaxRule |
Yes |
TaxRuleCondition — Tax rule conditions
| Field | Caption | Type | Required |
|---|---|---|---|
ConditionGroup |
Condition group | Integer | No |
Field |
Context field | String(64) | Yes |
ID |
Code | String(50) | Yes |
Operator |
Operator | TaxRuleOperatorEdt |
Yes |
TaxRule |
Tax rule | → TaxRule |
Yes |
Value |
Value | String(256) | No |
TaxSettings — Tax settings
| Field | Caption | Type | Required |
|---|---|---|---|
DefaultTax |
Default tax code | → TaxCode |
No |
DefaultTaxCode |
Default tax code | String(128) | No |
ID |
ID | String(50) | Yes |
PricesIncludeTax |
Prices include tax | Boolean | No |
TaxSubmission — Authority submissions
Append-only journal of filing attempts: one row per SubmitReturn/SubmitPayment. The mock writes Accepted or Rejected; a country package keeps writing here.
| Field | Caption | Type | Required |
|---|---|---|---|
Authority |
Authority | → TaxAuthority |
No |
ConnectionCode |
Connection | String(128) | No |
Environment |
Environment | String(16) | No |
ID |
Code | String(50) | Yes |
Kind |
Kind | String(16) | Yes |
LegalEntity |
Legal entity | → LegalEntity |
No |
Receipt |
Receipt | String(128) | Yes |
ResponseMessage |
Response | String(1024) | No |
SourceId |
Source | Guid | Yes |
Status |
Status | String(24) | Yes |
SubmittedAt |
Submitted at | DateTime | Yes |
Documents#
TaxCalculation — Tax calculation
Header fields:
| Field | Caption | Type | Required |
|---|---|---|---|
Currency |
Currency | → Currency |
Yes |
DeterminationReason |
Determination reason | String(1024) | No |
ID |
Number | String(50) | Yes |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
MatchedRule |
Matched rule | → TaxRule |
No |
RegistrationNumber |
Registration number | String(64) | No |
TaxPointDate |
Tax point date | DateTime | Yes |
Subtypes:
| Subtype | Caption |
|---|---|
Draft |
Draft |
Finalized |
Finalized |
Commands:
| Command | Caption | Target subtype |
|---|---|---|
FinalizeTaxCalculation |
Finalize tax calculation | — |
TaxDocument — Tax document
Country-blind e-invoice envelope. The country pack fills it; the host connector later signs and posts. Immutable after Cleared/Reported.
Header fields:
| Field | Caption | Type | Required |
|---|---|---|---|
Authority |
Authority | → TaxAuthority |
No |
EInvoiceKind |
Kind | String(16) | Yes |
ExternalId |
External id | String(128) | No |
ID |
Number | String(50) | Yes |
InvoiceCounter |
Chain counter | Integer | No |
InvoiceHash |
Invoice hash | String(128) | No |
InvoiceType |
Document class | String(32) | No |
LegalEntity |
Legal entity | → LegalEntity |
No |
Payload |
Payload | LargeText | No |
PreviousInvoiceHash |
Previous hash | String(128) | No |
RejectionReason |
Rejection reason | String(1024) | No |
SourceDocumentId |
Source | Guid | Yes |
SourceDocumentType |
Source type | String(64) | Yes |
Uuid |
UUID | Guid | Yes |
Subtypes:
| Subtype | Caption |
|---|---|
Draft |
Draft |
Issued |
Issued |
Cleared |
Cleared |
Reported |
Reported |
Rejected |
Rejected |
Commands:
| Command | Caption | Target subtype |
|---|---|---|
UaResolveTaxInvoice |
Ask the operator about this invoice | — |
TaxPayment — Tax payment
Header fields:
| Field | Caption | Type | Required |
|---|---|---|---|
ID |
Number | String(50) | Yes |
LegalEntity |
Legal entity | → LegalEntity |
No |
Subtypes:
| Subtype | Caption |
|---|---|
Draft |
Draft |
Paid |
Paid |
Commands:
| Command | Caption | Target subtype |
|---|---|---|
PayTax |
Pay tax | — |
TaxReturn — Tax return
Header fields:
| Field | Caption | Type | Required |
|---|---|---|---|
ID |
Number | String(50) | Yes |
InputTax |
Input tax | Money |
No |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
NetPayable |
Net payable | Money |
No |
OutputTax |
Output tax | Money |
No |
PeriodFrom |
Period from | DateTime | Yes |
PeriodTo |
Period to | DateTime | Yes |
TaxPeriod |
Tax period | → TaxPeriod |
No |
Subtypes:
| Subtype | Caption |
|---|---|
Draft |
Draft |
Filed |
Filed |
Commands:
| Command | Caption | Target subtype |
|---|---|---|
FileTaxReturn |
File tax return | — |
UaExportTaxReturn |
Export for filing | — |
SaExportTaxReturn |
Export for filing | — |
UaExportPayrollLevy |
Export payroll levies (4ДФ workpaper) |
— |
UaExportQuarterlyReturn |
Export quarterly payroll (ЄСВ+ПДФО+ВЗ workpaper) |
— |
UaExportDpsPayroll |
Export DPS payroll forms (J0500111) | — |
UaExportSingleTax |
Export single tax (J0103509) | — |
UaExportFopSingleTax |
Export FOP single tax (F0103309) | — |
Registers#
TaxLedger — Tax ledger
Source of tax returns; input/output tax base and amount by code/direction/entity
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
RecoverableAmount |
RecoverableAmount | Money |
— |
TaxAmount |
TaxAmount | Money |
— |
TaxBase |
TaxBase | Money |
— |
Analytics:
| Analytic | Caption | Type | Required |
|---|---|---|---|
TaxLedger-Direction |
TaxLedger-Direction | — | Yes |
TaxLedger-LegalEntity |
TaxLedger-LegalEntity | — | Yes |
TaxLedger-TaxCode |
TaxLedger-TaxCode | — | Yes |
Table parts#
TaxCalculationLines — Lines
Used by: TaxCalculation.
| Field | Caption | Type | Required |
|---|---|---|---|
Direction |
Direction | → TaxDirection |
Yes |
ExemptionReason |
Exemption reason | → TaxExemptionReason |
No |
RateOverridden |
Rate overridden | Boolean | No |
RateValue |
Rate | TaxRateValue |
Yes |
RecoverableAmount |
Recoverable amount | Money |
No |
TaxAmount |
Tax amount | Money |
Yes |
TaxBase |
Tax base | Money |
Yes |
TaxCategory |
Category | → TaxCategory |
No |
TaxCode |
Tax code | → TaxCode |
Yes |
TaxRate |
Rate ref | → TaxRate |
No |
TaxPaymentLines — Lines
Used by: TaxPayment.
| Field | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
Yes |
TaxCode |
Tax code | → TaxCode |
Yes |
TaxReturnLines — Lines
Used by: TaxReturn.
| Field | Caption | Type | Required |
|---|---|---|---|
Direction |
Direction | → TaxDirection |
Yes |
ReturnBox |
Return box | String(32) | No |
TaxAmount |
Tax amount | Money |
No |
TaxBase |
Tax base | Money |
No |
TaxCode |
Tax code | → TaxCode |
Yes |