22 · Ukraine — VAT (ПДВ) by first event, regimes, and the Unified Register of Tax Invoices (ЄРПН)
Updated
Customization(3) layer. Depends on Organization,
HR, Inventory, Purchasing,
Sales, and Tax — six models, the widest coverage
among the country packages.
← General specification · country package template — 20-Localization-Overview
Purpose#
The second country package after Saudi Arabia. It follows the same rule: the core knows nothing about Ukraine, and Ukraine does not rewrite the core. Tax is data — codes, rates, and rules in the Tax model — and the package adds only what the universal engine does not and should not have.
Ukraine has four things that KSA does not have:
- First event (Articles 187.1 and 198.2 of the Tax Code of Ukraine, ПКУ) — the tax liability arises on whichever happens earlier: shipment or payment. The universal engine calculates tax from a document; here tax is calculated from counterparty accumulation.
- Taxation regimes — one database can hold LLCs (ТОВ) on the general system and several individual entrepreneurs (ФОП) on the simplified system, with different rates and different bases.
- Payroll withholdings and three own documents — personal income tax (ПДФО), military levy (військовий збір), remittance of withheld tax to the budget, single social contribution (ЄСВ) for a FOP "for self", and the VAT refund claim (бюджетне відшкодування ПДВ) are handled by package objects, not by the common tax document.
- Unified Register of Tax Invoices (ЄРПН) through an operator — signatures and XML stay outside the platform. The package sends a flat JSON of facts, not a State Tax Service form.
Contour fact. 6 dictionaries, 4 dictionary extensions, 3 own documents (not zero — an important correction to the early understanding of the package), 8 registers, 5 services, 3 jobs, 13 commands, 9 extensions of other documents (one of them,
TaxReturn.LocalizationUkraine, is declared but empty), 4 outbound channels, 3 reporting views, and 17 integration tests.
Process description for analysts — Ukraine; this page is the object-level reference.
What is created (object map)#
| Kind | Name | Purpose |
|---|---|---|
| Model | LocalizationUkraine |
package model, Customization(3) layer |
| Dictionary | LocalizationUkraineSettings |
package settings (singleton) |
| Dictionary | UaSingleTaxLimit |
dated annual single-tax revenue limit (ПКУ 291.4) |
| Dictionary | UaRegimeChange |
audit journal of forced moves to the general system |
| Dictionary | UaBankConnection |
tenant half of the bank channel: address, environment, credential reference |
| Dictionary | UaLandPlot |
land plots for minimum tax liability (мінімальне податкове зобов'язання, МПЗ) |
| Dictionary | UaTaxFilingExport |
prepared return export (CSV/DECLAR XML) |
| Enum | UaTaxRegime |
tax regime of the legal entity |
| EDT | UaTaxRegimeEdt, UaBankRefEdt, UaBankTextEdt, UaBankTranTypeEdt |
regime field type; key, text, and posting type of an Autoclient bank statement |
| DictionaryExtension | LegalEntity.LocalizationUkraine |
regime, DPS region/office codes, legal-person flag |
| DictionaryExtension | Employee.LocalizationUkraine |
12 DPS fields for forms 4ДФ/D1/D5/D6 |
| DictionaryExtension | Position.LocalizationUkraine |
DK 003 profession-classifier code for D5 |
| DictionaryExtension | TimeOff.LocalizationUkraine |
D5 person category and unpaid leave for D1 |
| Document | UaTaxRemittance |
remittance of withheld PIT/military levy to the budget |
| Document | UaFopEsvAccrual |
accrual of ЄСВ for a FOP "for self" |
| Document | UaVatRefund |
VAT refund claim |
| Register | UaVatFirstEvent |
accumulation by contract for the output first event |
| Register | UaVatPayable |
VAT liability |
| Register | UaPurchaseFirstEvent |
supplier accumulation for the input first event |
| Register | UaVatCredit |
tax credit |
| Register | UaPayrollLevy |
PIT and military levy by employee |
| Register | UaBankStatement |
imported Autoclient postings (information register) |
| Register | UaFopEsv |
accrued ЄСВ for a FOP "for self", by legal entity |
| Register | UaVatRefundClaim |
claimed VAT refund amount |
| Service | UaFirstEvent |
first-event determination and accrual |
| Service | UaPayrollLevies |
payroll withholdings |
| Service | UaTaxFiling |
return-export assembly |
| Service | UaEInvoice |
tax-invoice envelope for ЄРПН |
| Service | UaBank |
Autoclient statement import and matching |
| Job | ApplyUaRegimeChanges |
move to the general system after exceeding the limit |
| Job | PullUaTaxInvoiceReceipts |
receive receipts from the ЄРПН operator |
| Job | PullUaBankStatements |
periodic Autoclient statement import |
| Command ×13 | UaExportTaxReturn, UaResolveTaxInvoice, UaExportPayrollLevy, UaExportQuarterlyReturn, UaExportDpsPayroll, UaExportSingleTax, UaExportFopSingleTax, UaPayTaxRemittance, UaVoidTaxRemittance, UaPostFopEsv, UaVoidFopEsv, UaPostVatRefund, UaVoidVatRefund |
declaration/DPS form exports and subtype transitions of the three own documents |
| DataPackage | vat-UA |
codes, rates, categories, and mappings for Ukrainian VAT, single tax, PIT, and military levy |
| — reused — | Tax, TaxCode, TaxRate, TaxReportMapping, TaxDocument, TaxLedger, TaxReturn (Tax); LegalEntity (Organization); PayrollLiability, PayrollAccrual, PayrollPayment (HR) |
not created here |
Total: 6 dictionaries, 8 registers, 3 documents, 5 services, 3 jobs, 13 commands, 1 enum, 4 EDTs, 4 dictionary extensions, 9 extensions of other documents (1 of them empty), 4 outbound channels, 3 reporting views, and 17 integration tests.
model.json#
{
"modelType": "Custom",
"modelVersion": "1.44.0",
"layerId": 3,
"publisher": "ZuloOne",
"name": "LocalizationUkraine"
}
Six dependencies (not an abstract template — the real minVersion values from
model.json):
| Model | minVersion | Why |
|---|---|---|
HR |
≥1.9.2 | Employee/Position/TimeOff extensions, PayrollAccrual/PayrollPayment |
Inventory |
≥1.9.0 | counterparties and item in the first event |
Organization |
≥1.4.1 | LegalEntity — regime, DPS codes, legal-person flag |
Purchasing |
≥1.12.0 | PurchaseOrder/VendorPayment/PurchaseCreditNote — input VAT |
Sales |
≥1.13.1 | SalesRealization/SalesCreditNote/CustomerPayment — output VAT |
Tax |
≥1.11.0 | codes, rates, TaxReportMapping, TaxLedger, TaxReturn |
As with KSA, the core has no backlink to the
package: the connection is extensions and data. There is no Accounting
dependency because the package does not touch the chart of accounts directly.
Reference data (DataPackages/vat-UA.json)#
The vat-UA manifest (DataPackages/index.json: when: "country",
country: "UA", sort: 26 — immediately after Saudi sort: 25) loads tax
legislation as four kinds of objects:
| Object | How many | What is inside |
|---|---|---|
TaxAuthority |
1 | DPS — State Tax Service of Ukraine (Державна податкова служба України) |
TaxJurisdiction |
1 | UA, level 0 |
Tax |
4 | VAT-UA (from 2011-01-01), EP-UA (single tax, from 2011-01-01), PDFO-UA (personal income tax, from 2011-01-01), VZ-UA (military levy, from 2014-08-03 — the actual introduction date of the levy) |
TaxCategory |
11 | STANDARD/REDUCED/ZERO_RATED/EXEMPT on VAT-UA; EP3/EP5/EP15/EP6/EP10 on EP-UA (all Treatment: STANDARD — single tax has no zero/exempt category, only different rates); PDFO, VZ |
TaxRate |
10 | VAT20 (0.20, from 2014-01-01), VAT7 (0.07, from 2014-04-01), EP3/EP5/EP15 (0.03/0.05/0.15), EP6/EP10 ("double" 0.06/0.10), PDFO18 (0.18, from 2016-01-01), VZ1_5 (0.015, 2014-08-03 → 2024-11-30), VZ5 (0.05, from 2024-12-01) |
TaxCode |
16 | see below |
TaxReportMapping |
18 | boxes of three declarations; see below |
Example with numbers: military levy increase. Salary of 30,000 ₴ accrued on 2024-11-30 withholds military levy at 1.5% = 450 ₴. The same salary accrued on 2024-12-01 withholds 5% = 1,500 ₴ — more than three times more one day later. The rate is selected by accrual date, not by payment date (see Salary deductions).
16 tax codes — not only standard/reduced/zero/exempt. In addition to the
obvious UA-S (20%), UA-7 (7%), UA-Z (zero), and UA-E (exempt), the
package has UA-ZX — zero rate for export, separate from ordinary UA-Z:
both map to category ZERO_RATED, but in declaration J0200126 they go to
different lines (2.1 for export, 3 for other zero-rated supplies). Single tax
has not only five "direct" codes (UA-EP3/5/15/6/10), but also double rates
for special circumstances that are usually absent from a short single-tax
description: UA-EPB3/UA-EPB5 for barter settlements (the non-cash form by
itself raises the rate to 6%/10%, regardless of the income limit), and
UA-EPF3/UA-EPF5 for income from activity forbidden for the simplified
system (the same double tariff). UA-PDFO and UA-VZ cover payroll
withholdings.
18 mappings cover THREE different ReturnType values, not one. J0200126
(VAT declaration) has 5 OUTPUT lines (UA-S→1.1, UA-7→1.2, UA-ZX→2.1,
UA-Z→3, UA-E→5) and 2 INPUT lines (UA-S→10.1, UA-7→10.2), all with
EffectiveFrom: 2024-10-01. J0103509 (single-tax declaration for a legal
entity) has 8 OUTPUT lines: UA-EP3→1.3, UA-EP5→1.4, UA-EP6→2.3,
UA-EP10→2.4, UA-EPB3→3.3, UA-EPB5→3.4, UA-EPF3→4.3, UA-EPF5→4.4
(EffectiveFrom: 2025-01-01). F0103309 (FOP declaration, a return code
separate from J0103509) has 3 lines: UA-EP3→05, UA-EP5→06, UA-EP15→07
(EffectiveFrom: 2025-01-01). These three ReturnType values are what
UaExportTaxReturn, UaExportSingleTax, and UaExportFopSingleTax export,
respectively.
What the package does not create. ЄСВ (social insurance) rates are not here;
they are in the common HR contour.
vat-UA carries only VAT/ЄП/ПДФО/ВЗ. The package also does not create data for
military-levy limits or a PIT base ceiling. The ceiling applies to ЄСВ, not to
withholdings (see Salary deductions).
Settings#
LocalizationUkraineSettings is one record per tenant. Fields and the effect of
each field:
| Field | What it sets | If it is empty |
|---|---|---|
SingleTaxCode3 |
single-tax code for 3% (with VAT) | the 3% simplified regime will not accrue single tax |
SingleTaxCode5 |
single-tax code for 5% (without VAT) | the 5% simplified regime will not accrue single tax |
SingleTaxCodeExcess |
15% code for exceeding the limit | the excess will not be taxed |
ExemptVatCode |
VAT exemption code | the regime change cannot set the mapping, and sales will use DefaultTaxCode |
IncomeTaxCode |
PIT code | the withholding will not reach the ledger |
MilitaryLevyCode |
military-levy code | same |
VatReturnType |
VAT declaration type | the export will not be assembled |
TaxInvoiceFormCode |
tax-invoice form code | the envelope leaves without the form code |
TaxInvoiceAdjustmentFormCode |
adjustment-calculation form code | same for an adjustment |
EInvoiceEnabled |
enables sending to ЄРПН | envelopes are assembled but not sent |
The taxpayer identification number is not stored here. It lives in
LegalEntity.TaxRegistrationNumber in Organization,
because it is a legal-entity attribute, not a country setting.
Taxation regimes#
UaTaxRegime is a field on the legal entity through a dictionary extension
(the Enum + EDT pair UaTaxRegimeEdt, because the field cannot reference the
enum directly). It has four values:
| Value | Who it is | VAT | Single tax |
|---|---|---|---|
VatPayer (default) |
VAT payer on the general system | by first event | no |
SimplifiedWithVat |
simplified taxpayer, 3% with VAT | by first event | 3% of income excluding VAT |
SimplifiedNoVat |
simplified taxpayer, 5% without VAT | no | 5% of income |
General |
general system without VAT | no | no |
Default is VatPayer: a tenant that does not know about regimes keeps accounting
as a regular VAT payer.
Example with numbers. A FOP on 3% with VAT received 120 ₴:
- VAT base = 120 / 1.20 = 100, liability 20 ₴;
- single-tax base is also 100, not 120: single tax is charged on income excluding VAT.
The same amount for a FOP on 5% without VAT gives a single-tax base of 120 ₴ and zero VAT.
A regime change is not only a field change. Exceeding the limit from
UaSingleTaxLimit writes a transition to UaRegimeChange, and job
ApplyUaRegimeChanges moves the legal entity to the general system from the
first day of the month after the quarter of the excess (ПКУ 293.8). The excess
itself is taxed at 15% (ПКУ 293.4). At the same time, the regime change sets and
removes TaxMapping for the exemption code; without that, sales after the
transition would use the default code.
Dictionaries#
UaRegimeChange#
Audit trail, not a service table (isLogged: true). The line lives from
excess detection until the transition date arrives, and remains after that.
Fields: see UaRegimeChange in Objects below.
| Field | Business note |
|---|---|
LegalEntity |
Legal entity that exceeded the limit. |
FromRegime / ToRegime |
A 3% simplified taxpayer was a VAT payer and remains one on the general system; a 5% simplified taxpayer was not a VAT payer and does not become one. |
ExceededOn |
Date of the movement that crossed the income limit. |
EffectiveFrom |
First day of the month AFTER the excess quarter (ПКУ 293.8), not the excess date. |
AppliedOn |
Date when the job actually changed the regime. Empty means the transition is still waiting for its date; this makes application idempotent. |
UaSingleTaxLimit#
Dated annual limit (ПКУ 291.4). It is data, not a code constant, because it changes every year with the minimum wage.
Fields: see UaSingleTaxLimit in Objects below.
| Field | Business note |
|---|---|
Code |
Usually the year, for example 2024 or 2025. |
Amount |
Revenue limit. Zero means the limit is not set and excess is not considered at all. |
EffectiveFrom / EffectiveTo |
Validity window. Empty EffectiveTo means open-ended. |
UaBankConnection#
Tenant half of the pb-statements channel: address, environment, account, and a
reference to the credential. The token itself is not written here, because
the dictionary is exported to the workspace and the workspace is in git. This is
not TaxAuthorityConnection: the tax operator and the bank are different
contours with different secrets.
Fields: see UaBankConnection in Objects below.
| Field | Meaning |
|---|---|
Code |
What the channel uses as connectionRef (not ID, which comes from a number sequence and cannot be quoted in the channel). |
LegalEntity, AccountNumber |
Which legal entity and which IBAN/account. |
BaseUrl |
Autoclient API address for this client. |
CredentialRef |
Credential name in Integration:Credentials (for example privatbank-merchant) — the token value lives in host configuration. |
Environment |
Free label (test/prod), not the tax enum Sandbox/Simulation/Production. |
TimeoutSeconds, MaxRetries |
Call parameters. |
AgentTag |
Tag of the agent that dials the address from the client's LAN when the bank is not reachable directly from the host. |
IsDisabled |
Optional Boolean is generated as false; an unchecked record participates. |
UaLandPlot#
Land plots for MPZ (minimum tax liability, мінімальне податкове зобов'язання) =
normative monetary valuation × rate from settings × months in the period / 12.
If the dictionary is empty, declaration appendices J0135709/F0133209 remain
empty.
Fields: see UaLandPlot in Objects below.
| Field | Meaning |
|---|---|
LegalEntity, Name, CadastralNumber |
Whose plot and which plot it is. |
NormativeValue |
Normative monetary valuation. MPZ is calculated as the sum of the legal entity's valuations. |
UaTaxFilingExport#
Prepared return export for submission, created by the UaExport* commands.
Fields: see UaTaxFilingExport in Objects below.
| Field | Meaning |
|---|---|
LegalEntity, ReturnType |
Whose return and which return code (the same code as TaxReportMapping.ReturnType). |
PeriodFrom/PeriodTo, CreatedOn |
Window and assembly time. |
Payload |
CSV content for manually transferring numbers to the cabinet. |
DeclarXml |
Unsigned DECLAR (windows-1251 inside the declaration; the field stores Unicode) under Ministry of Revenue Order No. 729 — for import into M.E.Doc; not a signed cabinet container (DPA Order No. 499 / QES). |
Dictionary extensions#
| Dictionary | Owner model | Adds |
|---|---|---|
LegalEntity.LocalizationUkraine |
Organization | UaTaxRegime (regime); DpsRegionCode/DpsOfficeCode — C_REG/C_STI codes for DPS forms; IsLegalPerson — TOV vs FOP. Unset intentionally means FOP: the optional Boolean is generated non-nullable, and this decides which excess rate applies — 15% (FOP, ПКУ 293.4/F0103309 line 07) or doubled 6%/10% (legal entity, J0103509 line 2). |
Employee.LocalizationUkraine |
HR | 12 DPS fields for forms 4ДФ/D1/D5/D6: DpsIncomeSign (101 salary / 102 civil-law contract), TaxCardNumber (РНОКПП), FireDate/FireReason, DpsTenureGround (special service record D6), DpsInternalPartTime/DpsPartTimeHours (internal part-time vs incomplete hours — DIFFERENT columns, do not confuse them), DpsTransferDate, DpsLaborCategory (1 employees / 2 military personnel / 3 civil-law contract — not derived from DpsIncomeSign automatically except the default), DpsLastName/DpsFirstName/DpsPatronymic/DpsSex — empty if not filled; HR core does not split full names into words. |
Position.LocalizationUkraine |
HR | DkppCode — DK 003 profession-classifier code for the D5 column. |
TimeOff.LocalizationUkraine |
HR | DpsPersonCategory (D5, 4/5/6); DpsUnpaidLeave — calendar days in D1 G13 (not derived from Kind=Absence: absence is not the same as unpaid leave). |
The HR personnel core knows nothing about DPS forms. All 13 fields
above live in this package's extensions, not directly in Employee/Position/
TimeOff.
Documents#
The three own documents are thin and one-directional, with the same skeleton:
OnBeforeCreateAsync: next → IRecordDefaults.SeedAsync → seed
LegalEntity if empty; OnBeforePostAsync: validate only on the subtype being
posted, and re-read the document through IDocumentManager.GetDocumentAsync.
UaTaxRemittance#
Remittance of withheld PIT/military levy to the budget. requiresPosting +
postOnSave, clonable. Header: required LegalEntity, required TaxCode (PIT
or military-levy code). This is the package's only document with a table part:
Lines (Employee × Amount).
Draft → Paid → Voided (both endpoint subtypes are isReadOnly). Transaction
script UaTaxRemittancePaidTx on Paid: for each line with non-empty
Employee and non-zero Amount, writes
RegisterMovementSpec("UaPayrollLevy").Dim("LegalEntity", …).Dim("Employee", …).Dim("TaxCode", …).Res("Transferred", …).
It does not touch TaxLedger: remittance does not change the liability; it only
closes it with money. Voided removes these movements itself (Mix). Manual
compensation is not needed here, unlike payroll payment, where PaidBase is
written by an event handler.
Pre-posting checks: LegalEntity and TaxCode are not empty, at least one line
exists, no amount is negative, and the sum of all lines is greater than zero.
UaFopEsvAccrual#
Accrual of ЄСВ for a FOP "for self". The employment contour does not write here;
that is SocialInsurance in HR. Header: required LegalEntity,
required Amount. No table part.
Draft → Posted → Voided. Transaction script UaFopEsvAccrualPostedTx on
Posted: when LegalEntity is non-empty and Amount != 0, writes
RegisterMovementSpec("UaFopEsv").Dim("LegalEntity", …).Res("Amount", …).
Checks: LegalEntity is not empty, Amount > 0.
UaVatRefund#
VAT refund claim (line 20.2 of declaration J0200126, not more than line 19).
Header: required LegalEntity, required Amount. No table part.
Draft → Posted → Voided. Transaction script UaVatRefundPostedTx on Posted
writes RegisterMovementSpec("UaVatRefundClaim").Dim("LegalEntity", …).Res("Amount", …).
Checks: LegalEntity is not empty, Amount > 0.
Registers#
Register dimensions and resources: see UaVatFirstEvent, UaVatPayable, UaPurchaseFirstEvent, UaVatCredit, UaPayrollLevy, UaBankStatement, UaFopEsv, and UaVatRefundClaim in Objects below.
| Register | Business question |
|---|---|
UaVatFirstEvent |
Has the output first event occurred? |
UaVatPayable |
VAT liability. |
UaPurchaseFirstEvent |
Input first event. |
UaVatCredit |
Input VAT credit. |
UaPayrollLevy |
PIT and military levy by person. |
UaBankStatement |
What came in or went out in the statement. This is an information register, not a payment document: CustomerPayment/VendorPayment are not created from it automatically; without EDRPOU reconciliation, the line may be somebody else's money on somebody else's contract. |
UaFopEsv |
Accrued ЄСВ for a FOP "for self"; read by application F0133109. |
UaVatRefundClaim |
Claimed refund amount read by line 20.2. |
Why the first event is keyed by dimensions, not analytics. The totals driver
calculates the accrual, and the driver sees only register coordinates — analytics
are unavailable to it. A register split by analytics would silently match zero
lines, without an error. Therefore, UaVatFirstEvent carries legal entity,
customer, and contract as dimensions. There are no duplicate analytics for them:
customer and contract are already columns, and outlet moves to UaVatPayable.
Why payroll needed a separate register. TaxLedger (Tax) is
split by direction, legal entity, and tax code. That is enough to pay the
budget, but not enough to show withholding by a specific employee. Form 4ДФ
needs the person, so UaPayrollLevy adds the Employee dimension.
UaBankStatement is the package's only registerEngineType: Information
register. The other seven are registerEngineType: Standard with a balance
table. The statement does not accumulate a balance; it stores posting facts, so
useBalanceTable: false and allowNegativeBalance: true describe an import log,
not a balance.
Salary deductions#
ЄСВ 22% did not require its own code. It is exactly the social-insurance contour in HR, and the package sets it through settings, not logic.
PIT 18% and military levy are withheld under separate codes and written to
UaPayrollLevy and TaxLedger. The military-levy rate is taken on the
accrual date: 1.5% until 2024-12-01, then 5% (see the
example with numbers above). The ЄСВ
base ceiling does not apply to withholdings; they have their own base.
Withholding reduces the debt to the employee through the PayrollLiability
.Withheld resource in HR. This is the contract for any second
withholder: without it, voiding the payroll fund would treat itself as the only
one.
Remittance of the withheld amount to the budget is a separate document,
UaTaxRemittance (above), not a subtype change on the
payroll statement: only that document writes Transferred in UaPayrollLevy.
ЄРПН — electronic tax invoice#
The envelope is the common TaxDocument (Tax), with subtypes
Draft → Issued → Cleared | Rejected; deduplication by source document and the
submission journal already exist in the core. The package adds service
UaEInvoice, two outbound channels (erpn-send and erpn-receipt), and job
PullUaTaxInvoiceReceipts.
Why it is not like KSA. In Saudi Arabia, the host assembles XML and the signature, and Fatoora responds to the same submission. In Ukraine, XML and the qualified electronic signature (КЕП) stay with the operator: the script does not know DSTU 4145, and the key is in the operator's cabinet. Therefore the package sends a flat JSON of facts — contour, number, date, tax number (ІПН), lines — not a DPS form. A made-up form field name would pass our validation and fail in the cabinet when the registration deadline is already urgent.
Successful sending does not close the invoice. The operator has only
accepted it for processing. The "sent, no response" state is resolved by a
query (UaResolveTaxInvoice or the receipt job), not by resubmission: a
repeat submission would create a second document for the same operation.
Channel erpn-gov is direct exchange with the state register for scenarios
where the operator is not involved (status/reference request). erpn-send and
erpn-receipt serve the regular operator route. Those three plus bank channel
pb-statements make the package's four outbound channels.
The Ukraine sales extension folder is
SalesRealization.LocalizationUkraine. Bindings inside point toSalesRealization; the handler is found byobjectNamein the script envelope, not by the folder name.
Extensions of other documents#
| Document | Owner model | Adds |
|---|---|---|
SalesRealization (SalesRealization.LocalizationUkraine folder) |
Sales | UaVatTx — first-event accrual; UaEInvoiceEventHandler — invoice envelope |
SalesCreditNote |
Sales | UaCreditVatTx — base movement; UaEInvoiceCreditNoteEventHandler — adjustment calculation |
CustomerPayment |
Sales | UaPaymentFirstEventTx — payment as a possible first event; UaPaymentEntityGuardEventHandler — guard against a legal entity that conflicts with the line contract |
PurchaseOrder |
Purchasing | UaPurchaseReceiptTx — receipt as the input first event |
VendorPayment |
Purchasing | UaVendorPaymentTx — supplier advance as the first event |
PurchaseCreditNote |
Purchasing | UaPurchaseCreditTx — Received movement and credit removal |
PayrollAccrual |
HR | UaPayrollLevyEventHandler — PIT and military-levy withholdings on accrual |
PayrollPayment |
HR | UaPayrollPaymentLevyEventHandler — the same pattern on the payment side: writes PayrollLiability.Withheld and TaxLedger, without accruing again |
TaxReturn |
Tax | Only .extension.json, no scripts. The extension aggregate is declared and empty, confirmed by a glob over the directory. The extension point is reserved, possibly for future Ukrainian return fields, but currently carries no fields or behavior. |
Direct package writes into other models' registers (PayrollLiability,
TaxLedger from the two HR handlers above, next to package register
UaPayrollLevy) are explained as a pattern in
20-Localization-Overview.
Services#
UaFirstEvent— determines whether the first event (shipment or payment) has occurred by the accumulation inUaVatFirstEvent/UaPurchaseFirstEvent, and calculates the accrual as the increase ofmax(Shipped, Paid), so that an advance and a later shipment for the same amount do not double-tax it. Called from transaction scripts such asUaVatTx,UaPaymentFirstEventTx, andUaPurchaseReceiptTx.UaPayrollLevies— calculates PIT/military levy by the rate effective on the accrual date (see Salary deductions) and writes all three destinations in one call:UaPayrollLevy,PayrollLiability.Withheld(HR), andTaxLedger(Tax).UaTaxFiling— assemblesUaTaxFilingExportby return code: readsTaxLedger/TaxReportMappingfor the period, creates CSV and, where applicable, unsignedDeclarXml. Common entry point for all sevenUaExport*commands.UaEInvoice— builds a flat JSON envelope for a tax invoice or adjustment calculation from the source document and queues channelerpn-send; parses receipts from inbound channelserpn-receipt/erpn-gov.UaBank— parses the Autoclient statement (REF+REFN, TRANTYPE C/D) intoUaBankStatementlines. It does not itself reconcile withCustomerPayment/VendorPaymentby EDRPOU; that is intentionally left to a person (see the register description above).
Outbound channels and jobs#
| Channel | Purpose |
|---|---|
erpn-send |
submits the tax invoice/adjustment calculation to the operator |
erpn-receipt |
receives receipts about the submitted invoice status |
erpn-gov |
direct exchange with the state register, bypassing the operator (status/reference request) |
pb-statements |
Autoclient bank statement import; connectionTable: UaBankConnection, not a tax dictionary |
| Job | Does |
|---|---|
ApplyUaRegimeChanges |
moves the legal entity to the general system from UaRegimeChange.EffectiveFrom, idempotently by AppliedOn |
PullUaTaxInvoiceReceipts |
pulls receipts through erpn-receipt and updates TaxDocument status |
PullUaBankStatements |
periodically pulls the statement through pb-statements and writes UaBankStatement lines |
Menu#
Root group Ukraine (displayOrder 12) with four subgroups:
- Settings —
LocalizationUkraineSettings,UaBankConnection. - Dictionaries —
UaSingleTaxLimit,UaRegimeChange,UaTaxFilingExport,UaLandPlot. - Registers —
UaVatPayable,UaVatFirstEvent,UaVatCredit,UaPurchaseFirstEvent,UaPayrollLevy,UaBankStatement,UaFopEsv,UaVatRefundClaim. - Documents —
UaTaxRemittance,UaFopEsvAccrual,UaVatRefund.
Services, jobs, and reporting views are not shown in the menu because they are not menu-navigable object kinds on this platform.
Reporting views#
Three Custom Reports read package registers:
| View | Source | Breakdown |
|---|---|---|
UaPayrollLevyByEmployeeCode |
UaPayrollLevy |
by employee code |
UaVatCreditBySupplier |
UaVatCredit |
by supplier |
UaVatPayableByCustomerContract |
UaVatPayable |
by customer and contract |
Printed declaration forms#
11 printed forms are physically located in directories of OTHER models. The form belongs to the host document, not to the package that fills it with data:
- 10 forms under
Tax/Documents/TaxReturn/PrintForms/:UaDps4DfZr,UaDpsCalculationZr,UaDpsD1Zr,UaDpsD5Zr,UaDpsD6Zr,UaFopSingleTaxDeclarationZr,UaLevyWorkpaperZr,UaQuarterlyWorkpaperZr,UaSingleTaxDeclarationZr,UaVatDeclarationZr. - 1 form under
Sales/Documents/SalesRealization/PrintForms/:UaFiscalReceiptSample.
Business logic and invariants#
Prepayment is taxed before shipment; shipment does not accrue tax a second
time. Accrual uses the increase of max(Shipped, Paid), not the sum: an
advance of 120 ₴ closes the base, and a later shipment for the same amount adds
nothing.
Partial prepayments together produce one full tax. Two half-events create exactly one full liability, not two and not one and a half.
A credit note moves the base, not only the tax, and does not release the liability until the prepayment is returned.
VAT from different outlets of one customer is not mixed because the contract is part of the register coordinates.
A payment whose legal entity conflicts with the line contract is rejected,
and an empty legal entity is taken from the contract. Otherwise, Shipped and
Paid would diverge by coordinates and tax would be accrued twice.
A non-VAT payer still writes events, but does not move credit: switching between regimes does not lose history.
Remittance and accrual of FOP ЄСВ / VAT refund do not accrue tax twice. All
three own documents write a resource register without a second entry in
TaxLedger: UaTaxRemittance closes a liability that is already accrued as
debt and does not accrue it again; UaFopEsvAccrual/UaVatRefund are
independent facts, not derivatives of sales or purchases.
Voided removes its own movements itself (Mix), except in one place. On
payroll payment, PaidBase is written by an event handler and removal requires
manual compensation. The package's own documents (UaTaxRemittance and others)
do not need that, because only a transaction script writes their movements.
Package tests#
17 integration tests cover: end-to-end VAT flow by first event
(UkraineVatFlowTest), seeded rates (UkraineSeededRatesTest), regimes and
forced change (UkraineTaxRegimeTest, UkraineForcedRegimeChangeTest), input
VAT on purchase (UkrainePurchaseVatTest), declaration exports
(UkraineFilingExportTest, UkraineQuarterlyFilingTest,
UkraineVatDeclarationFilingTest, UkraineFopSingleTaxDeclarationFilingTest,
UkraineSingleTaxDeclarationFilingTest, UkraineDpsPayrollFilingTest,
UkrainePayrollFilingTest), exemption mapping (UkraineExemptMappingTest),
payroll withholdings (UkrainePayrollLevyTest), bank connection and statement
(UkraineBankConnectionTest, UkraineBankStatementTest), and the single-tax
limit (UkraineSingleTaxLimitTest).
What's not there yet#
- No live exchange with M.E.Doc in CI: the MakeDoc/
ToGov/GetDocInfoadapter is in the package, but REST is a service on the accountant's PC. Setup note: M.E.Doc on the client. The exchange itself is not covered by tests. - PrivatBank statement import is implemented, but without a live sandbox.
Channel
pb-statements, dictionaryUaBankConnection, serviceUaBank, jobPullUaBankStatements, and registerUaBankStatementare all available in code and covered by tests (UkraineBankConnectionTest,UkraineBankStatementTest). What is missing: an Autoclient sandbox for checking against a live API and automatic creation ofCustomerPayment/VendorPaymentfrom a statement line. EDRPOU reconciliation is intentionally manual; see the register above. Setup note. - No Diia (Дія). Login is platform identity, not a package feature. Why it is separate.
- No DPS cabinet container (Order No. 499 / QES). Unsigned
DECLARfor the payroll J05 family is stored in fieldDeclarXmlfor import into M.E.Doc. Forms with official identifiers: payrollJ0500111/J0510411/J0510111/J0510511/J0510611(4ДФR01011=income sign 101 / R01012=102, R0107=line 3; D1 G5 citizenship / G6S sex / G8=26 civil-law contract / G111–G113 full name / G12 Sick / G13 unpaid / G14 HireDate/FireDate / G15 maternity leave / G21–G22 part-time and incomplete hours / G24 new workplace from the flag, not HireDate; D5: HireDate/FireDate/DpsTransferDate/ DkppCode; ordinary employers do not file D2/D3), VATJ0200126(form rows, notR00xG3; line 20.2 fromUaVatRefund;UA-ZX→2.1,UA-Z→3), single tax for legal entitiesJ0103509(UA-EP6/UA-EP10line 2,UA-EPB3/UA-EPB5line 3,UA-EPF3/UA-EPF5line 4, MPZ fromUaLandPlot), and FOPF0103309(F0133109fromUaFopEsvAccrual). Accrued amounts come from registers; paid amounts afterPayrollPayment; ЄСВ paid fromSocialInsurancePayment(D1 and R0107 are not reset); remitted from documentUaTaxRemittance;C_REG/C_STIare legal-entity fields in the CSV header and in theDeclarXmlhead. - Workpapers
UA-LEVYandUA-QPRremain for transfer without DPS column codes.
Links with other modules#
- Outgoing: Tax — codes, rates, mappings, ledger, invoice
envelope,
TaxReturndeclaration (empty extension); Sales and Purchasing — documents extended by the package; HR — accrual base, withholding resource, Employee/Position/ TimeOff fields; Organization — legal entity, its regime, and DPS codes; Inventory — counterparties and item in the first event. - Incoming: none. The package is the top layer; nothing references it.
Objects#
Generated from workspace metadata by node tools/gen-reference.mjs; edit the metadata, not this block.
Dictionaries#
LocalizationUkraineSettings — Ukraine settings
| Field | Caption | Type | Required |
|---|---|---|---|
EInvoiceEnabled |
E-invoice (tax invoice) enabled | Boolean | No |
ExemptVat |
Exempt VAT (no-VAT regimes) | → TaxCode |
No |
ExemptVatCode |
Exempt VAT code (no-VAT regimes) | String(64) | No |
ID |
ID | String(50) | Yes |
IncomeTax |
Personal income tax (payroll) | → TaxCode |
No |
IncomeTaxCode |
Personal income tax code (payroll) | String(64) | No |
MilitaryLevy |
Military levy (payroll) | → TaxCode |
No |
MilitaryLevyCode |
Military levy code (payroll) | String(64) | No |
MpzRate |
MPZ rate | Decimal(18,4) | No |
PayrollIncomeSign |
4ДФ income sign (payroll) |
String(8) | No |
SingleTax3 |
Single tax 3% (with VAT) | → TaxCode |
No |
SingleTax5 |
Single tax 5% (without VAT) | → TaxCode |
No |
SingleTaxCode3 |
Single tax code, 3% (with VAT) | String(64) | No |
SingleTaxCode5 |
Single tax code, 5% (without VAT) | String(64) | No |
SingleTaxCodeDouble3 |
Double rate code 6% (legal person, 3%) | String(64) | No |
SingleTaxCodeDouble5 |
Double rate code 10% (legal person, 5%) | String(64) | No |
SingleTaxCodeExcess |
Single tax code, 15% (over the limit) | String(64) | No |
SingleTaxDouble3 |
Double rate 6% (legal person, 3%) | → TaxCode |
No |
SingleTaxDouble5 |
Double rate 10% (legal person, 5%) | → TaxCode |
No |
SingleTaxExcess |
Single tax 15% (over the limit) | → TaxCode |
No |
TaxInvoiceAdjustmentFormCode |
Tax invoice adjustment form code | String(32) | No |
TaxInvoiceFormCode |
Tax invoice form code | String(32) | No |
VatReturnType |
Return type for export | String(32) | No |
UaBankConnection — Bank connections
| Field | Caption | Type | Required |
|---|---|---|---|
AccountNumber |
Account / IBAN | String(34) | No |
AgentTag |
Agent tag | String(100) | No |
BaseUrl |
Base URL | String(256) | Yes |
Code |
Code | String(128) | Yes |
CredentialRef |
Credential reference | String(128) | No |
Environment |
Environment | String(32) | No |
ID |
Code | String(50) | Yes |
IsDisabled |
Disabled | Boolean | No |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
MaxRetries |
Max retries | Integer | No |
Name |
Name | String(256) | Yes |
TimeoutSeconds |
Timeout, s | Integer | No |
UaLandPlot — Land plot (Ukraine MPZ)
| Field | Caption | Type | Required |
|---|---|---|---|
CadastralNumber |
Cadastral number | String(32) | No |
ID |
Code | String(50) | Yes |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
Name |
Name | String(256) | Yes |
NormativeValue |
Normative value | Money |
No |
UaRegimeChange — Forced regime change
| Field | Caption | Type | Required |
|---|---|---|---|
AppliedOn |
Applied on | DateTime | No |
EffectiveFrom |
Effective from | DateTime | Yes |
ExceededOn |
Exceeded on | DateTime | Yes |
FromRegime |
From regime | UaTaxRegimeEdt |
No |
ID |
Code | String(50) | Yes |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
ToRegime |
To regime | UaTaxRegimeEdt |
No |
UaSingleTaxLimit — Single tax revenue limit
| Field | Caption | Type | Required |
|---|---|---|---|
Amount |
Limit | Money |
Yes |
Code |
Code | String(32) | Yes |
EffectiveFrom |
Effective from | DateTime | Yes |
EffectiveTo |
Effective to | DateTime | No |
ID |
Code | String(50) | Yes |
UaTaxFilingExport — Tax filing export
| Field | Caption | Type | Required |
|---|---|---|---|
CreatedOn |
Created on | DateTime | No |
DeclarXml |
DECLAR XML (M.E.Doc import) | LargeText | No |
ID |
ID | String(50) | Yes |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
Payload |
Payload | LargeText | No |
PeriodFrom |
Period from | DateTime | Yes |
PeriodTo |
Period to | DateTime | Yes |
ReturnType |
Return type | String(32) | Yes |
Documents#
UaFopEsvAccrual — FOP ESS for self (Ukraine)
Header fields:
| Field | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
Yes |
ID |
Number | String(50) | Yes |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
Subtypes:
| Subtype | Caption |
|---|---|
Draft |
Draft |
Posted |
Posted |
Voided |
Voided |
Commands:
| Command | Caption | Target subtype |
|---|---|---|
UaPostFopEsv |
Post FOP ESS | — |
UaVoidFopEsv |
Void FOP ESS | — |
UaTaxRemittance — Tax remittance to budget (Ukraine)
Header fields:
| Field | Caption | Type | Required |
|---|---|---|---|
ID |
Number | String(50) | Yes |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
TaxCode |
Tax code | → TaxCode |
Yes |
Subtypes:
| Subtype | Caption |
|---|---|
Draft |
Draft |
Paid |
Paid |
Voided |
Voided |
Commands:
| Command | Caption | Target subtype |
|---|---|---|
UaPayTaxRemittance |
Remit to budget | — |
UaVoidTaxRemittance |
Void remittance | — |
UaVatRefund — VAT refund claim (Ukraine)
Header fields:
| Field | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
Yes |
ID |
Number | String(50) | Yes |
LegalEntity |
Legal entity | → LegalEntity |
Yes |
Subtypes:
| Subtype | Caption |
|---|---|
Draft |
Draft |
Posted |
Posted |
Voided |
Voided |
Commands:
| Command | Caption | Target subtype |
|---|---|---|
UaPostVatRefund |
Post VAT refund claim | — |
UaVoidVatRefund |
Void VAT refund claim | — |
Registers#
UaBankStatement — PrivatBank statement lines
Dimensions:
| Dimension | Caption | Type | Required |
|---|---|---|---|
BankRef |
Bank ref (REF+REFN) | UaBankRefEdt |
— |
LegalEntity |
Legal entity | → LegalEntity |
— |
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
— |
CounterpartyEdrpou |
Counterparty EDRPOU | UaBankTextEdt |
— |
CounterpartyName |
Counterparty name | UaBankTextEdt |
— |
Purpose |
Purpose | UaBankTextEdt |
— |
TranType |
C/D | UaBankTranTypeEdt |
— |
UaFopEsv — FOP ESS for self (Ukraine)
Dimensions:
| Dimension | Caption | Type | Required |
|---|---|---|---|
LegalEntity |
Legal entity | → LegalEntity |
— |
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
— |
UaPayrollLevy — Ukraine payroll levies withheld
Dimensions:
| Dimension | Caption | Type | Required |
|---|---|---|---|
Employee |
Employee | → Employee |
— |
LegalEntity |
Legal entity | → LegalEntity |
— |
TaxCode |
Tax code | → TaxCode |
— |
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
— |
Base |
Base | Money |
— |
PaidBase |
Paid income | Money |
— |
Transferred |
Transferred to budget | Money |
— |
UaPurchaseFirstEvent — Ukraine input VAT first event
Dimensions:
| Dimension | Caption | Type | Required |
|---|---|---|---|
LegalEntity |
Legal entity | → LegalEntity |
— |
Supplier |
Supplier | → Supplier |
— |
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Credited |
Base already credited | Money |
— |
PaidOut |
Paid out | Money |
— |
Received |
Received base | Money |
— |
ReceivedCoded |
Received base with line tax code | Money |
— |
ReceivedCodedVat |
VAT of coded received lines | Money |
— |
UaVatCredit — Ukraine input VAT credit
Dimensions:
| Dimension | Caption | Type | Required |
|---|---|---|---|
LegalEntity |
Legal entity | → LegalEntity |
— |
Supplier |
Supplier | → Supplier |
— |
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
— |
UaVatFirstEvent — Ukraine VAT first event
Dimensions:
| Dimension | Caption | Type | Required |
|---|---|---|---|
Customer |
Customer | → Customer |
— |
LegalEntity |
Legal entity | → LegalEntity |
— |
SalesContract |
Sales contract | → SalesContract |
— |
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Accrued |
Base already taxed | Money |
— |
EpAccrued |
Base already taxed (single tax) | Money |
— |
Paid |
Paid base | Money |
— |
Shipped |
Shipped base | Money |
— |
ShippedCoded |
Shipped base with line tax code | Money |
— |
ShippedCodedVat |
VAT of coded shipped lines | Money |
— |
UaVatPayable — Ukraine VAT payable
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
— |
Analytics:
| Analytic | Caption | Type | Required |
|---|---|---|---|
UaVatPayable-Customer |
UaVatPayable-Customer | — | Yes |
UaVatPayable-CustomerOutlet |
UaVatPayable-CustomerOutlet | — | Yes |
UaVatPayable-SalesContract |
UaVatPayable-SalesContract | — | Yes |
UaVatRefundClaim — VAT refund claim (Ukraine)
Dimensions:
| Dimension | Caption | Type | Required |
|---|---|---|---|
LegalEntity |
Legal entity | → LegalEntity |
— |
Resources:
| Resource | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
— |
Table parts#
UaTaxRemittanceLines — Remittance lines
Used by: UaTaxRemittance.
| Field | Caption | Type | Required |
|---|---|---|---|
Amount |
Amount | Money |
Yes |
Employee |
Employee | → Employee |
Yes |