Регістри в K2 ERP
1. Паспорт документа
| Атрибут | Значення |
|---|---|
| Назва | Технічне завдання на розробку підсистеми «Регістри» платформи K2 ERP |
| Шифр | K2-ERP-TZ-REG-001 |
| Версія | 1.2 (чернетка на узгодження; мова ядра — Python; +Додатки Д, Е) |
| Статус | На розгляді |
| Замовник | [підрозділ-замовник] |
| Виконавець | [команда платформи K2 ERP] |
| Тип документа | Технічне завдання (ГОСТ 34.602 адаптовано) |
| Аналог / прототип | Механізм регістрів платформи «1С:Підприємство 8.3» |
| Мова розробки | Python 3.12+ (див. розділ 4.3) |
| Технологічний стек | SQLAlchemy Core 2.x, Pydantic v2, Alembic, lark, psycopg 3 / pyodbc |
| СКБД | PostgreSQL 14+ (основна), MS SQL Server 2019+ (додаткова) |
1.1. Необхідні розширення MediaWiki
| Розширення | Теги / шаблони | Використано в документі | Постачання |
|---|---|---|---|
| Extension:Diagrams | <graphviz>, <uml> |
4 діаграми GraphViz + 5 діаграм PlantUML | Composer: samwilson/diagrams
|
| Extension:SyntaxHighlight | <syntaxhighlight> |
64 блоки коду (Python, SQL, YAML, lark) | Входить до складу MediaWiki — окремо встановлювати не потрібно |
Template:Note |
{{Note|текст}} |
Примітки та проєктні рішення | Створюється вручну (код нижче) |
Налаштування
// LocalSettings.php — конфігурація ВІКІ, у якій публікується це ТЗ.
// MediaWiki написана на PHP; до мови розробки K2 ERP (Python, розділ 4.3) це не стосується.
wfLoadExtension( 'Diagrams' );
// ОБОВ'ЯЗКОВО. За замовчуванням Diagrams рендерить у PNG; ER-діаграми розділу 9
// містять десятки колонок і в растрі нечитабельні.
$wgDiagramsDefaultFormat = 'svg';
// Варіант А — локальний рендеринг (потрібні бінарники на веб-сервері):
// apt install graphviz plantuml
// Варіант Б — зовнішній сервіс рендерингу (з версії 0.4.0 — опційний):
// $wgDiagramsServiceUrl = 'https://diagrams.example.com/subdir';
Шаблон Template:Note, якщо його немає у вікі:
<div style="border-left:4px solid #36c; background:#eaf3ff; padding:8px 12px; margin:8px 0;">
'''ⓘ''' {{{1}}}
</div>
Вимоги до версії
Кодування: обов'язкове налаштування
Як відрізнити дві різні проблеми:
| Симптом | Діагноз | Що робити |
|---|---|---|
���� — ромби зі знаком питання (U+FFFD). Кількість символів більша за кількість літер оригіналу: 1 кирилична літера → 2 ромби, стрілка → → 3 ромби |
Кодування. Байти UTF-8 декодовані як ASCII/Latin-1 — по одному � на байт |
Розділ нижче, п. «Виправлення» |
□□□□ — порожні прямокутники (tofu). Кількість збігається з кількістю літер |
Шрифти. Кодування правильне, але у шрифті немає гліфів кирилиці | apt install fonts-dejavu fonts-liberation. У документі вже проставлено skinparam defaultFontName "DejaVu Sans" і fontname="DejaVu Sans" для GraphViz
|
пÑивÑÑ — «мохібейк» із латиниці з діакритикою |
Подвійне перекодування UTF-8 ↔ Latin-1 | Перевірити $wgDiagramsServiceUrl — сервіс віддає не UTF-8
|
Діагностика (на сервері вікі):
# 1. Що каже сам PlantUML про своє кодування
java -jar /usr/share/plantuml/plantuml.jar -version | grep -i "Default Encoding"
# Default Encoding: UTF-8 <- норма
# Default Encoding: ANSI_X3.4-1968 <- ЦЕ ПРИЧИНА (це US-ASCII)
# Default Encoding: ISO-8859-1 <- ЦЕ ПРИЧИНА
# 2. Локаль процесу веб-сервера (а не вашого shell — вони різні!)
sudo -u www-data env | grep -E 'LANG|LC_'
# порожньо або LANG=C / LANG=POSIX -> причина
# 3. Відтворення поза MediaWiki — ізолює проблему від вікі
printf '@startuml
A -> B : кирилиця та стрілка â
@enduml
' | java -jar /usr/share/plantuml/plantuml.jar -pipe -tsvg > /tmp/test.svg
grep -c 'кирилиця' /tmp/test.svg # 1 = добре; 0 = поламано
# 4. Чи є шрифти з кирилицею
java -jar /usr/share/plantuml/plantuml.jar -printfonts | grep -i dejavu
Виправлення (за спаданням надійності — досить одного, але п. 1–2 разом надійніші):
// LocalSettings.php
// СПОСІБ 1 (рекомендований): задати кодування явно в команді PlantUML.
// $wgDiagramsLocalCommands перевизначає команди локального рендерингу.
$wgDiagramsLocalCommands = [
'plantuml' => '/usr/bin/java -Dfile.encoding=UTF-8 '
. '-jar /usr/share/plantuml/plantuml.jar -charset UTF-8',
];
# СПОСІБ 2: локаль процесу веб-сервера (systemd drop-in)
# /etc/systemd/system/php8.2-fpm.service.d/locale.conf
[Service]
Environment=LANG=C.UTF-8
Environment=LC_ALL=C.UTF-8
# далі: systemctl daemon-reload && systemctl restart php8.2-fpm
# СПОСІБ 3 (найчистіший): Java 18+.
# JEP 400 зробив UTF-8 кодуванням за замовчуванням незалежно від локалі ОС —
# проблема зникає структурно, без прапорців.
java -version
Смоук-тест перед публікацією
Прогнати на цільовій вікі — по одному представнику кожного типу:
| # | Що перевіряємо | Де в документі | Ознака успіху |
|---|---|---|---|
| 1 | Кодування кирилиці | 9.1 (підписи зв'язків) | Читається «є регістром», а не ���. Перевіряти першим — решта тестів без цього безглузді
|
| 2 | Гліфи кирилиці у шрифті | 5.1, 6.1 | Немає □□□
|
| 3 | <graphviz>: rankdir, shape=record, cylinder |
5.1, 6.1, 8.8, 9.5, Д.1, Д.8 | Діаграма рендериться, підписи не обрізані |
| 4 | <uml> + @startuml, entity, skinparam linetype ortho |
9.1–9.4 | ER-модель із колонками та позначками PK/FK |
| 5 | <uml> + @startgantt |
7.5.1 | Діаграма Ганта. Головний ризик: якщо розширення обгортає вміст у @startuml автоматично, @startgantt не спрацює
|
| 6 | $wgDiagramsDefaultFormat = 'svg' |
будь-яка | У HTML сторінки <img src="….svg">, а не .png
|
| 7 | GraphViz присутній для PlantUML | 9.1–9.4 | Немає повідомлення «Dot executable does not exist». PlantUML використовує dot для розкладки — plantuml без graphviz зрендерить лише sequence-діаграми (7.1), а ER-моделі — ні
|
Альтернативи та запасний варіант
| Варіант | Оцінка |
|---|---|
| Extension:Diagrams ≥ 1.0.0 (обрано) | Одне розширення на всі формати документа; не зберігає рендери як файли вікі; є дві реалізації сервісу рендерингу — PHP (Symfony) та Python (остання узгоджується зі стеком проєкту, розділ 4.3). |
| Extension:GraphViz + Extension:PlantUML окремо | Не рекомендується. Extension:GraphViz за даними трекера Wikimedia не супроводжується й потребує правок для роботи з актуальними версіями MediaWiki. Два розширення замість одного. |
Mermaid (<mermaid> у складі Diagrams або Extension:Mermaid) |
Рендериться на клієнті, не потребує бінарників на сервері. Але erDiagram Mermaid суттєво менш виразний за entity PlantUML: ER-моделі розділу 9 містять типи, ознаки PK/FK та коментарі до полів — це не переноситься без втрат. Придатний для простих схем (5.1, Д.1).
|
| Без розширень | Блоки діаграм відображатимуться як вихідний текст. Документ втрачає 9 із 12 схем — на відміну від версій ≤ 1.2, ASCII-дублів більше немає (див. нижче). |
Що навмисно залишено ASCII-схемами
Після переходу на Diagrams дублювання «діаграма + ASCII» прибрано: воно розходилося при правках. Але 8 блоків <pre> залишено свідомо — для них Diagrams не дає виграшу:
| Розділ | Блок | Чому не діаграма |
|---|---|---|
| 6.8.1–6.8.3 | Макети конструктора | PlantUML має @startsalt для wireframe'ів, але ASCII тут комунікує краще: він явно читається як ескіз і не створює хибного враження затвердженого дизайну. Salt додає рендер-ризик без користі.
|
| 7.4.1 | Timeline підсумків і ТА | Схема несе формулу («Залишок = Підсумок(P) + Σ рухів») поряд із віссю часу. Gantt це не передає, а @startuml timing — надмірність.
|
| Д.9 | Наскрізний розрахунок ЗП | Це покроковий наратив із числами, а не структура. Діаграма не додасть змісту. |
| Д.11, Е.8 | Чек-лист і дерево репозиторію | Список і файлове дерево. <pre> — рівно те, що треба.
|
2. Терміни, визначення та скорочення
| Термін | Визначення |
|---|---|
| Регістр | Об'єкт метаданих, призначений для зберігання та накопичення структурованої облікової інформації у вигляді записів фіксованої структури, з механізмами агрегації, зрізів і транзакційного зв'язку з документами-реєстраторами. |
| РВ | Регістр відомостей (аналог «Регистр сведений»). |
| РН | Регістр накопичення (аналог «Регистр накопления»). |
| РБ | Регістр бухгалтерії (аналог «Регистр бухгалтерии»). |
| РР | Регістр розрахунку (аналог «Регистр расчёта»). |
| Вимірювання (Dimension) | Поле регістру, що визначає розріз аналітики; входить у ключ запису/підсумку. |
| Ресурс (Resource) | Поле регістру, значення якого підсумовується (для РН/РБ/РР) або зберігається як значення (для РВ). |
| Реквізит (Attribute) | Довідкове поле запису, що не бере участі в ключі та не агрегується. |
| Реєстратор (Recorder) | Документ, який породив набір записів регістру та володіє ним. |
| Рухи (Movements) | Записи регістру, підпорядковані конкретному реєстратору. |
| Набір записів (RecordSet) | Прикладний об'єкт для читання/запису групи записів регістру за заданим відбором. |
| Підсумки (Totals) | Попередньо розраховані агрегати (залишки/обороти), що зберігаються в окремих таблицях. |
| ТА | Точка актуальності підсумків — момент часу, до якого підсумки розраховані. |
| Зріз останніх (SliceLast) | Вибірка останніх за періодом записів РВ у розрізі кожної комбінації вимірювань. |
| Субконто (ExtDimension, ED) | Додатковий розріз аналітики бухгалтерського рахунку. |
| ПВР | План видів розрахунку. |
| Витіснення | Механізм РР: запис із вищим пріоритетом скорочує фактичний період дії запису з нижчим. |
| Перерахунок (Recalculation) | Підпорядкований РР об'єкт, що реєструє записи, які потребують повторного розрахунку. |
| Віртуальна таблиця | Обчислювана таблиця в мові запитів, що не має фізичного відповідника (Залишки, Обороти, ЗрізОстанніх тощо). |
3. Призначення, цілі та підстава розробки
3.1. Підстава
Поточна модель K2 ERP не має уніфікованого декларативного механізму накопичувального обліку. Регістроподібні структури реалізуються ad hoc — окремими таблицями та процедурами під кожну прикладну задачу, що спричиняє:
- дублювання коду розрахунку залишків та оборотів;
- відсутність гарантованої узгодженості «документ ↔ рухи»;
- деградацію продуктивності на великих обсягах (повний перерахунок з початку обліку);
- високу вартість введення нової аналітики (зміна схеми БД + правка десятків процедур).
3.2. Мета
Створити в K2 ERP підсистему декларативно описуваних регістрів чотирьох типів, функціонально еквівалентну механізму регістрів «1С:Підприємство 8.3», з автоматичною генерацією схеми БД, підтримкою підсумків, віртуальних таблиць у мові запитів і транзакційного проведення документів.
3.3. Цілі проєкту (вимірювані)
| № | Ціль | Критерій досягнення |
|---|---|---|
| Ц1 | Декларативне визначення регістру | Новий регістр з 5 вимірюваннями та 3 ресурсами створюється в конструкторі без написання коду; DDL генерується автоматично. |
| Ц2 | Автоматична агрегація | Залишки/обороти доступні запитом без ручних процедур перерахунку. |
| Ц3 | Продуктивність | Отримання залишків на дату за таблицею 1 млрд рухів — ≤ 200 мс (P95) на еталонному стенді. |
| Ц4 | Цілісність | Неможливість існування рухів без активного реєстратора (100 % сценаріїв у тесті цілісності). |
| Ц5 | Міграція | Перенесення ≥ 90 % наявних «регістроподібних» структур K2 на новий механізм без втрати даних. |
3.4. Що НЕ входить до обсягу (Out of scope)
- Механізм розподілених інформаційних баз / реплікації рухів (окреме ТЗ).
- Візуальний конструктор звітів поверх регістрів (окреме ТЗ, залежність).
- OLAP-куби та зовнішні аналітичні вітрини.
- Автоматичне перенесення прикладного коду замовника (тільки дані + мапінг).
4. Припущення та обмеження щодо платформи K2 ERP
4.1. Припущення щодо платформи
| № | Припущення | Наслідок для ТЗ |
|---|---|---|
| П1 | K2 ERP має підсистему метаданих із власним репозиторієм об'єктів (документи, довідники, перерахування) і механізмом версіонування конфігурації. | Регістр вбудовується як новий рід об'єкта метаданих у наявний репозиторій (розділ 6). |
| П2 | Об'єкти-документи мають унікальний ідентифікатор посилання (GUID/UUID) та проведення як окрему операцію. | Реєстратор адресується парою «код типу + UUID» (розділ 8.2). |
| П3 | Платформа має власну мову запитів або транслятор у SQL. | Віртуальні таблиці реалізуються на рівні транслятора (розділ 7.6). Якщо мови запитів немає — потрібен окремий етап Е0 (розділ 14). |
| П4 | Цільова СКБД підтримує MERGE/INSERT ... ON CONFLICT, віконні функції, часткові та покривні індекси. |
Алгоритми підсумків (розділ 10) використовують ці можливості. |
| П5 | Прикладний код виконується на сервері застосунків із керованими транзакціями. | Проведення документа — одна транзакція БД (розділ 7.1). |
| П6 | Максимальна довжина ідентифікатора об'єкта БД — 63 символи (PostgreSQL). | Фізичні імена таблиць будуються за сурогатним числовим кодом, а не за іменем регістру (розділ 8.1). |
| П7 | Ядро платформи розробляється на Python 3.12+; допускаються C-розширення (binary-драйвери) та, за потреби, власні нативні модулі. | Визначає стек (4.3) і архітектурні принципи АП-1…АП-6 (4.4). [верифікувати: greenfield чи є наявний Python-код K2] |
| П8 | Прикладний (конфігураційний) код пишеться також на Python і виконується в тому самому процесі. | Породжує питання ізоляції та безпеки виконання довільного коду — відкрите питання В12. Якщо припущення хибне (потрібна вбудована мова) — обсяг Е1 зростає на ~2 чол.-міс. |
4.2. Обмеження
- О1. Зміна складу вимірювань/ресурсів існуючого регістру — операція реструктуризації з простоєм; онлайн-міграція не гарантується для таблиць > 100 млн записів.
- О2. Підсистема не гарантує коректність прикладної логіки проведення — лише цілісність механізму.
- О3. Мінімальна дискретність періоду — 1 секунда (тип
timestamp(0)) [уточнити: чи потрібна мілісекундна].
4.3. Технологічний стек
| Шар | Технологія | Обґрунтування вибору |
|---|---|---|
| Мова ядра | Python 3.12+ (цільова 3.12; щодо 3.13 — див. В14) | Рішення замовника. Наслідки — розділ 4.4. |
| Статична типізація | mypy --strict як gate у CI |
Ядро метаданих у динамічній мові без статичної перевірки — неприйнятний ризик (Р11). |
| Опис і валідація метаданих | Pydantic v2 | Маніфест регістру (розділ 6.7) стає типізованою моделлю: валідація, крос-полеві правила та JSON Schema для конструктора — «безкоштовно». Покриває ФТ-К-01. Приклад — Е.1. |
| Генерація DDL, абстракція діалектів СКБД | SQLAlchemy Core 2.x (без ORM) | Ключовий вибір. MetaData/Table/Column — готова модель схеми; діалекти PostgreSQL і MS SQL із коробки. Прямо закриває НФВ-16, НФВ-17 і знижує ризик Р7. Приклад — Е.2.
|
| Міграції / реструктуризація | Alembic | Автогенерація ALTER при зміні складу полів (Е10, НФВ-13, НФВ-15).
|
| Драйвери | psycopg 3 (binary, pipeline mode) або asyncpg; pyodbc для MS SQL |
Pipeline mode критичний для АП-1: пакетна вставка без round-trip на рядок. |
| Парсер мови запитів | lark (LALR) → AST → SQLAlchemy Core | Етап Е5. Дешевше за власний парсер; граматика декларативна. Ескіз — Е.4. |
| Гарячі шляхи | Cython / mypyc; за потреби Rust (PyO3 + maturin) |
Тільки за результатами бенчмарку (АП-3, Р10, В13). |
| Сервер застосунків | FastAPI + uvicorn; окремий пул процесів для проведення | — |
| Паралелізм | Процеси + asyncio для IO. Потоки — ні (GIL) |
Принцип АП-5. |
| Тестування | pytest, pytest-benchmark, hypothesis (property-based), testcontainers | Hypothesis обов'язковий для витіснення (Р5): інтервальна арифметика — ідеальний кандидат на property-based тести. |
| Профілювання | py-spy, scalene, EXPLAIN (ANALYZE, BUFFERS) |
Етап Е11. |
| Збірка | uv, ruff, wheels із нативними розширеннями під усі цільові платформи | — |
Явні заборони:
- ORM SQLAlchemy не використовується. Регістри працюють наборами записів, а не об'єктами; identity map і lazy loading тут шкідливі й провокують порушення АП-1. Використовується лише Core.
- PyPy не розглядається — несумісність із binary-драйверами та нативними розширеннями.
- Free-threaded build (PEP 703) — моніторити, але не закладати в архітектуру (В14).
4.4. Наслідки вибору Python: архітектурні принципи
Python на CPU-навантаженні повільніший за компільовані мови приблизно в 30–80 разів. Проєктована архітектура цьому не суперечить: основну роботу виконує СКБД, а ядро лише формує SQL. Але це вірно тільки за умови дотримання наведених принципів — їх порушення робить цілі НФВ недосяжними.
| ID | Принцип | Формулювання |
|---|---|---|
| АП-1 | «Жодного рядка за раз» | Заборонено: цикл Python із запитом до БД усередині; вставка рухів по одному; читання підсумків по одному ключу. Обов'язково: executemany / COPY / pipeline, set-based MERGE, один запит на набір записів. Детектор порушень — Е.7.
|
| АП-2 | Ядро — оркестратор, СКБД — обчислювач | Python формує SQL і передає керування. Агрегація, з'єднання, сортування — в СКБД. Виняток — гарячі шляхи АП-4, де це неможливо. |
| АП-3 | Гарячі шляхи ізольовані за інтерфейсом | Кожен кандидат на нативне переписування реалізується за Protocol-інтерфейсом з еталонною Python-реалізацією та бенчмарком у CI. Заміна на Cython/Rust не змінює виклик. Приклад — Е.5.
|
| АП-4 | Бюджет Python-часу зафіксовано | Див. 4.4.1. Перевищення бюджету — блокер приймання етапу. |
| АП-5 | Паралелізм — процесами | GIL робить потоки марними для CPU-роботи. Проведення IO-bound → asyncio; масштабування — пулом процесів за кількістю ядер.
|
| АП-6 | Типізація на межах | mypy --strict на ядрі; Pydantic v2 на всіх межах. Динамічний доступ до полів регістру компенсується генерацією .pyi-стабів (ФТ-К-07, Е.6).
|
4.4.1. Бюджет Python-часу на гарячих шляхах (АП-4)
| Шлях | Розділ | Обсяг операцій | Оцінка Python | Ризик | Мітигація |
|---|---|---|---|---|---|
| Формування рухів документа (100 рядків × 5 регістрів) | 7.1, Д.8 | ~500 конструювань об'єктів | 2–5 мс | Низький | — |
| Серіалізація рухів у пакетну вставку | 7.1 | ~500 кортежів | 1–3 мс | Низький | psycopg 3 pipeline / COPY
|
| Оновлення підсумків | 10.2 | 1 SQL MERGE на регістр |
≈ 0 мс | Низький | set-based (Е.3) |
| Трансляція віртуальної таблиці | 7.3, 10.3 | парсинг + побудова AST | 1–10 мс | Середній | LRU-кеш скомпільованих запитів за хешем (текст + сигнатура параметрів) |
| Витіснення періодів дії | 10.4 | Z\|²) на ключ; 10 тис. ключів × ~10 записів ≈ 106 інтервальних операцій | 1–3 с | Високий | Sweep-line O(n log n); ізоляція за АП-3; кандидат №1 на Cython/Rust; альтернатива — перенести в SQL на daterange + GiST (PostgreSQL)
|
| Розрахунок ЗП | Д.9 | 80 тис. записів × формула | 4–15 с | Високий | Заборона per-record запитів (АП-1); векторизація; кандидат №2 на нативний код |
| Реєстрація перерахунків | 10.5 | 1 SQL INSERT … SELECT |
≈ 0 мс | Низький | set-based |
| Генерація DDL, реструктуризація | 8, Е.2 | холодний шлях, разова операція | — | Низький | — |
4.4.2. Висновок щодо НФВ
| НФВ | Ціль | Частка Python | Вердикт |
|---|---|---|---|
| НФВ-01…03 (залишки, обороти) | 200 мс – 2 с | < 1 % (робота в СКБД) | Не залежить від мови ядра |
| НФВ-04 (проведення 100 рядків) | 300 мс | 5–15 мс ≈ 5 % | Досяжно за дотримання АП-1 |
| НФВ-05 (ОСВ) | 5 с | < 1 % | Не залежить від мови ядра |
| НФВ-06 (ЗП на 10 тис. осіб) | 10 хв | 4–15 с ≈ 2,5 % | Досяжно; запас ×40 |
| НФВ-07 (зріз останніх) | 500 мс | < 1 % | Не залежить від мови ядра |
| НФВ-08 (50 док/с) | — | 50 × 10 мс = 0,5 с CPU/с ≈ 50 % одного ядра | Досяжно; пул процесів (АП-5) |
| НФВ-09 (перерахунок підсумків) | 4 год | < 1 % | Не залежить від мови ядра |
Підсумок: цілі НФВ не переглядаються у зв'язку з вибором Python. Натомість вводиться жорстка вимога АП-1 — її порушення робить цілі недосяжними на будь-якій мові, просто на Python розплата настає на порядок раніше й помітніше.
5. Класифікація регістрів
5.1. Дерево типів

5.2. Порівняльна матриця типів
| Характеристика | РВ | РН (Залишки) | РН (Обороти) | РБ | РР |
|---|---|---|---|---|---|
| Ключ запису | Вимірювання (+Період) або Реєстратор+№ | Реєстратор+№ | Реєстратор+№ | Реєстратор+№ | Реєстратор+№ |
| Незалежний запис | Так | Ні | Ні | Ні | Ні |
| Періодичність | Опційна | Обов'язкова (Період) | Обов'язкова | Обов'язкова | Обов'язкова |
| Ресурси підсумовуються | Ні | Так (±) | Так (+) | Так (Дт/Кт) | Так |
| Вид руху (Прихід/Витрата) | — | Так | — | Дт/Кт | — |
| Таблиця залишків | — | Так | — | Так | — |
| Таблиця оборотів | — | Так (опц.) | Так | Так | — |
| Зв'язок із планом рахунків | — | — | — | Так | — |
| Зв'язок із ПВР | — | — | — | — | Так |
| Період дії / витіснення | — | — | — | — | Так |
| Базовий період | — | — | — | — | Так |
| Графік часу | — | — | — | — | Так |
| Субконто | — | — | — | Так | — |
| Перерахунки | — | — | — | — | Так |
| Типові приклади | Курси валют, ціни, кадрова історія, графік роботи | Товари на складах, взаєморозрахунки | Продажі, витрати за статтями | Госпрозрахунковий, управлінський, податковий облік | Нарахування ЗП, утримання |
6. Метамодель: як регістр задається
6.1. Місце в репозиторії метаданих

6.2. Загальні властивості будь-якого регістру
| Властивість | Тип | Обов'язк. | Опис / допустимі значення |
|---|---|---|---|
Ім'я |
Ідентифікатор | Так | Унікальне в межах роду. Латиниця/кирилиця, ≤ 80 симв., без пробілів. |
Синонім |
Рядок | Ні | Подання для користувача. |
Коментар |
Текст | Ні | — |
ВидРегістру |
Перелік | Так | Відомостей / Накопичення / Бухгалтерії / Розрахунку. Незмінна після створення.
|
РежимЗапису |
Перелік | Так | Незалежний / ПідпорядкованийРеєстратору. Для РН/РБ/РР — примусово ПідпорядкованийРеєстратору.
|
Періодичність |
Перелік | Так | Неперіодичний, ВМежахСекунди, ВМежахДня, ВМежахМісяця, ВМежахКварталу, ВМежахРоку, ПоПозиціїРеєстратора.
|
Реєстратори |
Список посилань на типи документів | Умовно | Обов'язково для РН/РБ/РР та підпорядкованого РВ. Порожній список = регістр непридатний до запису. |
БлокуванняДаних |
Перелік | Ні | Автоматичне / Кероване (за замовчуванням Кероване).
|
ВключатиДоЗвітів |
Булево | Ні | Показувати в універсальному конструкторі звітів. |
ПравоДоступуЗаЗамовчуванням |
Перелік | Ні | Заборонено / Читання.
|
ВикористовуватиРLS |
Булево | Ні | Row-level security (розділ 7.9). |
Партиціонування |
Структура | Ні | Немає / ЗаПеріодом(RANGE, крок) / ЗаВимірюванням(HASH, n).
|
6.3. Поля регістру
Кожне поле має:
| Властивість | Опис |
|---|---|
Ім'я, Синонім |
Ідентифікація. |
Роль |
Вимірювання / Ресурс / Реквізит.
|
Тип |
Примітивний (Число(p,s), Рядок(n), Дата, Булево, УНІ), посилальний (Довідник.X, Документ.Y, Перерахування.Z, ПланРахунків.A, ПланВидівХарактеристик.B) або складений (список типів).
|
Індексувати |
Не індексувати / Індексувати / ІндексуватиЗДодаткомВимірювань.
|
Ведуче (Master) |
Тільки для вимірювань. Якщо Істина — записи регістру видаляються разом з об'єктом, на який посилається вимірювання; регістр показується у формі цього об'єкта.
|
ОсновнийВідбір |
Брати участь у стандартному відборі форми списку. |
ЗаборонитиНезаповненіЗначення |
Валідація на запис. |
Специфічні прапорці полів по типах регістрів:
| Прапорець | Застосовний до | Опис |
|---|---|---|
Балансовий |
Ресурс/Вимірювання РБ | Ресурс бере участь у контролі рівності Дт=Кт; має єдине значення на запис (а не пару Дт/Кт). Вимірювання балансове — однакове для Дт і Кт. |
Облік (AccountingFlag) |
Ресурс РБ | Посилання на ознаку обліку рахунка з ПР; ресурс заповнюється, лише якщо ознака встановлена. |
ОблікСубконто |
Ресурс РБ | Посилання на ознаку обліку субконто (напр. кількісний облік у розрізі субконто). |
Базове (BaseDimension) |
Вимірювання РР | Використовується для зіставлення записів під час отримання бази розрахунку. |
ВикористовуватиПоточніЗначення |
Ресурс/Реквізит РР | — |
6.4. Специфічні властивості за типами
6.4.1. Регістр відомостей
| Властивість | Значення | Примітка |
|---|---|---|
Періодичність |
Неперіодичний … ПоПозиціїРеєстратора | Визначає наявність поля Період і його гранулярність.
|
РежимЗапису |
Незалежний / ПідпорядкованийРеєстратору | У незалежному ключ = (Період) + Вимірювання. |
ОсновнийВідбір |
— | |
ДозволитиПідсумки |
— | У РВ підсумків немає; замість них — зрізи. |
Ключ унікальності:
- Незалежний неперіодичний:
UNIQUE(Вимірювання…) - Незалежний періодичний:
UNIQUE(Період, Вимірювання…) - Підпорядкований:
PK(Реєстратор, НомерРядка), унікальність за вимірюваннями не контролюється.
6.4.2. Регістр накопичення
| Властивість | Значення |
|---|---|
ВидРегістру |
Залишки / Обороти
|
ДозволитиПідсумкиЗалишків |
Булево (тільки для виду «Залишки») |
ДозволитиПідсумкиОборотів |
Булево |
ПеріодичністьПідсумківОборотів |
День / Місяць (тільки для виду «Обороти»)
|
Агрегати |
Список визначень агрегатів (тільки для виду «Обороти»), див. 7.4.4 |
РежимАгрегатів |
Агрегати / Підсумки
|
6.4.3. Регістр бухгалтерії
| Властивість | Значення | Примітка |
|---|---|---|
ПланРахунків |
Посилання на об'єкт «План рахунків» | Обов'язково. Незмінне після створення. |
Кореспонденція |
Булево | Істина → подвійний запис (поля РахунокДт/РахунокКт); Хиба → уніграфічний (Рахунок + ВидРуху).
|
ДозволитиРозділенняПідсумків |
Булево | |
МаксКількістьСубконто |
Число (успадковується з ПР) | Ліміт платформи: 5 [узгодити; 1С — 3 за замовчуванням, до 5] |
Пов'язаний об'єкт План рахунків повинен підтримувати:
- ієрархію рахунків,
Код,Найменування,Вид(Активний/Пасивний/Активно-пасивний); Забалансовий(булево);Ознаки обліку(набір булевих реквізитів, напр. «Кількісний», «Валютний»);- табличну частину
ВидиСубконто(ВидСубконто → ПВХ, Тільки обороти, Підсумкова сума, ознаки обліку субконто); МаксКількістьСубконто.
6.4.4. Регістр розрахунку
| Властивість | Значення | Примітка |
|---|---|---|
ПланВидівРозрахунку |
Посилання | Обов'язково. |
Періодичність |
День/Місяць/Квартал/Рік |
Період реєстрації. |
ПеріодДії |
Булево | Вмикає поля ПеріодДіїПочаток/Кінець, фактичні періоди дії та витіснення.
|
БазовийПеріод |
БезБазовогоПеріоду / ЗаПеріодДії / ЗаПеріодРеєстрації |
Режим отримання бази. |
Графік |
Посилання на РВ | Регістр-графік часу. |
ЗначенняГрафіка |
Ресурс регістру-графіка | Напр. «Значення» (1/0 — робочий день) або «Годин». |
ДатаГрафіка |
Вимірювання регістру-графіка типу Дата |
— |
Перерахунки |
Список підпорядкованих об'єктів «Перерахунок» | Див. 7.5.3. |
Об'єкт Перерахунок (підпорядкований РР):
Ім'я;Вимірювання— список, кожне зіставляється з вимірюванням основного РР (ВимірюванняРегістру);- правило: перерахунок реєструє «об'єкт перерахунку» = (Реєстратор, ВидРозрахунку, Вимірювання…).
План видів розрахунку повинен підтримувати:
ВикористовуєПеріодДії;- табличні частини
БазовіВидиРозрахунку,ВитісняючіВидиРозрахунку,ВедучіВидиРозрахунку; ЗалежністьВідБази:НеЗалежить/ЗаПеріодДії/ЗаПеріодРеєстрації.
6.5. Стандартні реквізити (генеруються автоматично)
| Реквізит | РВ | РН | РБ | РР | Тип |
|---|---|---|---|---|---|
Період |
якщо періодичний | + | + | + (період реєстрації) | Дата/час |
Реєстратор |
якщо підпорядкований | + | + | + | Складене посилання (ДокументПосилання) |
НомерРядка |
якщо підпорядкований | + | + | + | Число(9,0) |
Активність |
якщо підпорядкований | + | + | + | Булево |
ВидРуху |
— | тільки «Залишки» | — | — | Перелік {Прихід, Витрата} |
Рахунок |
— | — | + (без кореспонденції) | — | ПланРахунківПосилання |
РахунокДт, РахунокКт |
— | — | + (з кореспонденцією) | — | ПланРахунківПосилання |
ВидРухуРахунку |
— | — | + (без кореспонденції) | — | Перелік {Дебет, Кредит} |
Субконто (колекція) |
— | — | + | — | Відповідність(ВидСубконто→Значення) |
ВидРозрахунку |
— | — | — | + | ПВРПосилання |
ПеріодДіїПочаток, ПеріодДіїКінець |
— | — | — | + (якщо ПеріодДії) | Дата |
БазовийПеріодПочаток, БазовийПеріодКінець |
— | — | — | + (якщо БазовийПеріод) | Дата |
Сторно |
— | — | — | + | Булево |
6.6. Кількісні обмеження метамоделі
| Параметр | Ліміт | Примітка |
|---|---|---|
| Кількість вимірювань | 20 | Понад 8 — попередження конструктора (деградація підсумків) |
| Кількість ресурсів | 20 | |
| Кількість реквізитів | 30 | |
| Кількість типів у складеному типі поля | 32 | |
| Кількість реєстраторів | Не обмежено | |
| Максимум субконто на рахунок | 5 | |
| Довжина ключа підсумків | ≤ 900 байт (обмеження індексу MS SQL) | Валідація в конструкторі |
| Глибина періодичності підсумків | День / Місяць | Тиждень/Квартал — не підтримуються (обчислюються згортанням) |
6.7. Декларативний опис (DSL)
Регістр зберігається в репозиторії у вигляді XML/YAML-маніфесту. Приклад:
# --- Регістр накопичення (вид: Залишки) ---
register:
name: ТовариНаСкладах
kind: Accumulation
accumulationKind: Balance
synonym: "Товари на складах"
periodicity: WithinSecond
writeMode: SubordinateToRecorder
enableBalanceTotals: true
enableTurnoverTotals: true
partitioning: { mode: RangeByPeriod, step: month }
dimensions:
- { name: Номенклатура, type: "Довідник.Номенклатура", master: true, index: true }
- { name: Склад, type: "Довідник.Склади", master: true, index: true }
- { name: Партія, type: ["Документ.ПрихіднаНакладна", "Документ.ВведенняЗалишків"], index: false }
- { name: Характеристика, type: "ПланВидівХарактеристик.Характеристики", nullable: true }
resources:
- { name: Кількість, type: "Число(15,3)" }
- { name: Сума, type: "Число(15,2)" }
attributes:
- { name: Коментар, type: "Рядок(200)" }
recorders:
- Документ.ПрихіднаНакладна
- Документ.ВидатковаНакладна
- Документ.ПереміщенняТоварів
- Документ.Інвентаризація
# --- Регістр відомостей (періодичний, незалежний) ---
register:
name: КурсиВалют
kind: Information
periodicity: WithinDay
writeMode: Independent
dimensions:
- { name: Валюта, type: "Довідник.Валюти", master: true }
resources:
- { name: Курс, type: "Число(15,4)" }
- { name: Кратність, type: "Число(10,0)", default: 1 }
# --- Регістр бухгалтерії ---
register:
name: Госпрозрахунковий
kind: Accounting
chartOfAccounts: ПланРахунків.Госпрозрахунковий
correspondence: true
periodicity: WithinSecond
dimensions:
- { name: Організація, type: "Довідник.Організації", balance: true }
- { name: Підрозділ, type: "Довідник.Підрозділи", balance: false }
resources:
- { name: Сума, type: "Число(15,2)", balance: true }
- { name: Кількість, type: "Число(15,3)", balance: false, accountingFlag: Кількісний }
- { name: ВалютнаСума, type: "Число(15,2)", balance: false, accountingFlag: Валютний }
attributes:
- { name: Зміст, type: "Рядок(255)" }
- { name: Валюта, type: "Довідник.Валюти" }
recorders: [Документ.БухгалтерськаОперація, Документ.ПрихіднаНакладна]
# --- Регістр розрахунку ---
register:
name: Нарахування
kind: Calculation
calculationTypesPlan: ПланВидівРозрахунку.ОсновніНарахування
periodicity: Month
actionPeriod: true
basePeriod: ByActionPeriod
schedule:
register: РегістрВідомостей.ГрафікРоботи
valueResource: Значення
dateDimension: ДатаГрафіка
dimensions:
- { name: Співробітник, type: "Довідник.Співробітники", baseDimension: true, master: true }
- { name: Підрозділ, type: "Довідник.Підрозділи" }
resources:
- { name: Результат, type: "Число(15,2)" }
- { name: ВідпрацьованоДнів, type: "Число(5,0)" }
- { name: НормаДнів, type: "Число(5,0)" }
recalculations:
- name: ПерерахунокЗаБазою
dimensions:
- { name: Співробітник, registerDimension: Співробітник }
recorders: [Документ.НарахуванняЗарплати]
6.8. UI конструктора метаданих
6.8.1. Загальний макет
┌─ Конфігуратор K2 ERP ─────────────────────────────────────────────────────────┐ │ Файл Правка Конфігурація Адміністрування Сервіс Довідка │ ├──────────────────────┬────────────────────────────────────────────────────────┤ │ Дерево конфігурації │ Регістр накопичення: ТовариНаСкладах │ │ │ ┌──────┬─────────┬──────────┬──────────┬───────────┐ │ │ ▾ Регістри │ │Основ.│Дані │Реєстратор│Форми │Права │ │ │ ▸ відомостей │ └──────┴─────────┴──────────┴──────────┴───────────┘ │ │ ▾ накопичення │ │ │ ▾ ТовариНаСклад. │ Ім'я [ТовариНаСкладах.............] │ │ ▾ Вимірювання │ Синонім [Товари на складах...........] │ │ Номенклат. │ Коментар [.............................] │ │ Склад │ │ │ Партія │ Вид регістру ( ) Обороти (•) Залишки │ │ ▾ Ресурси │ Періодичність [ У межах секунди ▾] │ │ Кількість │ Режим запису [ Підпорядкований реєстратору ▾] │ │ Сума │ │ │ ▸ Реквізити │ ☑ Дозволити підсумки залишків │ │ ▸ Форми │ ☑ Дозволити підсумки оборотів │ │ ▸ Взаєморозрах. │ Періодичність підсумків оборотів [Місяць ▾] │ │ ▸ бухгалтерії │ ☐ Дозволити розділення підсумків │ │ ▸ розрахунку │ │ │ │ Блокування даних [Кероване ▾] │ │ │ Партиціонування [За періодом, крок: місяць ▾] │ │ │ │ │ │ [ Конструктор рухів... ] [ Перевірити ] [ Закрити ] │ ├──────────────────────┴────────────────────────────────────────────────────────┤ │ ⚠ Попередження: 4 вимірювання + місячні підсумки → оцінка таблиці підсумків │ │ ~ 12 млн записів/рік. Розгляньте виключення вимірювання «Партія» з підсумків.│ └───────────────────────────────────────────────────────────────────────────────┘
6.8.2. Палітра властивостей поля
┌─ Властивості: Вимірювання «Партія» ────────────────┐ │ Ім'я [Партія...................] │ │ Синонім [Партія...................] │ │ Тип [Складений тип........ [...]]│ │ ├ Документ.ПрихіднаНакладна │ │ └ Документ.ВведенняЗалишків │ │ Індексувати [Не індексувати ▾] │ │ ☐ Ведуче │ │ ☐ Основний відбір │ │ ☐ Заборонити незаповнені значення │ │ ☑ Використовувати в підсумках │ │ Подання │ │ Підказка [Партія товару...........] │ └────────────────────────────────────────────────────┘
6.8.3. Вкладка «Реєстратори»
┌─ ТовариНаСкладах → Реєстратори ────────────────────────────┐ │ ☑ Документ.ПрихіднаНакладна │ │ ☑ Документ.ВидатковаНакладна │ │ ☑ Документ.ПереміщенняТоварів │ │ ☑ Документ.Інвентаризація │ │ ☐ Документ.Замовлення │ │ ☐ Документ.РахунокНаОплату │ │ [Позначити всі] [Зняти]│ │ ───────────────────────────────────────────────────────── │ │ ⓘ Знімання прапорця для документа, що має рухи, вимагає │ │ очищення рухів. Знайдено рухів: 0 │ └────────────────────────────────────────────────────────────┘
6.8.4. Вимоги до конструктора
- ФТ-К-01. Валідація метаопису в реальному часі з переліком помилок/попереджень.
- ФТ-К-02. Попередній перегляд згенерованого DDL (кнопка «Показати SQL»).
- ФТ-К-03. Калькулятор оцінки обсягу таблиць підсумків (добуток кардинальностей вимірювань × кількість періодів).
- ФТ-К-04. Заборона неприпустимих комбінацій (напр. «Незалежний» + «Регістр накопичення»).
- ФТ-К-05. «Конструктор рухів» — генерація коду формування рухів за табличною частиною документа.
- ФТ-К-06. Порівняння/об'єднання конфігурацій має коректно обробляти об'єкти-регістри.
- ФТ-К-07. Генерація
.pyi-стабів для кожного регістру за метаописом. Прикладний код звертається до полів динамічно (__getattr__), що недоступне дляmypy; згенеровані стаби повертають статичну перевірку. Перегенеровуються при кожній зміні метаданих, перевіряються в CI (див. Е.6). Вимога специфічна для Python, аналога в 1С не має.
7. Функціональні вимоги
7.1. Набір записів і проведення документа
ФТ-01. Платформа надає прикладний об'єкт НабірЗаписів для кожного регістру з властивостями: Відбір, Записувати, і методами Прочитати(), Записати(Заміщати), Очистити(), ЗаблокуватиДляЗміни(), Вивантажити(), Завантажити().
ФТ-02. Для документа доступна колекція Рухи з наборами записів усіх регістрів, де документ вказано реєстратором.
ФТ-03. Сценарій проведення:

ФТ-04. Запис набору записів у режимі Заміщати = Істина (за замовчуванням для рухів) виконує DELETE усіх записів за відбором + INSERT нових — атомарно.
ФТ-05. Скасування проведення документа: усі набори записів за реєстратором очищуються, підсумки коригуються, документ отримує Проведений = Хиба.
ФТ-06. Видалення документа-реєстратора (у т.ч. позначеного на видалення) зобов'язане каскадно видалити всі його рухи в межах однієї транзакції. Реалізація — на рівні платформи (не FK, оскільки посилання поліморфне) + фонова перевірка цілісності (розділ 7.10).
ФТ-07. Режими проведення:
| Режим | Умова | Поведінка |
|---|---|---|
| Оперативне | Дата документа = поточна дата, документ у межах «оперативної точки» | Дозволено контроль залишків «на зараз»; дата може перепризначатися на поточну. |
| Неоперативне | Дата в минулому/майбутньому або документ перепроводиться | Контроль залишків виконується на дату документа; можливий негативний залишок за налаштуванням. |
7.2. Активність записів
ФТ-08. Реквізит Активність = Хиба означає, що запис фізично існує, але не впливає на підсумки та не потрапляє до віртуальних таблиць.
ФТ-09. Зміна активності будь-якого запису тягне коригування підсумків.
ФТ-10. Скасування проведення документа переводить усі його записи в Активність = Хиба або видаляє їх — залежно від налаштування регістру ПоведінкаПриСкасуванні (Видаляти — за замовчуванням / Деактивувати).
7.3. Віртуальні таблиці
ФТ-11. Мова запитів надає для кожного регістру набір віртуальних таблиць. Транслятор перетворює звернення до них на SQL із використанням таблиць підсумків та «живих» рухів після ТА.
| Регістр | Віртуальна таблиця | Параметри | Повертає |
|---|---|---|---|
| РВ | ЗрізОстанніх |
(Період, Умова) | Останній запис на кожну комбінацію вимірювань з періодом ≤ Період |
ЗрізПерших |
(Період, Умова) | Перший запис з періодом ≥ Період | |
| (основна) | — | Усі записи | |
| РН.Залишки | Залишки |
(Період, Умова) | Вимірювання + <Ресурс>Залишок
|
Обороти |
(Початок, Кінець, Періодичність, Умова) | <Ресурс>Прихід, <Ресурс>Витрата, <Ресурс>Оборот
| |
ЗалишкиІОбороти |
(Початок, Кінець, Періодичність, МетодДоповнення, Умова) | <Р>ПочатковийЗалишок, <Р>Прихід, <Р>Витрата, <Р>Оборот, <Р>КінцевийЗалишок
| |
| (основна) | — | Рухи | |
| РН.Обороти | Обороти |
(Початок, Кінець, Періодичність, Умова) | <Ресурс>Оборот
|
| РБ | Залишки |
(Період, УмоваРахунку, Субконто, Умова) | <Р>Залишок, <Р>ЗалишокДт, <Р>ЗалишокКт
|
Обороти |
(Початок, Кінець, Періодичність, УмоваРахунку, Субконто, Умова, УмоваКорРахунку, КорСубконто) | <Р>Оборот, <Р>ОборотДт, <Р>ОборотКт
| |
ЗалишкиІОбороти |
(…) | Повний набір | |
ОборотиДтКт |
(Початок, Кінець, Періодичність, УмоваРахункуДт, СубконтоДт, УмоваРахункуКт, СубконтоКт) | Кореспонденції | |
ДвиженияССубконто |
(Початок, Кінець, УмоваРахунку, Субконто, Умова) | Рухи з розшифровкою субконто | |
Субконто |
(Умова, Порядок…) | Значення субконто записів | |
| РР | ДаніГрафіка |
(Умова) | ЗначенняПеріодуДії, ЗначенняПеріодуРеєстрації, ЗначенняБазовогоПеріоду, ЗначенняФактичногоПеріодуДії
|
ФактичнийПеріодДії |
(Умова) | Записи з фактичними (після витіснення) межами | |
БазаРозрахунку |
(ВимірюванняБази, ВимірюванняОсновногоРегістру, Розрізи, ВидиРозрахунку, Умова) | Агреговані значення ресурсів базових видів розрахунку | |
| (основна) | — | Рухи |
ФТ-12. Приклад запиту:
ВИБРАТИ
Залишки.Номенклатура,
Залишки.Склад,
Залишки.КількістьЗалишок,
Обороти.КількістьПрихід,
Обороти.КількістьВитрата
З
РегістрНакопичення.ТовариНаСкладах.ЗалишкиІОбороти(
&ПочатокПеріоду,
&КінецьПеріоду,
Місяць,
Рух,
Склад = &Склад
І Номенклатура В ІЄРАРХІЇ (&ГрупаНоменклатури)
) ЯК Залишки
ФТ-13. Параметри віртуальних таблиць зобов'язані транслюватися у WHERE підзапиту до звернення до підсумків (push-down), а не фільтруватися після агрегації.
7.4. Підсумки
7.4.1. Модель підсумків залишків
Таблиця залишків зберігає кумулятивний залишок на початок кожного періоду + спеціальний рядок «підсумок за всі періоди» з period = '5999-11-01' (маркер MAXPERIOD).
Таблиця підсумків (місячна) ТА
┌────┬────┬────┬────┬────┐ ▼
─────┬────┬────┬───┴────┴────┴────┴────┴────┴────────────────────┬──────────►
│ │ │ 01.10 01.11 01.12 01.01 01.02 15.02 t
│ │ │ ● ● ● ● ●
│ │ │ бал. бал. бал. бал. бал.
└─ «живі» рухи ─┘
(сканування)
Залишок на 10.02 = Підсумок(01.02) + Σ рухів (01.02 … 10.02]
Залишок на 20.02 = Підсумок(01.02) + Σ рухів (01.02 … 20.02] // після ТА — теж
ФТ-14. Регламентна операція «Перерахунок підсумків» перебудовує таблиці підсумків з нуля. ФТ-15. Регламентна операція «Встановлення періоду розрахунку підсумків» переносить ТА вперед/назад. ФТ-16. При записі рухів раніше ТА підсумки коригуються інкрементально (див. 10.2), без повного перерахунку.
7.4.2. Розділення підсумків
ФТ-17. За увімкненого ДозволитиРозділенняПідсумків у таблиці підсумків додається колонка splitter smallint. Одночасні транзакції пишуть у різні «сплітери», що усуває конкуренцію за рядок. Читання агрегує по всіх сплітерах. Регламентне завдання зливає сплітери.
7.4.3. Обороти
ФТ-18. Таблиця оборотів зберігає агрегати за календарний період (день або місяць — за налаштуванням), без кумуляції.
7.4.4. Агрегати (тільки РН виду «Обороти»)
ФТ-19. Агрегат — додаткова матеріалізована таблиця з підмножиною вимірювань і власною періодичністю (День/Місяць/Квартал/Рік). Оптимізатор запитів обирає найдешевший агрегат, що покриває запит.
ФТ-20. Режими: Реальний час (оновлюється при записі рухів) / Регламентний (оновлюється завданням; запит враховує рухи після дати актуальності агрегату).
ФТ-21. «Порадник агрегатів» аналізує статистику запитів і пропонує склад агрегатів (ВизначитиОптимальніАгрегати()).
7.5. Механіка регістру розрахунку
7.5.1. Витіснення за періодом дії
ФТ-22. Якщо у ПВР для виду розрахунку A вказано B у складі «Витісняючі види розрахунку», то запис виду B скорочує фактичний період дії запису виду A на інтервал перетину.
Diagrams error (with plantuml command): Error line 2 in file: /tmp/diagrams_ina7db0da747e5.plantuml Some diagram description contains errors
ФТ-23. Фактичні періоди дії матеріалізуються у службовій таблиці rg_c_<код>_ap під час запису набору записів.
ФТ-24. Витіснення обчислюється в межах збігу усіх вимірювань регістру та перетину періодів реєстрації, визначених ПВР.
7.5.2. Базовий період і база розрахунку
ФТ-25. Віртуальна таблиця БазаРозрахунку повертає суму ресурсів записів базових видів розрахунку, зіставлених за базовими вимірюваннями, з урахуванням режиму ЗалежністьВідБази:
ЗаПеріодДії— беруться записи, чий період дії перетинається з базовим періодом поточного запису; результат пропорційно розподіляється за часткою перетину (за графіком, якщо заданий);ЗаПеріодРеєстрації— беруться записи, чий період реєстрації потрапляє в базовий період.
7.5.3. Перерахунки
ФТ-26. При записі/зміні/видаленні запису базового виду розрахунку платформа автоматично формує записи у таблиці перерахунку для всіх залежних записів (за збігом вимірювань перерахунку та потраплянням у базовий період). ФТ-27. Прикладний код читає перерахунок як набір записів, перепроводить відповідні документи та очищує оброблені записи. ФТ-28. Перерахунок доступний як об'єкт для звіту «Документи до перерахунку».
7.5.4. Графік часу
ФТ-29. Графік — це РВ з обов'язковим вимірюванням типу Дата та числовим ресурсом. Віртуальна таблиця ДаніГрафіка повертає суму значень графіка за відповідний період (дії / реєстрації / базовий / фактичний період дії).
ФТ-30. Якщо у графіку є додаткові вимірювання (напр. ВидГрафіка), вони зіставляються з однойменними реквізитами/вимірюваннями РР.
7.6. Прикладний API
from datetime import date
from decimal import Decimal
from k2.registers import AccumulationMovementType, DataLock, DataLockMode, Registers
from k2.documents import Document, PostingContext
# ============ Регістр відомостей: незалежний запис ============
mgr = Registers.information("КурсиВалют")
rm = mgr.create_record_manager()
rm.Період = date(2026, 7, 17)
rm.Валюта = usd
rm.Курс = Decimal("41.85")
rm.Кратність = 1
rm.write(replace=True) # INSERT ... ON CONFLICT DO UPDATE
# зріз останніх
row = mgr.get_last(date.today(), {"Валюта": usd})
курс: Decimal = row.Курс
# АП-1: масове завантаження — ОДИН запит, а не цикл із write() усередині
rs = mgr.create_record_set()
rs.load([{"Період": d, "Валюта": v, "Курс": k, "Кратність": 1}
for d, v, k in nbu_rates]) # 3 000 курсів
rs.write(replace=True) # 1 × executemany, ~40 мс
# ============ Регістр накопичення: рухи документа ============
class ВидатковаНакладна(Document):
def on_posting(self, ctx: PostingContext) -> None:
rs = self.movements.ТовариНаСкладах
rs.write = True # набір буде записано наприкінці транзакції
rs.clear()
# АП-1: набір наповнюється генератором, запис — одним batch
rs.extend(
rs.new(
Період=self.Дата,
ВидРуху=AccumulationMovementType.EXPENSE,
Номенклатура=r.Номенклатура,
Склад=self.Склад,
Партія=r.Партія,
Кількість=r.Кількість,
Сума=r.Сума,
)
for r in self.Товари
)
# керовані блокування перед контролем залишків
with DataLock() as dl: # __enter__ виконує lock()
li = dl.add("РегістрНакопичення.ТовариНаСкладах.Залишки")
li.mode = DataLockMode.EXCLUSIVE
li.data_source = rs.unload("Номенклатура", "Склад")
li.use_from_data_source("Номенклатура", "Номенклатура")
li.use_from_data_source("Склад", "Склад")
rs.write_now() # примусовий запис до контролю
self._control_balances(ctx) # прикладна перевірка від'ємних залишків
# ============ Регістр бухгалтерії ============
def _accounting(self) -> None:
rs = self.movements.Госпрозрахунковий
rs.write = True
for r in self.Товари:
e = rs.add_accounting_entry(
Період=self.Дата,
Організація=self.Організація,
РахунокДт=accounts["281"],
РахунокКт=accounts["631"],
Сума=r.Сума,
КількістьДт=r.Кількість,
)
e.subconto_dt[ВидиСубконто.Номенклатура] = r.Номенклатура
e.subconto_dt[ВидиСубконто.Склади] = self.Склад
e.subconto_kt[ВидиСубконто.Контрагенти] = self.Контрагент
# ============ Регістр розрахунку ============
def _calculation(self) -> None:
rs = self.movements.Нарахування
rs.write = True
rs.append(rs.new(
ВидРозрахунку=calc_types["Оклад"],
Період=self.Дата, # період реєстрації
ПеріодДіїПочаток=date(2026, 7, 1),
ПеріодДіїКінець=date(2026, 7, 31),
БазовийПеріодПочаток=date(2026, 6, 1),
БазовийПеріодКінець=date(2026, 6, 30),
Співробітник=self.Співробітник,
Результат=Decimal(0), # заповниться після розрахунку
))
rs.write_now()
# Отримання даних графіка та бази розрахунку — запитом (розділ 7.3), не циклом.
7.7. Блокування та конкурентність
| № | Вимога |
|---|---|
| ФТ-31 | Режим керованих блокувань — за замовчуванням. Рівень ізоляції транзакцій: READ COMMITTED (PostgreSQL) / READ COMMITTED SNAPSHOT (MS SQL).
|
| ФТ-32 | Об'єкт DataLock дозволяє встановити розділювальне/виняткове блокування на «простір» регістру за значеннями вимірювань (реалізація — pg_advisory_xact_lock за хешем ключа або SELECT ... FOR UPDATE по таблиці підсумків).
|
| ФТ-33 | Запис рухів документа блокує тільки записи цього реєстратора (PK містить реєстратор першим). |
| ФТ-34 | Оновлення підсумків не повинно призводити до дедлоків: усі MERGE-операції в межах транзакції виконуються у детермінованому порядку сортування ключа.
|
| ФТ-35 | Гарантія: паралельне проведення двох документів з різними наборами вимірювань не конкурує (за увімкненого розділення підсумків). |
| ФТ-36 | Таймаут очікування блокування — 20 с (налаштовується); при перевищенні — керована помилка з текстом конфліктного ключа. |
7.8. Права доступу
ФТ-37. Для регістру визначаються права: Читання, Додавання, Зміна, Видалення, Перегляд, Редагування, ПереглядПідсумків.
ФТ-38. Обмеження на рівні записів (RLS) задаються шаблоном умови по вимірюваннях; транслюються в WHERE усіх звернень, включно з віртуальними таблицями.
ФТ-39. Ризик: RLS на віртуальних таблицях залишків потребує узгодженості з підсумками. Рішення — RLS дозволено тільки по вимірюваннях, що входять до ключа підсумків; конструктор валідує це правило.
7.9. Журналювання, аудит, службові сервіси
ФТ-40. Реєстрація в журналі: перерахунок підсумків, зміна ТА, реструктуризація регістру, злиття сплітерів. ФТ-41. Службовий сервіс «Перевірка цілісності регістрів»:
- «висячі» рухи (реєстратор не існує або не проведений);
- розбіжність підсумків і сум рухів (контрольний перерахунок у тіньову таблицю з порівнянням);
- некоректні фактичні періоди дії РР;
- для РБ — порушення рівності Дт = Кт за балансовим ресурсом у межах реєстратора.
ФТ-42. Звіт «Аналіз розміру регістрів»: кількість записів, розмір таблиць та індексів, дата останнього перерахунку підсумків.
8. Модель даних (як це виглядає в базі даних)
8.1. Конвенції іменування
Фізичні імена не успадковують прикладні імена (обмеження довжини ідентифікатора, кирилиця, перейменування об'єктів). Ім'я будується за сурогатним кодом об'єкта метаданих <id> (4-значний hex/dec).
| Шаблон | Призначення | Приклад |
|---|---|---|
rg_i_<id> |
Регістр відомостей — основна таблиця | rg_i_0007
|
rg_a_<id> |
Регістр накопичення — рухи | rg_a_0012
|
rg_a_<id>_tb |
РН — підсумки залишків (Totals Balance) | rg_a_0012_tb
|
rg_a_<id>_tt |
РН — підсумки оборотів (Totals Turnover) | rg_a_0012_tt
|
rg_a_<id>_ag<n> |
РН — агрегат № n | rg_a_0031_ag1
|
rg_b_<id> |
Регістр бухгалтерії — проводки | rg_b_0020
|
rg_b_<id>_ed |
РБ — субконто (якщо винесено в дочірню таблицю) | rg_b_0020_ed
|
rg_b_<id>_tb<k> |
РБ — підсумки залишків за рівнем субконто k (0…N) | rg_b_0020_tb2
|
rg_b_<id>_tt<k> |
РБ — підсумки оборотів за рівнем субконто k | rg_b_0020_tt1
|
rg_b_<id>_tc |
РБ — обороти між рахунками (кореспонденції) | rg_b_0020_tc
|
rg_c_<id> |
Регістр розрахунку — рухи | rg_c_0040
|
rg_c_<id>_ap |
РР — фактичні періоди дії | rg_c_0040_ap
|
rg_c_<id>_rc<m> |
РР — перерахунок № m | rg_c_0040_rc1
|
rg_<*>_<id>_opt |
Службова: ТА підсумків, налаштування | rg_a_0012_opt
|
md_* |
Таблиці метаданих | md_register
|
Колонки полів: d<n> — вимірювання, r<n> — ресурс, a<n> — реквізит, де n — порядковий номер поля в метаописі (стабільний, не перевикористовується після видалення поля). Мапінг «прикладне ім'я ↔ фізична колонка» зберігається в md_register_field.
8.2. Типізація та зберігання значень
8.2.1. Примітивні типи
| Тип платформи | PostgreSQL | MS SQL |
|---|---|---|
| Число(p,s) | numeric(p,s) |
numeric(p,s)
|
| Рядок(n) | varchar(n) |
nvarchar(n)
|
| Рядок(необмежена) | text |
nvarchar(max)
|
| Дата / ДатаЧас | timestamp(0) |
datetime2(0)
|
| Булево | boolean |
bit
|
| УНІ | uuid |
uniqueidentifier
|
8.2.2. Посилальні типи
- Простий (один тип): одна колонка
d<n>_r uuid NOT NULL, зовнішній ключ до таблиці об'єкта. Порожнє посилання ='00000000-0000-0000-0000-000000000000'(а не NULL — щоб зберегти семантику «пустого значення» у ключах підсумків). - Складений (кілька типів / примітив + посилання): група колонок:
| Колонка | Тип | Опис |
|---|---|---|
d<n>_t |
smallint |
Код типу: 1=Невизначено, 2=Булево, 3=Число, 4=Рядок, 5=Дата, ≥100 = md_type.id посилального типу
|
d<n>_b |
boolean |
Значення, якщо _t=2 |
d<n>_n |
numeric(38,10) |
Значення, якщо _t=3 |
d<n>_s |
varchar(N) |
Значення, якщо _t=4 |
d<n>_d |
timestamp(0) |
Значення, якщо _t=5 |
d<n>_r |
uuid |
Значення, якщо _t≥100 |
8.2.3. Реєстратор
Завжди складений (будь-який з дозволених документів):
rec_t smallint NOT NULL— код типу документа (md_type.id);rec_r uuid NOT NULL— посилання.
FK неможливий (поліморфізм) → цілісність забезпечується платформою + сервісом перевірки (ФТ-41).
8.2.4. Маркери періодів
| Константа | Значення | Використання |
|---|---|---|
MINPERIOD |
0001-01-01 00:00:00 |
Нижня межа |
MAXPERIOD |
5999-11-01 00:00:00 |
Рядок «підсумок за всі періоди» в таблицях залишків |
8.3. Таблиці метаданих
-- ============ РЕПОЗИТОРІЙ МЕТАДАНИХ ============
CREATE TABLE md_type ( -- реєстр усіх типів конфігурації
id smallint PRIMARY KEY,
kind varchar(20) NOT NULL, -- Catalog|Document|Enum|ChartOfAccounts|ChartOfCalcTypes|Register|...
name varchar(80) NOT NULL,
table_name varchar(63),
UNIQUE (kind, name)
);
CREATE TABLE md_register (
id smallint PRIMARY KEY, -- = md_type.id
name varchar(80) NOT NULL UNIQUE,
synonym varchar(255),
comment text,
kind varchar(12) NOT NULL
CHECK (kind IN ('Information','Accumulation','Accounting','Calculation')),
-- загальні
write_mode varchar(24) NOT NULL DEFAULT 'SubordinateToRecorder'
CHECK (write_mode IN ('Independent','SubordinateToRecorder')),
periodicity varchar(24) NOT NULL DEFAULT 'Nonperiodical'
CHECK (periodicity IN ('Nonperiodical','WithinSecond','WithinDay','WithinMonth',
'WithinQuarter','WithinYear','ByRecorderPosition')),
lock_mode varchar(12) NOT NULL DEFAULT 'Managed',
use_rls boolean NOT NULL DEFAULT false,
partition_mode varchar(20) NOT NULL DEFAULT 'None',
partition_step varchar(10),
-- РН
accumulation_kind varchar(10) CHECK (accumulation_kind IN ('Balance','Turnover')),
enable_bal_totals boolean NOT NULL DEFAULT true,
enable_tno_totals boolean NOT NULL DEFAULT false,
tno_totals_period varchar(6) CHECK (tno_totals_period IN ('Day','Month')),
split_totals boolean NOT NULL DEFAULT false,
split_count smallint NOT NULL DEFAULT 1,
aggregates_mode varchar(10),
-- РБ
chart_of_accounts_id smallint REFERENCES md_type(id),
correspondence boolean NOT NULL DEFAULT true,
max_extdim_count smallint NOT NULL DEFAULT 3,
-- РР
calc_types_plan_id smallint REFERENCES md_type(id),
use_action_period boolean NOT NULL DEFAULT false,
base_period_mode varchar(24)
CHECK (base_period_mode IN ('None','ByActionPeriod','ByRegistrationPeriod')),
schedule_register_id smallint REFERENCES md_register(id),
schedule_value_field smallint,
schedule_date_field smallint,
-- службове
physical_table varchar(63) NOT NULL,
struct_version int NOT NULL DEFAULT 1,
CONSTRAINT chk_kind_props CHECK (
(kind = 'Accumulation' AND accumulation_kind IS NOT NULL) OR
(kind = 'Accounting' AND chart_of_accounts_id IS NOT NULL) OR
(kind = 'Calculation' AND calc_types_plan_id IS NOT NULL) OR
(kind = 'Information')
)
);
CREATE TABLE md_register_field (
id smallint NOT NULL, -- порядковий номер поля (стабільний)
register_id smallint NOT NULL REFERENCES md_register(id) ON DELETE CASCADE,
name varchar(80) NOT NULL,
synonym varchar(255),
role varchar(12) NOT NULL CHECK (role IN ('Dimension','Resource','Attribute')),
data_type_expr text NOT NULL, -- серіалізований опис типу (може бути складеним)
is_composite boolean NOT NULL DEFAULT false,
precision_ smallint,
scale_ smallint,
length_ int,
index_mode varchar(28) NOT NULL DEFAULT 'None'
CHECK (index_mode IN ('None','Index','IndexWithAddDimensions')),
is_master boolean NOT NULL DEFAULT false, -- «Ведуче»
in_default_filter boolean NOT NULL DEFAULT false,
deny_empty boolean NOT NULL DEFAULT false,
use_in_totals boolean NOT NULL DEFAULT true,
-- РБ
is_balance boolean NOT NULL DEFAULT false,
accounting_flag varchar(80),
extdim_flag varchar(80),
-- РР
is_base_dimension boolean NOT NULL DEFAULT false,
-- фізика
column_prefix varchar(8) NOT NULL, -- 'd3','r1','a2'
PRIMARY KEY (register_id, id),
UNIQUE (register_id, name)
);
CREATE TABLE md_register_recorder (
register_id smallint NOT NULL REFERENCES md_register(id) ON DELETE CASCADE,
doc_type_id smallint NOT NULL REFERENCES md_type(id),
PRIMARY KEY (register_id, doc_type_id)
);
CREATE TABLE md_register_aggregate (
id smallint NOT NULL,
register_id smallint NOT NULL REFERENCES md_register(id) ON DELETE CASCADE,
name varchar(80) NOT NULL,
periodicity varchar(8) NOT NULL CHECK (periodicity IN ('Day','Month','Quarter','Year')),
use_mode varchar(12) NOT NULL DEFAULT 'Auto',
is_realtime boolean NOT NULL DEFAULT false,
actual_upto timestamp(0),
physical_table varchar(63) NOT NULL,
PRIMARY KEY (register_id, id)
);
CREATE TABLE md_register_aggregate_dim (
register_id smallint NOT NULL,
aggregate_id smallint NOT NULL,
field_id smallint NOT NULL,
PRIMARY KEY (register_id, aggregate_id, field_id),
FOREIGN KEY (register_id, aggregate_id) REFERENCES md_register_aggregate(register_id, id) ON DELETE CASCADE
);
CREATE TABLE md_register_recalc (
id smallint NOT NULL,
register_id smallint NOT NULL REFERENCES md_register(id) ON DELETE CASCADE,
name varchar(80) NOT NULL,
physical_table varchar(63) NOT NULL,
PRIMARY KEY (register_id, id)
);
CREATE TABLE md_register_recalc_dim (
register_id smallint NOT NULL,
recalc_id smallint NOT NULL,
name varchar(80) NOT NULL,
reg_field_id smallint NOT NULL, -- вимірювання основного регістру
column_prefix varchar(8) NOT NULL,
PRIMARY KEY (register_id, recalc_id, name),
FOREIGN KEY (register_id, recalc_id) REFERENCES md_register_recalc(register_id, id) ON DELETE CASCADE
);
8.4. Регістр відомостей
8.4.1. Незалежний періодичний (приклад: КурсиВалют)
CREATE TABLE rg_i_0007 (
period timestamp(0) NOT NULL, -- Період (усічений до дня: WithinDay)
d1_r uuid NOT NULL, -- Вимірювання «Валюта»
r1 numeric(15,4) NOT NULL, -- Ресурс «Курс»
r2 numeric(10,0) NOT NULL DEFAULT 1, -- Ресурс «Кратність»
CONSTRAINT pk_rg_i_0007 PRIMARY KEY (period, d1_r)
);
-- Індекс для зрізу останніх у розрізі вимірювання
CREATE INDEX ix_rg_i_0007_d1 ON rg_i_0007 (d1_r, period DESC) INCLUDE (r1, r2);
ALTER TABLE rg_i_0007 ADD CONSTRAINT fk_rg_i_0007_d1
FOREIGN KEY (d1_r) REFERENCES cat_0003(ref); -- Довідник.Валюти
8.4.2. Підпорядкований реєстратору
CREATE TABLE rg_i_0008 (
rec_t smallint NOT NULL,
rec_r uuid NOT NULL,
line_no int NOT NULL,
period timestamp(0) NOT NULL,
active boolean NOT NULL DEFAULT true,
d1_r uuid NOT NULL,
r1 numeric(15,2) NOT NULL,
CONSTRAINT pk_rg_i_0008 PRIMARY KEY (rec_t, rec_r, line_no)
);
CREATE INDEX ix_rg_i_0008_period ON rg_i_0008 (period, rec_t, rec_r, line_no);
CREATE INDEX ix_rg_i_0008_d1 ON rg_i_0008 (d1_r, period) WHERE active;
8.4.3. Незалежний неперіодичний
CREATE TABLE rg_i_0009 (
d1_r uuid NOT NULL,
d2_r uuid NOT NULL,
r1 varchar(200),
CONSTRAINT pk_rg_i_0009 PRIMARY KEY (d1_r, d2_r)
);
8.5. Регістр накопичення
8.5.1. Таблиця рухів
-- ТовариНаСкладах: dims = Номенклатура(d1), Склад(d2), Партія(d3, складений), Характеристика(d4)
-- res = Кількість(r1), Сума(r2); attr = Коментар(a1)
CREATE TABLE rg_a_0012 (
rec_t smallint NOT NULL,
rec_r uuid NOT NULL,
line_no int NOT NULL,
period timestamp(0) NOT NULL,
active boolean NOT NULL DEFAULT true,
mvt smallint NOT NULL CHECK (mvt IN (0,1)), -- 0=Прихід, 1=Витрата
d1_r uuid NOT NULL,
d2_r uuid NOT NULL,
d3_t smallint NOT NULL, -- складене вимірювання «Партія»
d3_r uuid NOT NULL,
d4_r uuid NOT NULL,
r1 numeric(15,3) NOT NULL DEFAULT 0,
r2 numeric(15,2) NOT NULL DEFAULT 0,
a1 varchar(200),
CONSTRAINT pk_rg_a_0012 PRIMARY KEY (rec_t, rec_r, line_no)
) PARTITION BY RANGE (period);
CREATE TABLE rg_a_0012_p2026m07 PARTITION OF rg_a_0012
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
-- ... інші секції створюються регламентним завданням
CREATE INDEX ix_rg_a_0012_period ON rg_a_0012 (period, rec_t, rec_r, line_no);
CREATE INDEX ix_rg_a_0012_d1 ON rg_a_0012 (d1_r, period, d2_r) WHERE active; -- Номенклатура (Індексувати з дод. вимірюваннями)
CREATE INDEX ix_rg_a_0012_d2 ON rg_a_0012 (d2_r, period, d1_r) WHERE active; -- Склад
8.5.2. Таблиця підсумків залишків
CREATE TABLE rg_a_0012_tb (
period timestamp(0) NOT NULL, -- початок місяця; MAXPERIOD = підсумок за всі періоди
splitter smallint NOT NULL DEFAULT 0,
d1_r uuid NOT NULL,
d2_r uuid NOT NULL,
d3_t smallint NOT NULL,
d3_r uuid NOT NULL,
d4_r uuid NOT NULL,
r1 numeric(20,3) NOT NULL, -- Кількість, знаковий залишок
r2 numeric(20,2) NOT NULL, -- Сума
CONSTRAINT pk_rg_a_0012_tb PRIMARY KEY (period, d1_r, d2_r, d3_t, d3_r, d4_r, splitter)
);
-- покривний індекс для типового відбору «залишки по складу»
CREATE INDEX ix_rg_a_0012_tb_d2 ON rg_a_0012_tb (d2_r, period, d1_r) INCLUDE (r1, r2);
Семантика: рядок з period = P містить залишок на початок періоду P. Рядок з period = MAXPERIOD — поточний залишок за всі періоди (використовується для запитів «залишки на зараз», найчастіший кейс).
8.5.3. Таблиця підсумків оборотів
CREATE TABLE rg_a_0012_tt (
period timestamp(0) NOT NULL, -- початок періоду агрегації (день/місяць)
splitter smallint NOT NULL DEFAULT 0,
d1_r uuid NOT NULL,
d2_r uuid NOT NULL,
d3_t smallint NOT NULL,
d3_r uuid NOT NULL,
d4_r uuid NOT NULL,
r1_in numeric(20,3) NOT NULL DEFAULT 0, -- Кількість Прихід
r1_out numeric(20,3) NOT NULL DEFAULT 0, -- Кількість Витрата
r2_in numeric(20,2) NOT NULL DEFAULT 0,
r2_out numeric(20,2) NOT NULL DEFAULT 0,
CONSTRAINT pk_rg_a_0012_tt PRIMARY KEY (period, d1_r, d2_r, d3_t, d3_r, d4_r, splitter)
);
8.5.4. Службова таблиця налаштувань
CREATE TABLE rg_a_0012_opt (
id smallint PRIMARY KEY DEFAULT 1 CHECK (id = 1),
totals_actual_to timestamp(0) NOT NULL, -- ТА: підсумки розраховані до
use_bal_totals boolean NOT NULL DEFAULT true,
use_tno_totals boolean NOT NULL DEFAULT true,
min_period timestamp(0), -- період першого руху
last_recalc_at timestamp(0),
split_merge_at timestamp(0)
);
8.5.5. Агрегат (для РН виду «Обороти»)
-- Агрегат «Продажі за місяцями по номенклатурі та складу» (без Партії, без Характеристики)
CREATE TABLE rg_a_0031_ag1 (
period timestamp(0) NOT NULL,
d1_r uuid NOT NULL,
d2_r uuid NOT NULL,
r1 numeric(20,3) NOT NULL,
r2 numeric(20,2) NOT NULL,
CONSTRAINT pk_rg_a_0031_ag1 PRIMARY KEY (period, d1_r, d2_r)
);
8.6. Регістр бухгалтерії
8.6.1. План рахунків (супутні таблиці)
CREATE TABLE coa_0005 ( -- ПланРахунків.Госпрозрахунковий
ref uuid PRIMARY KEY,
parent_ref uuid REFERENCES coa_0005(ref),
code varchar(20) NOT NULL,
order_code varchar(40) NOT NULL, -- впорядкований код для ієрархічних запитів
name varchar(150) NOT NULL,
acc_type smallint NOT NULL, -- 0=Активний,1=Пасивний,2=Активно-пасивний
off_balance boolean NOT NULL DEFAULT false,
ed_count smallint NOT NULL DEFAULT 0, -- фактична кількість субконто
fl_qty boolean NOT NULL DEFAULT false, -- ознака обліку «Кількісний»
fl_cur boolean NOT NULL DEFAULT false, -- ознака обліку «Валютний»
marked_del boolean NOT NULL DEFAULT false
);
CREATE UNIQUE INDEX ux_coa_0005_code ON coa_0005 (code);
CREATE TABLE coa_0005_ed ( -- ТЧ «ВидиСубконто»
ref uuid NOT NULL REFERENCES coa_0005(ref) ON DELETE CASCADE,
line_no int NOT NULL,
ed_type_r uuid NOT NULL, -- ПланВидівХарактеристик.ВидиСубконто
only_tno boolean NOT NULL DEFAULT false, -- «Тільки обороти»
sum_flag boolean NOT NULL DEFAULT true, -- «Підсумкова сума»
fl_qty boolean NOT NULL DEFAULT false, -- ознака обліку субконто «Кількісний»
PRIMARY KEY (ref, line_no)
);
8.6.2. Таблиця проводок (з кореспонденцією)
-- Госпрозрахунковий: dims = Організація(d1, балансове), Підрозділ(d2, небалансове)
-- res = Сума(r1, балансовий), Кількість(r2, небалансовий), ВалютнаСума(r3, небалансовий)
-- attr = Зміст(a1), Валюта(a2); max_extdim_count = 3
CREATE TABLE rg_b_0020 (
rec_t smallint NOT NULL,
rec_r uuid NOT NULL,
line_no int NOT NULL,
period timestamp(0) NOT NULL,
active boolean NOT NULL DEFAULT true,
acc_dt_r uuid NOT NULL REFERENCES coa_0005(ref),
acc_kt_r uuid NOT NULL REFERENCES coa_0005(ref),
-- балансове вимірювання: одне значення на проводку
d1_r uuid NOT NULL,
-- небалансове вимірювання: окремо для Дт і Кт
d2_dt_r uuid NOT NULL,
d2_kt_r uuid NOT NULL,
-- субконто Дт (3 слоти, складений тип: ПВХ-значення)
ed_dt1_t smallint, ed_dt1_r uuid,
ed_dt2_t smallint, ed_dt2_r uuid,
ed_dt3_t smallint, ed_dt3_r uuid,
ed_dt_h bigint NOT NULL DEFAULT 0, -- хеш набору субконто Дт (для JOIN з підсумками)
-- субконто Кт
ed_kt1_t smallint, ed_kt1_r uuid,
ed_kt2_t smallint, ed_kt2_r uuid,
ed_kt3_t smallint, ed_kt3_r uuid,
ed_kt_h bigint NOT NULL DEFAULT 0,
-- балансовий ресурс: одне значення
r1 numeric(20,2) NOT NULL DEFAULT 0, -- Сума
-- небалансові ресурси: пара Дт/Кт
r2_dt numeric(20,3) NOT NULL DEFAULT 0, -- Кількість Дт
r2_kt numeric(20,3) NOT NULL DEFAULT 0, -- Кількість Кт
r3_dt numeric(20,2) NOT NULL DEFAULT 0, -- ВалютнаСума Дт
r3_kt numeric(20,2) NOT NULL DEFAULT 0,
a1 varchar(255),
a2_r uuid,
CONSTRAINT pk_rg_b_0020 PRIMARY KEY (rec_t, rec_r, line_no)
) PARTITION BY RANGE (period);
CREATE INDEX ix_rg_b_0020_period ON rg_b_0020 (period, rec_t, rec_r, line_no);
CREATE INDEX ix_rg_b_0020_acc_dt ON rg_b_0020 (acc_dt_r, period) WHERE active;
CREATE INDEX ix_rg_b_0020_acc_kt ON rg_b_0020 (acc_kt_r, period) WHERE active;
CREATE INDEX ix_rg_b_0020_ed_dt ON rg_b_0020 (ed_dt1_t, ed_dt1_r, period) WHERE active;
8.6.3. Таблиця проводок (без кореспонденції)
CREATE TABLE rg_b_0021 (
rec_t smallint NOT NULL,
rec_r uuid NOT NULL,
line_no int NOT NULL,
period timestamp(0) NOT NULL,
active boolean NOT NULL DEFAULT true,
acc_mvt smallint NOT NULL CHECK (acc_mvt IN (0,1)), -- 0=Дебет, 1=Кредит
acc_r uuid NOT NULL REFERENCES coa_0006(ref),
d1_r uuid NOT NULL,
ed1_t smallint, ed1_r uuid,
ed2_t smallint, ed2_r uuid,
ed3_t smallint, ed3_r uuid,
ed_h bigint NOT NULL DEFAULT 0,
r1 numeric(20,2) NOT NULL DEFAULT 0,
CONSTRAINT pk_rg_b_0021 PRIMARY KEY (rec_t, rec_r, line_no)
);
8.6.4. Підсумки РБ
Підсумки будуються на кількох «рівнях субконто» — щоб запит без відбору за субконто не сканував найдетальнішу таблицю.
-- Рівень 0: залишки по рахунку + балансові вимірювання (без субконто)
CREATE TABLE rg_b_0020_tb0 (
period timestamp(0) NOT NULL,
splitter smallint NOT NULL DEFAULT 0,
acc_r uuid NOT NULL,
d1_r uuid NOT NULL, -- Організація (балансове)
r1 numeric(24,2) NOT NULL, -- Сума: >0 = дебетовий, <0 = кредитовий залишок
r2 numeric(24,3) NOT NULL, -- Кількість
r3 numeric(24,2) NOT NULL,
CONSTRAINT pk_rg_b_0020_tb0 PRIMARY KEY (period, acc_r, d1_r, splitter)
);
-- Рівень 1..3: залишки з розшифровкою по субконто (тільки для рахунків з ed_count >= k)
CREATE TABLE rg_b_0020_tb1 (
period timestamp(0) NOT NULL, splitter smallint NOT NULL DEFAULT 0,
acc_r uuid NOT NULL, d1_r uuid NOT NULL,
ed1_t smallint NOT NULL, ed1_r uuid NOT NULL,
r1 numeric(24,2) NOT NULL, r2 numeric(24,3) NOT NULL, r3 numeric(24,2) NOT NULL,
CONSTRAINT pk_rg_b_0020_tb1 PRIMARY KEY (period, acc_r, d1_r, ed1_t, ed1_r, splitter)
);
CREATE TABLE rg_b_0020_tb2 (
period timestamp(0) NOT NULL, splitter smallint NOT NULL DEFAULT 0,
acc_r uuid NOT NULL, d1_r uuid NOT NULL,
ed1_t smallint NOT NULL, ed1_r uuid NOT NULL,
ed2_t smallint NOT NULL, ed2_r uuid NOT NULL,
r1 numeric(24,2) NOT NULL, r2 numeric(24,3) NOT NULL, r3 numeric(24,2) NOT NULL,
CONSTRAINT pk_rg_b_0020_tb2 PRIMARY KEY (period, acc_r, d1_r, ed1_t, ed1_r, ed2_t, ed2_r, splitter)
);
-- rg_b_0020_tb3 — аналогічно, 3 субконто
-- Обороти (аналогічні рівні): tt0..tt3
CREATE TABLE rg_b_0020_tt0 (
period timestamp(0) NOT NULL,
splitter smallint NOT NULL DEFAULT 0,
acc_r uuid NOT NULL,
d1_r uuid NOT NULL,
r1_dt numeric(24,2) NOT NULL, r1_kt numeric(24,2) NOT NULL,
r2_dt numeric(24,3) NOT NULL, r2_kt numeric(24,3) NOT NULL,
r3_dt numeric(24,2) NOT NULL, r3_kt numeric(24,2) NOT NULL,
CONSTRAINT pk_rg_b_0020_tt0 PRIMARY KEY (period, acc_r, d1_r, splitter)
);
-- Обороти між рахунками (кореспонденції)
CREATE TABLE rg_b_0020_tc (
period timestamp(0) NOT NULL,
splitter smallint NOT NULL DEFAULT 0,
acc_dt_r uuid NOT NULL,
acc_kt_r uuid NOT NULL,
d1_r uuid NOT NULL,
r1 numeric(24,2) NOT NULL,
CONSTRAINT pk_rg_b_0020_tc PRIMARY KEY (period, acc_dt_r, acc_kt_r, d1_r, splitter)
);
Правило запису рівнів: для проводки з рахунком, що має ed_count = k, оновлюються таблиці tb0 … tbK (k+1 таблиць). Рахунки без субконто оновлюють лише tb0. Субконто з ознакою «Тільки обороти» не потрапляють у tb*, лише в tt*.
8.7. Регістр розрахунку
-- Нарахування: dims = Співробітник(d1, базове), Підрозділ(d2)
-- res = Результат(r1), ВідпрацьованоДнів(r2), НормаДнів(r3)
-- attr = Графік(a1)
CREATE TABLE rg_c_0040 (
rec_t smallint NOT NULL,
rec_r uuid NOT NULL,
line_no int NOT NULL,
period timestamp(0) NOT NULL, -- період реєстрації (початок місяця)
active boolean NOT NULL DEFAULT true,
reversal boolean NOT NULL DEFAULT false, -- Сторно
calc_type_r uuid NOT NULL, -- ВидРозрахунку → pct_0011(ref)
act_begin timestamp(0) NOT NULL, -- ПеріодДіїПочаток
act_end timestamp(0) NOT NULL, -- ПеріодДіїКінець
base_begin timestamp(0), -- БазовийПеріодПочаток
base_end timestamp(0), -- БазовийПеріодКінець
d1_r uuid NOT NULL,
d2_r uuid NOT NULL,
r1 numeric(15,2) NOT NULL DEFAULT 0,
r2 numeric(5,0) NOT NULL DEFAULT 0,
r3 numeric(5,0) NOT NULL DEFAULT 0,
a1_r uuid,
CONSTRAINT pk_rg_c_0040 PRIMARY KEY (rec_t, rec_r, line_no),
CONSTRAINT chk_rg_c_0040_act CHECK (act_end >= act_begin)
);
CREATE INDEX ix_rg_c_0040_period ON rg_c_0040 (period, rec_t, rec_r, line_no);
CREATE INDEX ix_rg_c_0040_d1 ON rg_c_0040 (d1_r, period, calc_type_r) WHERE active;
CREATE INDEX ix_rg_c_0040_act ON rg_c_0040 (d1_r, act_begin, act_end) WHERE active;
CREATE INDEX ix_rg_c_0040_base ON rg_c_0040 (d1_r, calc_type_r, base_begin, base_end) WHERE active;
-- Фактичні періоди дії (після витіснення). Один запис → 0..N інтервалів.
CREATE TABLE rg_c_0040_ap (
rec_t smallint NOT NULL,
rec_r uuid NOT NULL,
line_no int NOT NULL,
interval_no smallint NOT NULL,
calc_type_r uuid NOT NULL,
d1_r uuid NOT NULL, -- денормалізація базового вимірювання для швидкого JOIN
fact_begin timestamp(0) NOT NULL,
fact_end timestamp(0) NOT NULL,
CONSTRAINT pk_rg_c_0040_ap PRIMARY KEY (rec_t, rec_r, line_no, interval_no),
CONSTRAINT fk_rg_c_0040_ap FOREIGN KEY (rec_t, rec_r, line_no)
REFERENCES rg_c_0040 (rec_t, rec_r, line_no) ON DELETE CASCADE
);
CREATE INDEX ix_rg_c_0040_ap_d1 ON rg_c_0040_ap (d1_r, fact_begin, fact_end);
-- Перерахунок «ПерерахунокЗаБазою»
CREATE TABLE rg_c_0040_rc1 (
rec_t smallint NOT NULL, -- реєстратор запису, який треба перерахувати
rec_r uuid NOT NULL,
calc_type_r uuid NOT NULL, -- вид розрахунку, що потребує перерахунку
d1_r uuid NOT NULL, -- вимірювання перерахунку «Співробітник»
src_rec_t smallint NOT NULL, -- реєстратор, що спричинив перерахунок
src_rec_r uuid NOT NULL,
created_at timestamp(0) NOT NULL DEFAULT now(),
CONSTRAINT pk_rg_c_0040_rc1 PRIMARY KEY (rec_t, rec_r, calc_type_r, d1_r)
);
CREATE INDEX ix_rg_c_0040_rc1_src ON rg_c_0040_rc1 (src_rec_t, src_rec_r);
8.8. Зведення: які таблиці породжує кожен тип регістру

Оцінка: для РБ з МаксКількістьСубконто = 3 генерується 10 фізичних таблиць на один регістр.
9. ER-моделі
9.1. ER-модель метаданих (репозиторій)

9.2. ER-модель: регістр накопичення (вид «Залишки»)

9.3. ER-модель: регістр бухгалтерії

9.4. ER-модель: регістр розрахунку

9.5. Зведена концептуальна ER (усі типи)
Diagrams error (with dot command): Warning: flat edge between adjacent nodes one of which has a record shape - replace records with HTML-like labels Edge MDF -> MD Error: lost MD MDF edge
Легенда:
(PK*)— ключ для незалежного РВ;(PK**)— для підпорядкованого.- Пунктир «реєстратор» — логічний зв'язок без FK (поліморфне посилання, підтримується платформою).
- Пунктир «генерує DDL» — метадані → фізична схема (не зв'язок даних).
10. Ключові алгоритми
10.1. Запис набору записів (режим «Заміщати»)
def write_record_set(reg: Register, rs: RecordSet, replace: bool = True) -> None:
"""Розділ 7.1, ФТ-01…ФТ-04. Уся операція — в одній транзакції."""
check_rights(reg, Right.INSERT | Right.UPDATE) # ФТ-37
validate(reg, rs) # реєстратор ∈ md_register_recorder;
# deny_empty; для РБ — Σ Дт = Σ Кт
with conn.begin(): # ФТ-03: одна транзакція
old = read_for_update(reg, rs.filter) # SELECT ... FOR UPDATE
# АП-2: дельта рахується набором, один раз; тільки active-записи
delta = aggregate(rs.active_records, sign=+1) + aggregate(old, sign=-1)
if replace: # ФТ-04
conn.execute(delete(tbl).where(*rs.filter.clauses)) # 1 запит
conn.execute(insert(tbl), rs.as_dicts()) # 1 executemany (АП-1)
update_totals(reg, delta) # -> merge_totals(), розділи 10.2, Е.3
if reg.kind is RegisterKind.CALCULATION:
keys = rs.affected_keys | affected_keys(old)
recompute_actual_periods(reg, keys) # розділ 10.4
register_recalculations(reg, [*rs.records, *old]) # розділ 10.5
10.2. Інкрементальне оновлення підсумків залишків
-- Δ (delta) — тимчасова таблиця: (period_month, d1_r, d2_r, d3_t, d3_r, d4_r, dr1, dr2)
-- де dr = Σ (CASE WHEN mvt = 0 THEN +значення ELSE -значення END)
-- Крок 1. Кумулятивні залишки: рух у місяці M впливає на підсумки всіх періодів > M
-- та на рядок MAXPERIOD.
INSERT INTO rg_a_0012_tb AS t (period, splitter, d1_r, d2_r, d3_t, d3_r, d4_r, r1, r2)
SELECT p.period, :splitter, d.d1_r, d.d2_r, d.d3_t, d.d3_r, d.d4_r, d.dr1, d.dr2
FROM delta d
CROSS JOIN LATERAL (
SELECT period FROM totals_periods
WHERE period > d.period_month
AND period <= (SELECT totals_actual_to FROM rg_a_0012_opt)
UNION ALL SELECT TIMESTAMP '5999-11-01' -- MAXPERIOD
) p
ORDER BY 1,3,4,5,6,7 -- детермінований порядок → без дедлоків
ON CONFLICT (period, d1_r, d2_r, d3_t, d3_r, d4_r, splitter)
DO UPDATE SET r1 = t.r1 + EXCLUDED.r1,
r2 = t.r2 + EXCLUDED.r2;
-- Крок 2. Прибирання нульових рядків (регламентно, не в транзакції проведення)
DELETE FROM rg_a_0012_tb WHERE r1 = 0 AND r2 = 0 AND period <> TIMESTAMP '5999-11-01';
10.3. Трансляція віртуальної таблиці «Залишки»
-- РегістрНакопичення.ТовариНаСкладах.Залишки(&Період, Склад = &Склад)
WITH boundary AS (
SELECT COALESCE(MAX(period), TIMESTAMP '0001-01-01') AS p
FROM totals_periods
WHERE period <= :Період
AND period <= (SELECT totals_actual_to FROM rg_a_0012_opt)
),
base AS ( -- залишок на межу періоду з підсумків
SELECT t.d1_r, t.d2_r, t.d3_t, t.d3_r, t.d4_r,
SUM(t.r1) AS r1, SUM(t.r2) AS r2
FROM rg_a_0012_tb t, boundary b
WHERE t.period = b.p
AND t.d2_r = :Склад -- push-down параметра ВТ
GROUP BY 1,2,3,4,5
),
tail AS ( -- «живі» рухи після межі й до &Період
SELECT m.d1_r, m.d2_r, m.d3_t, m.d3_r, m.d4_r,
SUM(CASE WHEN m.mvt = 0 THEN m.r1 ELSE -m.r1 END) AS r1,
SUM(CASE WHEN m.mvt = 0 THEN m.r2 ELSE -m.r2 END) AS r2
FROM rg_a_0012 m, boundary b
WHERE m.active
AND m.period > b.p
AND m.period <= :Період
AND m.d2_r = :Склад
GROUP BY 1,2,3,4,5
)
SELECT d1_r AS "Номенклатура", d2_r AS "Склад",
d3_t, d3_r AS "Партія", d4_r AS "Характеристика",
SUM(r1) AS "КількістьЗалишок",
SUM(r2) AS "СумаЗалишок"
FROM (SELECT * FROM base UNION ALL SELECT * FROM tail) u
GROUP BY 1,2,3,4,5
HAVING SUM(r1) <> 0 OR SUM(r2) <> 0; -- нульові залишки не повертаються
Оптимізація для найчастішого випадку: якщо :Період ≥ ТА і в регістрі немає рухів у майбутньому — base читається з рядка MAXPERIOD одним index seek, tail = ∅.
10.4. Розрахунок фактичних періодів дії (витіснення)
def recompute_actual_periods(reg: Register, keys: set[DimKey]) -> None:
"""Розділ 7.5.1, ФТ-22…ФТ-24. Тут — оркестрація; сам алгоритм — Додаток Е.5."""
tbl = schema.movements(reg)
ap = schema.actual_periods(reg) # rg_c_<id>_ap
# 1. Читання всіх записів зачеплених ключів — ОДИН запит (АП-1)
records = conn.execute(
select(tbl).where(tbl.c.active, dim_key_in(tbl, keys))
).all()
# 2. Перерахунок інтервалів. Гарячий шлях: бюджет ≤ 3 c (АП-4),
# реалізація за Protocol-інтерфейсом (АП-3) — див. Е.5.
# Пріоритет: A витісняється B, якщо B ∈ ПВР[A].ВитісняючіВидиРозрахунку
intervals = displacement_engine.compute(records, displaces=plan.displaces)
# 3. Заміщення фактичних періодів — два запити на весь набір, не на запис
conn.execute(delete(ap).where(record_key_in(ap, intervals.keys())))
conn.execute(insert(ap), [
{"rec_t": k.rec_t, "rec_r": k.rec_r, "line_no": k.line_no,
"interval_no": n, "calc_type_r": k.calc_type, "d1_r": k.base_dim,
"fact_begin": begin, "fact_end": end}
for k, ivs in intervals.items()
for n, (begin, end) in enumerate(ivs) # 1 запис -> 0..N інтервалів
])
Складність: O(|Z|² ) на ключ. Обмеження: якщо |Z| > 500 для одного ключа — попередження в журналі та перехід на алгоритм замітання (sweep line) O(|Z| log |Z|).
10.5. Реєстрація перерахунків
def register_recalculations(reg: Register, changed: Sequence[CalcRecord]) -> None:
"""Розділ 7.5.3, ФТ-26. АП-2: пошук залежних виконує СКБД, а не цикл у Python."""
q = schema.movements(reg).alias("q")
b = schema.calc_plan_base(reg).alias("b") # ТЧ «БазовіВидиРозрахунку» ПВР
for recalc in reg.recalculations:
rc = schema.recalc_table(reg, recalc) # rg_c_<id>_rc<n>
src = values_of(changed).alias("src") # змінені записи як VALUES-таблиця
# інтервальний предикат залежно від режиму базового періоду
if reg.base_period == "ByActionPeriod":
in_base = and_(src.c.act_begin <= q.c.base_end,
src.c.act_end >= q.c.base_begin) # перетин періодів
else: # ByRegistrationPeriod
in_base = src.c.period.between(q.c.base_begin, q.c.base_end)
dependent = (
select(q.c.rec_t, q.c.rec_r, q.c.calc_type_r,
*[q.c[c] for c in recalc.dim_columns],
src.c.rec_t.label("src_rec_t"), src.c.rec_r.label("src_rec_r"))
.select_from(
src.join(b, b.c.base_type_r == src.c.calc_type_r) # z — базовий для q
.join(q, q.c.calc_type_r == b.c.ref))
.where(q.c.active, in_base,
*[q.c[c] == src.c[c] for c in recalc.dim_columns],
tuple_(q.c.rec_t, q.c.rec_r) != tuple_(src.c.rec_t, src.c.rec_r))
.distinct()
)
stmt = pg_insert(rc).from_select(list(rc.columns.keys()), dependent)
conn.execute(stmt.on_conflict_do_nothing()) # 1 запит на перерахунок (АП-1)
10.6. Зріз останніх
-- РегістрВідомостей.КурсиВалют.ЗрізОстанніх(&Дата, Валюта В (&Список))
SELECT DISTINCT ON (r.d1_r)
r.d1_r AS "Валюта", r.period AS "Період", r.r1 AS "Курс", r.r2 AS "Кратність"
FROM rg_i_0007 r
WHERE r.period <= :Дата
AND r.d1_r = ANY (:Список)
ORDER BY r.d1_r, r.period DESC; -- використовує ix_rg_i_0007_d1 (d1_r, period DESC)
-- Для MS SQL: ROW_NUMBER() OVER (PARTITION BY d1_r ORDER BY period DESC) = 1
-- або OUTER APPLY (SELECT TOP 1 ...) для великих списків.
11. Нефункціональні вимоги
11.1. Продуктивність
Еталонний стенд: 16 vCPU, 128 ГБ RAM, NVMe, PostgreSQL 15, БД 1,5 ТБ.
| № | Операція | Обсяг | Ціль (P95) |
|---|---|---|---|
| НФВ-01 | Залишки на поточну дату, відбір за 1 вимірюванням | 1 млрд рухів, 20 млн рядків підсумків | ≤ 200 мс |
| НФВ-02 | Залишки на довільну дату в межах ТА | те саме | ≤ 400 мс |
| НФВ-03 | Обороти за 12 місяців із групуванням | 1 млрд рухів | ≤ 2 с |
| НФВ-04 | Проведення документа зі 100 рядками рухів (3 регістри) | — | ≤ 300 мс |
| НФВ-05 | Оборотно-сальдова відомість по всіх рахунках за місяць | 200 млн проводок | ≤ 5 с |
| НФВ-06 | Розрахунок ЗП: 10 000 співробітників × 8 видів розрахунку | — | ≤ 10 хв |
| НФВ-07 | Зріз останніх по 5 000 елементів вимірювання | 50 млн записів РВ | ≤ 500 мс |
| НФВ-08 | Пропускна здатність проведення | — | ≥ 50 док/с (10 паралельних сеансів) |
| НФВ-09 | Перерахунок підсумків РН | 1 млрд рухів | ≤ 4 год (у вікні обслуговування) |
11.2. Масштабованість і обсяги
| Параметр | Значення |
|---|---|
| Макс. записів в одному регістрі | 1010 |
| Макс. кількість регістрів у конфігурації | 2 000 |
| Макс. одночасних сеансів проведення | 200 |
| Секціонування | Обов'язкове для таблиць > 100 млн записів |
11.3. Надійність і цілісність
- НФВ-10. Рухи та реєстратор змінюються в одній транзакції БД; часткове проведення неможливе.
- НФВ-11. Аварійне завершення сервера не залишає підсумки неузгодженими (гарантується транзакцією).
- НФВ-12. Сервіс перевірки цілісності (ФТ-41) виконується регламентно; розбіжність підсумків = інцидент P1.
11.4. Супроводжуваність
- НФВ-13. Реструктуризація регістру (додавання ресурсу/реквізиту) для таблиці ≤ 100 млн записів — ≤ 30 хв.
- НФВ-14. Додавання вимірювання вимагає перебудови підсумків; система повинна попереджати про оцінку часу до початку операції.
- НФВ-15. Усі DDL-операції — ідемпотентні та скриптовані (можливість застосувати вручну).
11.5. Сумісність
- НФВ-16. PostgreSQL 14+ та MS SQL Server 2019+ — обидві платформи з однаковою функціональністю.
- НФВ-17. Генератор DDL ізольований у шар «діалект СКБД».
12. Міграція існуючих структур K2 ERP
12.1. Етапи
| № | Етап | Результат |
|---|---|---|
| М1 | Інвентаризація | Реєстр наявних «регістроподібних» таблиць K2 з класифікацією за типом-мішенню (РВ/РН/РБ/РР). |
| М2 | Мапінг | Для кожної таблиці — метаопис цільового регістру + правила перетворення колонок. |
| М3 | Створення регістрів | Генерація метаданих і DDL у тестовому контурі. |
| М4 | Перенесення даних | ETL: історичні дані → рухи, з призначенням реєстратора (реальний документ або службовий «ВведенняЗалишків»). |
| М5 | Побудова підсумків | Повний перерахунок. |
| М6 | Звірка | Порівняння залишків/оборотів «стара система vs нова» на контрольних датах; допуск розбіжності = 0. |
| М7 | Перемикання коду | Заміна прямих SQL-звернень на віртуальні таблиці; період паралельної роботи ≥ 1 звітний період. |
| М8 | Виведення з експлуатації | Видалення старих таблиць після 2 закритих періодів. |
12.2. Проблемні випадки
| Випадок | Рішення |
|---|---|
| Історичні записи без документа-джерела | Службовий документ ВведенняЗалишків (один на період/розділ).
|
| Записи з дублями за ключем (для незалежного РВ) | Дедуплікація з протоколом; конфлікти — на ручний розбір. |
| Дробові періоди / некоректні дати (NULL, 1900-01-01) | Нормалізація до MINPERIOD; протокол. |
| Прикладний код, що пише в таблиці напряму | Заборона на рівні прав БД після М7; аудит звернень на етапі М3–М6. |
| Аналітика, відсутня в цільовій моделі | Перенесення в реквізити (не у вимірювання) або відмова від міграції з обґрунтуванням. |
13. Приймальні випробування
13.1. Функціональні тести (витяг)
| ID | Сценарій | Очікуваний результат |
|---|---|---|
| ФТ-Т-01 | Створити РВ періодичний незалежний, записати 2 записи з однаковим ключем | Другий запис заміщує перший; у таблиці 1 рядок |
| ФТ-Т-02 | Провести документ → 5 рухів РН | 5 записів у rg_a_*; підсумки MAXPERIOD змінилися на суму дельти
|
| ФТ-Т-03 | Скасувати проведення | 0 записів; підсумки повернулися до вихідних значень |
| ФТ-Т-04 | Видалити документ-реєстратор | Рухи видалені каскадно; перевірка цілісності — 0 помилок |
| ФТ-Т-05 | Записати рух заднім числом (−6 міс.) | Залишки на всі дати після руху коректні (звірка з повним перерахунком) |
| ФТ-Т-06 | Установити Активність = Хиба |
Запис не впливає на віртуальні таблиці; підсумки скориговані |
| ФТ-Т-07 | РБ: провести незбалансовану проводку (Дт ≠ Кт за балансовим ресурсом) | Помилка проведення, транзакція відкочена |
| ФТ-Т-08 | РБ: ОСВ по рахунку з 3 субконто | Підсумки за рівнями 0–3 узгоджені між собою (згортка tb3 → tb0) |
| ФТ-Т-09 | РР: сценарій «Оклад + Відпустка + Лікарняний» (розд. 7.5.1) | Фактичні періоди дії відповідають еталону; «Оклад» дає 2 інтервали |
| ФТ-Т-10 | РР: зміна базового запису | У таблиці перерахунку з'явився запис для залежного документа |
| ФТ-Т-11 | Паралельне проведення 2 документів за різними складами | Обидва проходять без очікування (розділення підсумків увімкнено) |
| ФТ-Т-12 | Паралельне проведення 2 документів за одним складом і одним товаром з контролем залишків | Один чекає, другий проходить; від'ємного залишку немає |
| ФТ-Т-13 | Перерахунок підсумків після хаотичних 106 операцій | Підсумки збігаються з повним перерахунком «з нуля» до копійки |
| ФТ-Т-14 | Реструктуризація: додати ресурс до РН із 10 млн записів | Дані збережені, підсумки перебудовані, час ≤ НФВ-13 |
| ФТ-Т-15 | RLS: користувач з обмеженням по організації | Віртуальні таблиці повертають лише дозволені дані |
13.2. Навантажувальні тести
| ID | Тест | Критерій |
|---|---|---|
| НТ-01 | Генерація 1 млрд рухів РН, заміри НФВ-01…НФВ-03 | Досягнення цілей |
| НТ-02 | Профіль «90 % рухів поточним періодом, 10 % заднім числом» — порівняння двох стратегій підсумків (розд. 10.2) | Обґрунтований вибір Рішення №3 |
| НТ-03 | Пікове проведення 50 док/с протягом 1 год | Відсутність дедлоків; P95 ≤ НФВ-04 |
| НТ-04 | РБ: субконто-слоти vs дочірня таблиця, 200 млн проводок | Обґрунтований вибір Рішення №2 |
| НТ-05 | Розрахунок ЗП на 10 000 співробітників | НФВ-06 |
| НТ-06 | Деградація за 24 год безперервної роботи | Приріст часу відгуку ≤ 15 % |
14. Етапи робіт та оцінка
| Етап | Зміст | Результат | Оцінка, чол.-міс. | Залежності |
|---|---|---|---|---|
| Е0 | Обстеження платформи, верифікація припущень П1–П6, ескізний проєкт | Затверджений архітектурний ескіз, уточнене ТЗ | 1,5 | — |
| Е1 | Метамодель + репозиторій + генератор DDL + конструктор (базовий) | Створення регістру будь-якого типу з генерацією схеми | 3 | Е0 |
| Е2 | Регістр відомостей: рантайм, набори записів, зрізи, форми | РВ у продуктиві | 2,5 | Е1 |
| Е3 | Регістр накопичення: рухи, підсумки, ТА, віртуальні таблиці | РН у продуктиві | 5 | Е2 |
| Е4 | Механізм проведення документів, блокування, оперативне/неоперативне проведення | Транзакційне проведення | 2,5 | Е3 |
| Е5 | Розширення мови запитів (віртуальні таблиці, транслятор, push-down) | Запити до ВТ | 3,5 | Е3 |
| Е6 | Регістр бухгалтерії + план рахунків + субконто + підсумки за рівнями | РБ у продуктиві | 6 | Е4, Е5 |
| Е7 | Регістр розрахунку + ПВР + витіснення + база + графіки + перерахунки | РР у продуктиві | 6,5 | Е4, Е5 |
| Е8 | Агрегати, розділення підсумків, порадник агрегатів | Оптимізація | 2,5 | Е3 |
| Е9 | Права доступу, RLS, аудит, сервіси цілісності | Безпека та адміністрування | 2 | Е6, Е7 |
| Е10 | Реструктуризація, міграція, інструменти звірки | Інструменти міграції | 2 | Е6, Е7 |
| Е11 | Навантажувальне тестування, оптимізація, документація, профілювання Python + бенчмарки гарячих шляхів | Приймальні випробування | 4,5 | усі |
| РАЗОМ | 41,5 | |||
Мінімальний життєздатний обсяг (MVP): Е0–Е5 (РВ + РН + проведення + запити) ≈ 18 чол.-міс. — покриває ~70 % прикладних сценаріїв K2 ERP.
14.1. Вплив вибору Python на оцінку
| Етап | Було (мова не визначена) | Стало (Python) | Δ | Причина |
|---|---|---|---|---|
| Е1 (метамодель, DDL, конструктор) | 4 | 3 | −1 | SQLAlchemy Core закриває генерацію DDL і діалекти СКБД; Pydantic v2 — валідацію DSL. Разом ~30 % етапу — готове. |
| Е5 (мова запитів, транслятор) | 4 | 3,5 | −0,5 | lark (граматика) + SQLAlchemy Core expression language замість власного AST→SQL. |
| Е7 (регістр розрахунку) | 6 | 6,5 | +0,5 | Ізоляція витіснення за інтерфейсом (АП-3) + резерв на нативну реалізацію. |
| Е10 (реструктуризація, міграція) | 2,5 | 2 | −0,5 | Alembic покриває автогенерацію ALTER.
|
| Е11 (навантаження, оптимізація) | 3 | 4,5 | +1,5 | Профілювання Python, бенчмарки бюджету АП-4, ймовірні Cython/Rust-модулі, wheels у CI. |
| РАЗОМ | 41,5 | 41,5 | 0 | Економія на інфраструктурному коді компенсує витрати на оптимізацію. |
Висновок: Python — нейтральний за трудомісткістю для цього проєкту. Він здешевлює те, чого тут багато (метадані, генерація схеми, абстракція діалектів, міграції), і дорожчає те, чого тут мало (гарячі CPU-цикли). Ризик зміщується з «не встигнемо написати» на «не вкладемося в бюджет часу виконання» — тому бенчмарки гарячих шляхів (АП-4) вводяться в CI з етапу Е3, а не наприкінці.
15. Ризики
| ID | Ризик | Ймов. | Вплив | Пом'якшення |
|---|---|---|---|---|
| Р1 | У K2 ERP немає власної мови запитів → віртуальні таблиці нікуди вбудовувати | Сер. | Крит. | Верифікувати на Е0. План «Б»: віртуальні таблиці як параметризовані view/TVF + шар ORM. |
| Р2 | Конкуренція за таблицю підсумків на піках проведення | Вис. | Вис. | Розділення підсумків (сплітери), НТ-03, детермінований порядок MERGE. |
| Р3 | Рухи «заднім числом» роблять оновлення підсумків O(N періодів) | Вис. | Сер. | Рішення №3 (dirty months + регламентний перерахунок), НТ-02. |
| Р4 | «Широка» схема субконто → реструктуризація при зміні ліміту | Сер. | Сер. | Зафіксувати ліміт 5 із запасом; НТ-04; план «Б» — дочірня таблиця. |
| Р5 | Механізм витіснення РР — найскладніший і найпомилковіший вузол | Вис. | Вис. | Виділити в окремий модуль з еталонним набором з 50+ тест-кейсів; property-based тестування. |
| Р6 | Обсяг Е6+Е7 недооцінено (досвід 1С: це роки розробки) | Вис. | Вис. | MVP-підхід: спершу Е0–Е5, рішення про Е6/Е7 — після ретроспективи. |
| Р7 | Різна поведінка PostgreSQL і MS SQL (плани, MERGE, ізоляція) | Сер. | Сер. | Єдиний набір тестів на обох СКБД у CI від Е1. |
| Р8 | Міграція історичних даних без реєстраторів | Вис. | Сер. | Службовий документ «ВведенняЗалишків»; етап звірки М6 з нульовим допуском. |
| Р9 | Патентні / ліцензійні претензії щодо копіювання механізмів | Низ. | Вис. | Юридична експертиза до Е1: реалізація концепцій обліку (загальновідомих), а не коду/API; уникати дослівного копіювання найменувань API та документації. |
| Р10 | Гарячі шляхи (витіснення, розрахунок ЗП) на чистому Python не вкладаються в бюджет АП-4 | Сер. | Сер. | Ізоляція за Protocol-інтерфейсом (АП-3) + бенчмарк у CI від Е7; готовність до Cython/Rust; альтернатива для витіснення — daterange+GiST у PostgreSQL.
|
| Р11 | Динамічна типізація в ядрі метаданих → помилки типів спливають у рантаймі на продуктиві | Вис. | Сер. | mypy --strict як обов'язковий gate у CI; Pydantic v2 на всіх межах; генерація .pyi-стабів для прикладного коду (ФТ-К-07).
|
| Р12 | GIL обмежує вертикальне масштабування сервера застосунків | Сер. | Низ. | АП-5: asyncio для IO + мультипроцесний пул. Розрахунок у 4.4.2 показує запас: 50 док/с ≈ 50 % одного ядра. |
| Р13 | Нативні розширення (Cython/Rust) ускладнюють збірку, кросплатформність і пошук інженерів | Низ. | Низ. | Вводяться тільки за результатами бенчмарку; wheels під усі цільові платформи в CI; питання В13. |
| Р14 | Виконання прикладного Python-коду в процесі сервера = виконання довільного коду з правами сервера (П8) | Вис. | Крит. | Питання В12 до Е0. Варіанти: (а) довірена модель — конфігурацію пише лише вендор/партнер; (б) ізоляція процесами + seccomp/AppArmor; (в) обмежений DSL замість повного Python. У 1С вбудована мова існує в т.ч. з цієї причини. |
16. Додатки
Додаток А. Матриця відповідності «1С ↔ K2 ERP»
| Об'єкт / поняття 1С | Пропонований аналог K2 ERP | Примітка |
|---|---|---|
| РегистрСведений | Регістри.Відомостей.* |
Повний паритет |
| РегистрНакопления | Регістри.Накопичення.* |
Повний паритет |
| РегистрБухгалтерии | Регістри.Бухгалтерії.* |
Субконто зберігаються інакше (слоти) |
| РегистрРасчета | Регістри.Розрахунку.* |
Повний паритет |
| Регистратор | Реєстратор |
— |
| ВидДвижения | ВидРуху |
— |
| Итоги | Підсумки |
— |
| Точка актуальности итогов | ТА підсумків |
— |
| Разделение итогов | Розділення підсумків (splitter) |
— |
| Агрегаты | Агрегати |
— |
| Субконто / ПланВидовХарактеристик | Субконто / ПланВидівХарактеристик |
— |
| ПланВидовРасчета | ПланВидівРозрахунку |
— |
| Перерасчет | Перерахунок |
— |
| ПериодДействия / Вытеснение | ПеріодДії / Витіснення |
— |
| Границя послідовності | Не реалізується | Механізм визнано застарілим; замість нього — перепроведення за списком «брудних» документів |
| РегистрыСведений з режимом «ПоПозицииРегистратора» | ПоПозиціїРеєстратора |
Реалізується як (period, rec_t, rec_r, line_no) у ключі |
Додаток Б. Альтернатива зберігання субконто (план «Б» до Рішення №2)
-- Дочірня таблиця «ключ-значення» замість слотів
CREATE TABLE rg_b_0020_ed (
rec_t smallint NOT NULL,
rec_r uuid NOT NULL,
line_no int NOT NULL,
side smallint NOT NULL CHECK (side IN (0,1)), -- 0=Дт, 1=Кт
ed_no smallint NOT NULL, -- порядковий номер субконто рахунка
ed_type_r uuid NOT NULL, -- вид субконто
val_t smallint NOT NULL,
val_r uuid,
val_n numeric(38,10),
val_s varchar(150),
val_d timestamp(0),
PRIMARY KEY (rec_t, rec_r, line_no, side, ed_no),
FOREIGN KEY (rec_t, rec_r, line_no) REFERENCES rg_b_0020 (rec_t, rec_r, line_no) ON DELETE CASCADE
);
CREATE INDEX ix_rg_b_0020_ed_val ON rg_b_0020_ed (ed_type_r, val_t, val_r);
| Критерій | Слоти (Рішення №2) | Дочірня таблиця (план «Б») |
|---|---|---|
| Відбір за 1 субконто | Index seek по колонці | JOIN + семі-з'єднання |
| Відбір за 3 субконто | 3 предикати в одному скані | 3 JOIN |
Зміна МаксКількістьСубконто |
ALTER TABLE (реструктуризація) | Без змін схеми |
| Розмір рядка проводки | +48 байт × 2 (Дт/Кт) | Базовий |
| Складність генератора DDL | Вища | Нижча |
| Рекомендація | Обрано за замовчуванням | Резерв за результатами НТ-04 |
Додаток В. Перелік вимог (реєстр трасування)
| ID | Розділ | Коротко | Пріоритет | Тест |
|---|---|---|---|---|
| ФТ-01…ФТ-07 | 7.1 | Набори записів, проведення | Must | ФТ-Т-02…04 |
| ФТ-08…ФТ-10 | 7.2 | Активність | Must | ФТ-Т-06 |
| ФТ-11…ФТ-13 | 7.3 | Віртуальні таблиці | Must | ФТ-Т-08, НТ-01 |
| ФТ-14…ФТ-18 | 7.4 | Підсумки, ТА | Must | ФТ-Т-05, ФТ-Т-13 |
| ФТ-19…ФТ-21 | 7.4.4 | Агрегати | Should | НТ-01 |
| ФТ-22…ФТ-30 | 7.5 | Механіка РР | Must (для Е7) | ФТ-Т-09, ФТ-Т-10 |
| ФТ-31…ФТ-36 | 7.7 | Блокування | Must | ФТ-Т-11, ФТ-Т-12, НТ-03 |
| ФТ-37…ФТ-39 | 7.8 | Права, RLS | Must | ФТ-Т-15 |
| ФТ-40…ФТ-42 | 7.9 | Аудит, цілісність | Should | ФТ-Т-04 |
| ФТ-К-01…06 | 6.8.4 | Конструктор | Should | — |
| НФВ-01…НФВ-09 | 11.1 | Продуктивність | Must | НТ-01…НТ-06 |
| НФВ-10…НФВ-12 | 11.3 | Цілісність | Must | ФТ-Т-13 |
| НФВ-13…НФВ-17 | 11.4–11.5 | Супровід, сумісність | Should | ФТ-Т-14 |
Додаток Г. Відкриті питання
| № | Питання | Кому | Термін |
|---|---|---|---|
| В1 | Чи існує в K2 ERP мова запитів / транслятор у SQL? (визначає Р1 та обсяг Е5) | Архітектор платформи | До Е0 |
| В2 | Формат ідентифікації об'єктів: UUID чи сурогатний int? | Архітектор платформи | До Е0 |
| В3 | Чи потрібна мілісекундна дискретність періоду? | Замовник | До Е1 |
| В4 | Максимальна кількість субконто: 3 чи 5? | Головний бухгалтер / методолог | До Е6 |
| В5 | Чи потрібен механізм «Границя послідовності»? (пропонується відмова) | Методолог | До Е3 |
| В6 | Пріоритет типів регістрів: чи достатньо MVP (РВ + РН) для першого релізу? | Замовник | До Е0 |
| В7 | Правова оцінка ризику Р9 | Юридична служба | До Е1 |
| В8 | Цільова СКБД для першої черги: тільки PostgreSQL чи одразу обидві? | Архітектор / ІТ-експлуатація | До Е1 |
| В9 | Міжпланова база розрахунку: чи потрібна можливість брати базу з іншого регістру розрахунку (Утримання ← ОсновніНарахування)? Якщо так — ФТ-25 і md_register потребують розширення полем base_register_id (див. Д.7.2) |
Методолог ЗП / архітектор | До Е7 |
| В10 | Чи прийнятна поведінка підпорядкованого РВ без контролю унікальності за (Період + Вимірювання)? (сумісність із 1С vs. захист від дублів — див. Д.3.2, антипатерн А-6) | Методолог | До Е2 |
| В11 | Конфігуратор: web (FastAPI + React) чи desktop (PySide6/Qt)? Макети розділу 6.8 намальовані як desktop | Замовник / UX | До Е1 |
| В12 | Ключове. Прикладний код — повноцінний Python чи обмежений DSL? Повний Python у процесі сервера = виконання довільного коду з правами сервера (ризик Р14) + неможливість статично перевірити конфігурацію партнера | Архітектор + служба безпеки | До Е0 |
| В13 | Чи допустимі нативні розширення (Cython/Rust) у складі постачання? Якщо ні — потрібен план «Б» для витіснення (SQL на daterange) |
ІТ-експлуатація | До Е7 |
| В14 | Цільова версія: 3.12 чи 3.13? Чи розглядається free-threaded build (PEP 703) як шлях зняття Р12? | Архітектор | До Е1 |
| В15 | Мова ідентифікаторів прикладного API: кирилиця (rs.new(Номенклатура=…)) чи транслітерація/англійська? Впливає на ФТ-К-07 і читабельність |
Замовник / методолог | До Е2 |
Додаток Д. Каталог прикладів регістрів
Додаток містить прикладні приклади регістрів усіх типів і підвидів. Для кожного наведено: декларацію (DSL), пояснення проєктних рішень, приклад даних у фізичних таблицях і типовий запит. Приклади призначені для: (а) перевірки повноти метамоделі на етапі Е0; (б) формування набору приймальних тестів; (в) навчання прикладних розробників.
Д.1. Як обрати тип регістру (дерево рішень)

Контрольні питання при проєктуванні будь-якого регістру:
- Чи можна відповісти на всі прикладні питання лише полями цього регістру, без з'єднання з документами? Якщо ні — бракує реквізиту (не вимірювання!).
- Яка кардинальність таблиці підсумків = ∏(кількість значень вимірювань) × кількість періодів? Якщо > 108 — переглянути склад вимірювань.
- Чи потрібно фільтрувати/групувати за цим полем у звітах? Так → вимірювання. Ні, лише показувати → реквізит.
- Чи підсумовується поле? Так → ресурс. Ні → реквізит або вимірювання.
Д.2. Зведений каталог
| № | Регістр | Тип | Підвид / режим | Вимірювання | Ресурси | Розділ |
|---|---|---|---|---|---|---|
| 1 | КурсиВалют | РВ | Періодичний (день), незалежний | Валюта | Курс, Кратність | Д.3.1 |
| 2 | ЦіниНоменклатури | РВ | Періодичний (день), підпорядкований | Номенклатура, Характеристика, ТипЦін | Ціна | Д.3.2 |
| 3 | ШтрихкодиНоменклатури | РВ | Неперіодичний, незалежний | Штрихкод | Номенклатура, Характеристика, Упаковка | Д.3.3 |
| 4 | ГрафікРоботи | РВ | Неперіодичний, незалежний | ВидГрафіка, ДатаГрафіка | Значення, Годин | Д.3.4 |
| 5 | КадровіДані | РВ | По позиції реєстратора, підпорядкований | Співробітник | Підрозділ, Посада, Ставка, ВидЗайнятості | Д.3.5 |
| 6 | НалаштуванняКористувачів | РВ | Неперіодичний, незалежний | Користувач, Налаштування | Значення (складений) | Д.3.6 |
| 7 | ТовариНаСкладах | РН | Залишки | Номенклатура, Склад, Партія, Характеристика | Кількість, Сума | Д.4.1 |
| 8 | ВзаєморозрахункиЗКонтрагентами | РН | Залишки | Організація, Контрагент, Договір, Замовлення | Сума, СумаВал | Д.4.2 |
| 9 | ТовариВРезерві | РН | Залишки | Номенклатура, Склад, Замовлення, Характеристика | Кількість | Д.4.3 |
| 10 | ГрошовіКоштиБезготівкові | РН | Залишки | Організація, БанківськийРахунок | Сума, СумаВал | Д.4.4 |
| 11 | Продажі | РН | Обороти (місяць) + агрегати | Номенклатура, Контрагент, Договір, Менеджер, Склад | Кількість, Сума, СумаБезПДВ, Собівартість | Д.5.1 |
| 12 | ВитратиЗаСтаттями | РН | Обороти (місяць) | Організація, Підрозділ, СтаттяВитрат, Проєкт | Сума | Д.5.2 |
| 13 | Госпрозрахунковий | РБ | З кореспонденцією | Організація (бал.), Підрозділ (небал.) | Сума (бал.), Кількість, ВалютнаСума | Д.6.1 |
| 14 | ПодатковийОблік | РБ | Без кореспонденції | Організація, ВидРізниці | Сума | Д.6.2 |
| 15 | Бюджетування | РБ | З кореспонденцією | Сценарій, ЦФВ, Проєкт | Сума | Д.6.3 |
| 16 | ОсновніНарахування | РР | Період дії + база за періодом дії | Співробітник (баз.), Підрозділ | Результат, ВідпрацьованоДнів, НормаДнів | Д.7.1 |
| 17 | Утримання | РР | Без періоду дії, база за періодом реєстрації | Співробітник (баз.), Підрозділ | Результат, БазаОподаткування | Д.7.2 |
Д.3. Регістри відомостей
Д.3.1. КурсиВалют — періодичний, незалежний
Задача: зберігати історію курсів валют; отримувати курс на будь-яку дату.
register:
name: КурсиВалют
kind: Information
synonym: "Курси валют"
periodicity: WithinDay # один курс на валюту на день
writeMode: Independent # курси вводяться вручну/завантажуються з НБУ
dimensions:
- { name: Валюта, type: "Довідник.Валюти", master: true, denyEmpty: true }
resources:
- { name: Курс, type: "Число(15,4)", denyEmpty: true }
- { name: Кратність, type: "Число(10,0)", default: 1 }
Проєктні рішення:
Independent— курс не породжується документом, він є зовнішнім фактом.master: trueдля Валюти — при видаленні валюти курси видаляються; регістр видно у формі елемента довідника.- Періодичність
WithinDay, а неWithinSecond— повторний запис курсу за той самий день замістить попередній, а не створить дубль.
Дані у таблиці rg_i_0007:
| period | d1_r (Валюта) | r1 (Курс) | r2 (Кратність) |
|---|---|---|---|
| 2026-07-15 00:00:00 | USD | 41.8200 | 1 |
| 2026-07-15 00:00:00 | EUR | 45.6100 | 1 |
| 2026-07-16 00:00:00 | USD | 41.8500 | 1 |
| 2026-07-17 00:00:00 | USD | 41.9000 | 1 |
| 2026-07-17 00:00:00 | EUR | 45.7300 | 1 |
Запит (курс на дату документа):
ВИБРАТИ Курси.Курс, Курси.Кратність
З РегістрВідомостей.КурсиВалют.ЗрізОстанніх(&ДатаДокумента, Валюта = &Валюта) ЯК Курси
# або через менеджер
курс = Registers.information("КурсиВалют").get_last(doc.Дата, {"Валюта": doc.Валюта})
сума_грн: Decimal = doc.СумаВалюти * курс.Курс / курс.Кратність
Д.3.2. ЦіниНоменклатури — періодичний, підпорядкований реєстратору
Задача: зберігати історію цін; ціна встановлюється наказом (документом), який можна скасувати.
register:
name: ЦіниНоменклатури
kind: Information
synonym: "Ціни номенклатури"
periodicity: WithinDay
writeMode: SubordinateToRecorder # ← ключова відмінність від КурсиВалют
dimensions:
- { name: Номенклатура, type: "Довідник.Номенклатура", master: true, index: true }
- { name: Характеристика, type: "Довідник.ХарактеристикиНоменклатури", nullable: true }
- { name: ТипЦін, type: "Довідник.ТипиЦін", master: true, denyEmpty: true }
resources:
- { name: Ціна, type: "Число(15,2)", denyEmpty: true }
attributes:
- { name: Валюта, type: "Довідник.Валюти" }
- { name: Одиниця, type: "Довідник.ОдиниціВиміру" }
recorders:
- Документ.ВстановленняЦінНоменклатури
Проєктні рішення:
SubordinateToRecorder— потрібна юридична прив'язка ціни до наказу, можливість скасувати наказ і «відкотити» ціни одним рухом.ВалютаіОдиниця— реквізити, а не вимірювання: за ними не фільтрують і не будують зрізи, вони визначаються типом цін. Помилка новачка — зробити Валюту вимірюванням, що подвоїть кількість записів і зламає зріз останніх.Характеристикадозволяє порожнє значення — для номенклатури без характеристик.
Дані у таблиці rg_i_0010:
| rec_t | rec_r | line_no | period | active | d1_r (Номенкл.) | d2_r (Характ.) | d3_r (ТипЦін) | r1 (Ціна) | a1_r (Валюта) |
|---|---|---|---|---|---|---|---|---|---|
| 15 | a3f…01 | 1 | 2026-07-01 | true | Кава Lavazza 1кг | (порожня) | Роздрібна | 480.00 | UAH |
| 15 | a3f…01 | 2 | 2026-07-01 | true | Кава Lavazza 1кг | (порожня) | Оптова | 410.00 | UAH |
| 15 | a3f…01 | 3 | 2026-07-01 | true | Чай Ahmad 100г | (порожня) | Роздрібна | 95.00 | UAH |
| 15 | b7c…04 | 1 | 2026-07-15 | true | Кава Lavazza 1кг | (порожня) | Роздрібна | 495.00 | UAH |
Д.3.3. ШтрихкодиНоменклатури — неперіодичний, незалежний
Задача: за штрихкодом знайти номенклатуру. Класичний приклад «вимірювання = те, за чим шукаємо; ресурс = те, що знаходимо».
register:
name: ШтрихкодиНоменклатури
kind: Information
periodicity: Nonperiodical
writeMode: Independent
dimensions:
- { name: Штрихкод, type: "Рядок(30)", denyEmpty: true } # ← ключ пошуку
resources:
- { name: Номенклатура, type: "Довідник.Номенклатура", denyEmpty: true }
- { name: Характеристика, type: "Довідник.ХарактеристикиНоменклатури", nullable: true }
- { name: Упаковка, type: "Довідник.УпаковкиНоменклатури", nullable: true }
CREATE TABLE rg_i_0011 (
d1_s varchar(30) NOT NULL, -- Штрихкод
r1_r uuid NOT NULL, -- Номенклатура
r2_r uuid NOT NULL, -- Характеристика
r3_r uuid NOT NULL, -- Упаковка
CONSTRAINT pk_rg_i_0011 PRIMARY KEY (d1_s) -- ← унікальність штрихкоду «безкоштовно»
);
CREATE INDEX ix_rg_i_0011_r1 ON rg_i_0011 (r1_r); -- зворотний пошук: штрихкоди товару
Проєктне рішення: PK за вимірюванням автоматично гарантує, що один штрихкод не може вказувати на два товари. Якби Номенклатуру зробили вимірюванням, а Штрихкод — ресурсом, ця гарантія зникла б, і довелося б писати перевірку в прикладному коді.
Д.3.4. ГрафікРоботи — неперіодичний, незалежний (використовується як графік для РР)
register:
name: ГрафікРоботи
kind: Information
periodicity: Nonperiodical
writeMode: Independent
dimensions:
- { name: ВидГрафіка, type: "Довідник.ГрафікиРоботи", master: true, denyEmpty: true }
- { name: ДатаГрафіка, type: "Дата", denyEmpty: true } # ← вимірювання типу Дата, не Період!
resources:
- { name: Значення, type: "Число(1,0)" } # 1 = робочий день, 0 = вихідний
- { name: Годин, type: "Число(4,2)" } # тривалість зміни
Проєктне рішення (важливе): ДатаГрафіка — це вимірювання типу Дата, а не стандартний реквізит Період. Регістр неперіодичний. Причина: графік описує календар (кожна дата — окремий факт), а не історію зміни значення. Якби використали періодичність, зріз останніх повертав би «останній робочий день», що безглуздо. Механізм РР (ДаніГрафіка) очікує саме таку структуру.
Дані у таблиці rg_i_0015:
| d1_r (ВидГрафіка) | d2_d (ДатаГрафіка) | r1 (Значення) | r2 (Годин) |
|---|---|---|---|
| П'ятиденка | 2026-07-01 | 1 | 8.00 |
| П'ятиденка | 2026-07-04 | 0 | 0.00 |
| П'ятиденка | 2026-07-05 | 0 | 0.00 |
| П'ятиденка | 2026-07-06 | 1 | 8.00 |
| Змінний 2/2 | 2026-07-01 | 1 | 12.00 |
| Змінний 2/2 | 2026-07-03 | 0 | 0.00 |
Д.3.5. КадровіДані — по позиції реєстратора
Задача: зберігати актуальні кадрові дані співробітника з історією; підтримати кілька наказів однією датою в правильному порядку.
register:
name: КадровіДані
kind: Information
periodicity: ByRecorderPosition # ← упорядкування за позицією документа, не лише за датою
writeMode: SubordinateToRecorder
dimensions:
- { name: Співробітник, type: "Довідник.Співробітники", master: true, denyEmpty: true }
resources:
- { name: Підрозділ, type: "Довідник.Підрозділи" }
- { name: Посада, type: "Довідник.Посади" }
- { name: Ставка, type: "Число(5,2)" }
- { name: ВидЗайнятості, type: "Перерахування.ВидиЗайнятості" }
- { name: ГрафікРоботи, type: "Довідник.ГрафікиРоботи" }
recorders:
- Документ.ПрийомНаРоботу
- Документ.КадровеПереміщення
- Документ.Звільнення
Проєктне рішення: ByRecorderPosition — коли два накази оформлені однією датою (переміщення + зміна ставки), звичайна періодичність «у межах дня» дала б недетермінований результат зрізу. Позиція реєстратора додає до ключа (rec_t, rec_r, line_no) і впорядковує за моментом часу документа.
Фізична схема:
CREATE TABLE rg_i_0016 (
period timestamp(0) NOT NULL,
rec_t smallint NOT NULL,
rec_r uuid NOT NULL,
line_no int NOT NULL,
active boolean NOT NULL DEFAULT true,
d1_r uuid NOT NULL, -- Співробітник
r1_r uuid NOT NULL, r2_r uuid NOT NULL,
r3 numeric(5,2) NOT NULL,
r4 smallint NOT NULL, -- Перерахування зберігається як порядковий номер
r5_r uuid NOT NULL,
CONSTRAINT pk_rg_i_0016 PRIMARY KEY (period, rec_t, rec_r, line_no) -- ← Період у ключі ПЕРШИЙ
);
CREATE INDEX ix_rg_i_0016_d1 ON rg_i_0016 (d1_r, period DESC, rec_t, rec_r, line_no DESC)
WHERE active;
Д.3.6. НалаштуванняКористувачів — складений тип ресурсу
register:
name: НалаштуванняКористувачів
kind: Information
periodicity: Nonperiodical
writeMode: Independent
dimensions:
- { name: Користувач, type: "Довідник.Користувачі", master: true }
- { name: Налаштування, type: "ПланВидівХарактеристик.НалаштуванняКористувачів", master: true }
resources:
- name: Значення
type: ["Рядок(500)", "Число(15,2)", "Дата", "Булево",
"Довідник.Організації", "Довідник.Склади"] # ← складений тип
CREATE TABLE rg_i_0018 (
d1_r uuid NOT NULL,
d2_r uuid NOT NULL,
r1_t smallint NOT NULL, -- код типу значення
r1_b boolean,
r1_n numeric(15,2),
r1_s varchar(500),
r1_d timestamp(0),
r1_r uuid,
CONSTRAINT pk_rg_i_0018 PRIMARY KEY (d1_r, d2_r)
);
| d1_r | d2_r | r1_t | r1_b | r1_n | r1_s | r1_d | r1_r |
|---|---|---|---|---|---|---|---|
| Іваненко | ОсновнаОрганізація | 101 | ТОВ «Альфа» | ||||
| Іваненко | ОсновнийСклад | 104 | Центральний | ||||
| Іваненко | ПоказуватиПідказки | 2 | true | ||||
| Петренко | КількістьРядківСписку | 3 | 50 |
Д.4. Регістри накопичення, вид «Залишки»
Д.4.1. ТовариНаСкладах
Декларація — розділ 6.7, фізична схема — розділ 8.5, ER — розділ 9.2. Тут — дані.
Рухи (rg_a_0012) після проведення трьох документів:
| rec_t | rec_r | line_no | period | mvt | d1_r (Номенкл.) | d2_r (Склад) | r1 (К-сть) | r2 (Сума) |
|---|---|---|---|---|---|---|---|---|
| 2 (ПрихНакл) | p1 | 1 | 2026-06-10 | 0 Прихід | Кава Lavazza | Центральний | 100.000 | 35 000.00 |
| 2 (ПрихНакл) | p1 | 2 | 2026-06-10 | 0 Прихід | Чай Ahmad | Центральний | 200.000 | 12 000.00 |
| 3 (ВидНакл) | v1 | 1 | 2026-07-05 | 1 Витрата | Кава Lavazza | Центральний | 30.000 | 10 500.00 |
| 4 (Переміщ) | m1 | 1 | 2026-07-12 | 1 Витрата | Кава Lavazza | Центральний | 20.000 | 7 000.00 |
| 4 (Переміщ) | m1 | 2 | 2026-07-12 | 0 Прихід | Кава Lavazza | Роздрібний | 20.000 | 7 000.00 |
Підсумки залишків (rg_a_0012_tb), ТА = 2026-07-31:
| period | d1_r | d2_r | r1 (К-сть ±) | r2 (Сума ±) | Коментар |
|---|---|---|---|---|---|
| 2026-07-01 | Кава Lavazza | Центральний | 100.000 | 35 000.00 | залишок на початок липня |
| 2026-07-01 | Чай Ahmad | Центральний | 200.000 | 12 000.00 | |
| 2026-08-01 | Кава Lavazza | Центральний | 50.000 | 17 500.00 | 100 − 30 − 20 |
| 2026-08-01 | Кава Lavazza | Роздрібний | 20.000 | 7 000.00 | |
| 2026-08-01 | Чай Ahmad | Центральний | 200.000 | 12 000.00 | |
| 5999-11-01 | Кава Lavazza | Центральний | 50.000 | 17 500.00 | MAXPERIOD — поточний залишок |
| 5999-11-01 | Кава Lavazza | Роздрібний | 20.000 | 7 000.00 | MAXPERIOD |
| 5999-11-01 | Чай Ahmad | Центральний | 200.000 | 12 000.00 | MAXPERIOD |
(Вимірювання Партія і Характеристика для стислості опущені.)
Д.4.2. ВзаєморозрахункиЗКонтрагентами
Задача: облік дебіторської/кредиторської заборгованості в розрізі договорів і замовлень.
register:
name: ВзаєморозрахункиЗКонтрагентами
kind: Accumulation
accumulationKind: Balance
periodicity: WithinSecond
enableBalanceTotals: true
enableTurnoverTotals: true
partitioning: { mode: RangeByPeriod, step: month }
dimensions:
- { name: Організація, type: "Довідник.Організації", denyEmpty: true, index: true }
- { name: Контрагент, type: "Довідник.Контрагенти", master: true, index: true }
- { name: Договір, type: "Довідник.ДоговориКонтрагентів", master: true, index: true }
- { name: Замовлення, type: ["Документ.ЗамовленняПокупця", "Документ.ЗамовленняПостачальнику"],
nullable: true }
resources:
- { name: Сума, type: "Число(15,2)" } # у валюті регламентованого обліку
- { name: СумаВал, type: "Число(15,2)" } # у валюті договору
attributes:
- { name: Коментар, type: "Рядок(200)" }
recorders:
- Документ.ВидатковаНакладна
- Документ.ПрихіднаНакладна
- Документ.ПлатіжнеДоручення
- Документ.КасовийОрдер
- Документ.КоригуванняБоргу
Угода про знак (обов'язково фіксується в методології!):
| Вид руху | Сенс | Приклад документа |
|---|---|---|
| Прихід | Борг контрагента перед нами зростає (дебіторка +) | Видаткова накладна |
| Витрата | Борг контрагента перед нами зменшується (оплата) | Платіжне доручення вхідне |
Кредиторська заборгованість = від'ємний залишок. Альтернатива — два окремих ресурси (СумаДт/СумаКт) або два регістри; обраний варіант — один знаковий ресурс (простіше, менший обсяг підсумків).
Приклад даних:
| Документ | period | mvt | Контрагент | Договір | r1 (Сума) | Залишок після |
|---|---|---|---|---|---|---|
| ВидатковаНакладна №12 | 2026-07-05 | Прихід | ТОВ «Бета» | Основний | 12 000.00 | +12 000 (винні нам) |
| ПлатіжнеДоручення №88 | 2026-07-09 | Витрата | ТОВ «Бета» | Основний | 8 000.00 | +4 000 |
| ПлатіжнеДоручення №91 | 2026-07-16 | Витрата | ТОВ «Бета» | Основний | 6 000.00 | −2 000 (передоплата) |
Запит (акт звірки):
ВИБРАТИ
Дані.Контрагент, Дані.Договір,
Дані.СумаПочатковийЗалишок, Дані.СумаПрихід,
Дані.СумаВитрата, Дані.СумаКінцевийЗалишок
З РегістрНакопичення.ВзаєморозрахункиЗКонтрагентами.ЗалишкиІОбороти(
&Початок, &Кінець, Авто, Рух,
Організація = &Організація І Контрагент = &Контрагент
) ЯК Дані
Д.4.3. ТовариВРезерві — регістр «супутник»
register:
name: ТовариВРезерві
kind: Accumulation
accumulationKind: Balance
periodicity: WithinSecond
dimensions:
- { name: Номенклатура, type: "Довідник.Номенклатура", master: true, index: true }
- { name: Характеристика, type: "Довідник.ХарактеристикиНоменклатури", nullable: true }
- { name: Склад, type: "Довідник.Склади", master: true, index: true }
- { name: Замовлення, type: "Документ.ЗамовленняПокупця", master: true, denyEmpty: true }
resources:
- { name: Кількість, type: "Число(15,3)" }
recorders:
- Документ.ЗамовленняПокупця
- Документ.ВидатковаНакладна
- Документ.ЗакриттяЗамовлень
Патерн «доступний залишок» — з'єднання двох регістрів:
ВИБРАТИ
ЄСТЬNULL(Зал.Номенклатура, Рез.Номенклатура) ЯК Номенклатура,
ЄСТЬNULL(Зал.Склад, Рез.Склад) ЯК Склад,
ЄСТЬNULL(Зал.КількістьЗалишок, 0) ЯК ВСкладі,
ЄСТЬNULL(Рез.КількістьЗалишок, 0) ЯК ВРезерві,
ЄСТЬNULL(Зал.КількістьЗалишок, 0) - ЄСТЬNULL(Рез.КількістьЗалишок, 0) ЯК Доступно
З РегістрНакопичення.ТовариНаСкладах.Залишки(&Період, Склад = &Склад) ЯК Зал
ПОВНЕ З'ЄДНАННЯ РегістрНакопичення.ТовариВРезерві.Залишки(&Період, Склад = &Склад) ЯК Рез
ЗА Зал.Номенклатура = Рез.Номенклатура І Зал.Склад = Рез.Склад
Д.4.4. ГрошовіКоштиБезготівкові
register:
name: ГрошовіКоштиБезготівкові
kind: Accumulation
accumulationKind: Balance
periodicity: WithinDay # достатньо дня: банківська виписка — денна
dimensions:
- { name: Організація, type: "Довідник.Організації", denyEmpty: true }
- { name: БанківськийРахунок, type: "Довідник.БанківськіРахунки", master: true, denyEmpty: true }
resources:
- { name: Сума, type: "Число(15,2)" }
- { name: СумаВал, type: "Число(15,2)" }
recorders: [Документ.ПлатіжнеДоручення, Документ.БанківськаВиписка, Документ.ПереміщенняКоштів]
Проєктне рішення: Валюта — не вимірювання, тому що вона однозначно визначається банківським рахунком (функціональна залежність). Її додавання як вимірювання створило б надлишковість і ризик неузгоджених даних. Валюта отримується з'єднанням із довідником.
Д.5. Регістри накопичення, вид «Обороти»
Д.5.1. Продажі — з агрегатами
register:
name: Продажі
kind: Accumulation
accumulationKind: Turnover # ← залишків не буває: «залишок продажів» безглуздий
periodicity: WithinSecond
turnoverTotalsPeriod: Month
aggregatesMode: Aggregates
partitioning: { mode: RangeByPeriod, step: month }
dimensions:
- { name: Номенклатура, type: "Довідник.Номенклатура", index: true }
- { name: Характеристика, type: "Довідник.ХарактеристикиНоменклатури", nullable: true }
- { name: Контрагент, type: "Довідник.Контрагенти", index: true }
- { name: Договір, type: "Довідник.ДоговориКонтрагентів" }
- { name: Менеджер, type: "Довідник.Користувачі", index: true }
- { name: Склад, type: "Довідник.Склади" }
resources:
- { name: Кількість, type: "Число(15,3)" }
- { name: Сума, type: "Число(15,2)" }
- { name: СумаБезПДВ, type: "Число(15,2)" }
- { name: Собівартість, type: "Число(15,2)" }
recorders: [Документ.ВидатковаНакладна, Документ.ПовернненняВідПокупця, Документ.ЗвітПроРоздрібніПродажі]
aggregates:
- name: ПродажіПоМісяцяхІТоварах
periodicity: Month
dimensions: [Номенклатура, Склад]
realtime: true
- name: ПродажіПоМенеджерах
periodicity: Day
dimensions: [Менеджер]
realtime: false # оновлюється регламентно
- name: ПродажіПоКонтрагентах
periodicity: Quarter
dimensions: [Контрагент, Договір]
realtime: false
Проєктні рішення:
Turnover, а неBalance: немаєВидРуху, немає таблиці залишків → удвічі менше підсумків і немає кумулятивних оновлень. Повернення відображається від'ємною сумою, а не «витратою».- 6 вимірювань → таблиця підсумків надто широка для швидких звітів → компенсуємо агрегатами.
Ефект агрегатів:
| Звіт | Без агрегатів (rg_a_0031_tt) |
З агрегатом | Виграш |
|---|---|---|---|
| Продажі по товарах за рік | скан 8 млн рядків підсумків | ag1: 40 тис. рядків |
×200 |
| Рейтинг менеджерів за квартал | скан 8 млн рядків | ag2: 1,8 тис. рядків |
×4000 |
| Продажі по контрагенту за 3 роки | скан 8 млн рядків | ag3: 60 тис. рядків |
×130 |
| Продажі по товару + контрагенту | агрегат не покриває → підсумки | — | — |
CREATE TABLE rg_a_0031_ag1 ( -- Місяць × (Номенклатура, Склад)
period timestamp(0) NOT NULL, d1_r uuid NOT NULL, d6_r uuid NOT NULL,
r1 numeric(20,3) NOT NULL, r2 numeric(20,2) NOT NULL,
r3 numeric(20,2) NOT NULL, r4 numeric(20,2) NOT NULL,
CONSTRAINT pk_rg_a_0031_ag1 PRIMARY KEY (period, d1_r, d6_r)
);
CREATE TABLE rg_a_0031_ag2 ( -- День × (Менеджер)
period timestamp(0) NOT NULL, d5_r uuid NOT NULL,
r1 numeric(20,3) NOT NULL, r2 numeric(20,2) NOT NULL,
r3 numeric(20,2) NOT NULL, r4 numeric(20,2) NOT NULL,
CONSTRAINT pk_rg_a_0031_ag2 PRIMARY KEY (period, d5_r)
);
Д.5.2. ВитратиЗаСтаттями
register:
name: ВитратиЗаСтаттями
kind: Accumulation
accumulationKind: Turnover
periodicity: WithinDay
turnoverTotalsPeriod: Month
dimensions:
- { name: Організація, type: "Довідник.Організації", denyEmpty: true }
- { name: Підрозділ, type: "Довідник.Підрозділи", index: true }
- { name: СтаттяВитрат, type: "Довідник.СтаттіВитрат", master: true, denyEmpty: true, index: true }
- { name: Проєкт, type: "Довідник.Проєкти", nullable: true }
resources:
- { name: Сума, type: "Число(15,2)" }
recorders: [Документ.АвансовийЗвіт, Документ.НадходженняПослуг, Документ.НарахуванняЗарплати, Документ.Амортизація]
Приклад даних (rg_a_0032_tt, підсумки оборотів за місяць):
| period | Підрозділ | СтаттяВитрат | Проєкт | r1 (Сума) |
|---|---|---|---|---|
| 2026-07-01 | Відділ продажів | Оренда | (порожній) | 45 000.00 |
| 2026-07-01 | Відділ продажів | ЗП і нарахування | (порожній) | 320 000.00 |
| 2026-07-01 | Відділ продажів | Реклама | Запуск «Альфа» | 78 000.00 |
| 2026-07-01 | ІТ-відділ | ЗП і нарахування | (порожній) | 410 000.00 |
| 2026-07-01 | ІТ-відділ | Ліцензії ПЗ | (порожній) | 62 000.00 |
Д.6. Регістри бухгалтерії
Д.6.1. Госпрозрахунковий — з кореспонденцією
Декларація — розділ 6.7, схема — 8.6, ER — 9.3. Тут — приклад проводок.
Господарська операція: реалізація товару на 12 000 грн (у т.ч. ПДВ 2 000), собівартість 7 000 грн.
Проводки (rg_b_0020, реєстратор = ВидатковаНакладна №12):
| line | acc_dt | ed_dt1 | ed_dt2 | acc_kt | ed_kt1 | ed_kt2 | r1 (Сума) | r2_dt | r2_kt |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 361 Розрах. з покупцями | ТОВ «Бета» | Договір №5 | 702 Дохід від реалізації | Кава Lavazza | — | 12 000.00 | 30.000 | |
| 2 | 702 Дохід від реалізації | Кава Lavazza | — | 641 Розрах. за податками | ПДВ | — | 2 000.00 | ||
| 3 | 902 Собівартість реалізації | Кава Lavazza | — | 281 Товари на складі | Кава Lavazza | Центральний | 7 000.00 | 30.000 | 30.000 |
Контроль: Σ(Сума) по Дт = Σ(Сума) по Кт у межах реєстратора → 12 000 + 2 000 + 7 000 = 21 000 з обох боків. ✔
Підсумки, рівень 0 (rg_b_0020_tb0), на 5999-11-01:
| acc_r | d1_r (Організація) | r1 (Сума ±) | Інтерпретація |
|---|---|---|---|
| 281 | ТОВ «Альфа» | +28 000.00 | дебетовий залишок (товари є) |
| 361 | ТОВ «Альфа» | +12 000.00 | дебетовий (дебіторка) |
| 641 | ТОВ «Альфа» | −2 000.00 | кредитовий (винні бюджету) |
| 702 | ТОВ «Альфа» | −10 000.00 | кредитовий (дохід) |
| 902 | ТОВ «Альфа» | +7 000.00 | дебетовий (собівартість) |
Підсумки, рівень 2 (rg_b_0020_tb2) — для рахунку 281 (2 субконто: Номенклатура, Склад):
| acc_r | ed1_r (Номенклатура) | ed2_r (Склад) | r1 (Сума) | r2 (Кількість) |
|---|---|---|---|---|
| 281 | Кава Lavazza | Центральний | 17 500.00 | 50.000 |
| 281 | Кава Lavazza | Роздрібний | 7 000.00 | 20.000 |
| 281 | Чай Ahmad | Центральний | 12 000.00 | 200.000 |
Згортка tb2 → tb0: 17 500 + 7 000 + 12 000 = 36 500… (різниця з таблицею вище — інші операції; у тесті ФТ-Т-08 перевіряється точна рівність).
Запит (ОСВ по рахунку):
ВИБРАТИ
Дані.Субконто1 ЯК Номенклатура, Дані.Субконто2 ЯК Склад,
Дані.СумаПочатковийЗалишокДт, Дані.КількістьПочатковийЗалишокДт,
Дані.СумаОборотДт, Дані.СумаОборотКт,
Дані.СумаКінцевийЗалишокДт, Дані.КількістьКінцевийЗалишокДт
З РегістрБухгалтерії.Госпрозрахунковий.ЗалишкиІОбороти(
&Початок, &Кінець, Місяць,
Рахунок = &Рахунок281,
, Організація = &Організація,
,
) ЯК Дані
Д.6.2. ПодатковийОблік — без кореспонденції
Задача: податковий облік із розділенням на постійні/тимчасові різниці, де подвійний запис не потрібен (кожен факт реєструється окремо по Дт або по Кт).
register:
name: ПодатковийОблік
kind: Accounting
chartOfAccounts: ПланРахунків.Податковий
correspondence: false # ← уніграфічний облік
periodicity: WithinSecond
dimensions:
- { name: Організація, type: "Довідник.Організації", balance: true, denyEmpty: true }
- { name: ВидРізниці, type: "Перерахування.ВидиРізниць", balance: true } # БУ|ПУ|ПР|ТР
resources:
- { name: Сума, type: "Число(15,2)", balance: true }
attributes:
- { name: Зміст, type: "Рядок(255)" }
recorders: [Документ.БухгалтерськаОперація, Документ.ВидатковаНакладна, Документ.НадходженняПослуг]
Проводки (rg_b_0021):
| line | acc_mvt | acc_r | ed1 (Номенкл.) | d1 (Організація) | d2 (ВидРізниці) | r1 (Сума) |
|---|---|---|---|---|---|---|
| 1 | 0 Дебет | 902.НУ | Кава Lavazza | ТОВ «Альфа» | ПУ податковий | 7 000.00 |
| 2 | 1 Кредит | 281.НУ | Кава Lavazza | ТОВ «Альфа» | ПУ | 7 000.00 |
| 3 | 0 Дебет | 902.НУ | Кава Lavazza | ТОВ «Альфа» | ПР постійна різниця | 350.00 |
| 4 | 1 Кредит | 281.НУ | Кава Lavazza | ТОВ «Альфа» | ПР | 350.00 |
Відмінності від Д.6.1:
| З кореспонденцією | Без кореспонденції | |
|---|---|---|
| Один факт = | 1 запис (Дт+Кт) | 2 записи (окремо Дт, окремо Кт) |
| Поля рахунків | acc_dt_r, acc_kt_r |
acc_r + acc_mvt
|
| Субконто | 2 набори (Дт, Кт) | 1 набір |
| Кількість записів | N | 2N |
Таблиця _tc (кореспонденції) |
Є | Немає — «шахова відомість» неможлива |
| Контроль Дт=Кт | У межах реєстратора | Опційно (може бути незбалансовано за задумом) |
| Коли обирати | Класичний облік, потрібен аналіз кореспонденцій | Облік без потреби в кореспонденціях, забалансовий, статистичний |
Д.6.3. Бюджетування
register:
name: Бюджетування
kind: Accounting
chartOfAccounts: ПланРахунків.Бюджетування
correspondence: true
periodicity: WithinMonth # бюджет — помісячний, до секунди не потрібно
dimensions:
- { name: Сценарій, type: "Довідник.СценаріїБюджетування", balance: true, denyEmpty: true }
- { name: ЦФВ, type: "Довідник.ЦентриФінансовоїВідповідальності", balance: true }
- { name: Проєкт, type: "Довідник.Проєкти", balance: true, nullable: true }
resources:
- { name: Сума, type: "Число(15,2)", balance: true }
recorders: [Документ.БюджетнаОперація, Документ.ВведенняБюджету]
Проєктне рішення: Сценарій — вимірювання, а не окремий регістр на кожен сценарій (План/Факт/Прогноз). Це дозволяє план-фактний аналіз одним запитом. Усі три вимірювання балансові — бюджетна проводка не змінює сценарій/ЦФВ між дебетом і кредитом.
Д.7. Регістри розрахунку
Д.7.1. ОсновніНарахування — період дії + витіснення
План видів розрахунку ОсновніНарахування:
| Вид розрахунку | Період дії | Витісняючі | Базові | Залежність від бази |
|---|---|---|---|---|
| Оклад | Так | Відпустка, Лікарняний | — | Не залежить |
| Відпустка | Так | Лікарняний | Оклад, Премія | За періодом дії |
| Лікарняний | Так | — | Оклад | За періодом дії |
| Премія | Ні | — | Оклад | За періодом реєстрації |
Декларація регістру — розділ 6.7.
Записи (rg_c_0040), період реєстрації = липень 2026:
| line | calc_type | period | act_begin | act_end | base_begin | base_end | d1 (Співробітник) | r1 (Результат) |
|---|---|---|---|---|---|---|---|---|
| 1 | Оклад | 2026-07-01 | 2026-07-01 | 2026-07-31 | Іваненко | 18 000.00 | ||
| 2 | Відпустка | 2026-07-01 | 2026-07-10 | 2026-07-17 | 2026-01-01 | 2026-06-30 | Іваненко | 6 420.00 |
| 3 | Лікарняний | 2026-07-01 | 2026-07-14 | 2026-07-22 | 2026-01-01 | 2026-06-30 | Іваненко | 5 180.00 |
| 4 | Оклад | 2026-07-01 | 2026-07-01 | 2026-07-31 | Петренко | 22 000.00 |
Фактичні періоди дії (rg_c_0040_ap) — результат витіснення:
| rec/line | interval_no | calc_type | d1 | fact_begin | fact_end | Коментар |
|---|---|---|---|---|---|---|
| n1 / 1 | 0 | Оклад | Іваненко | 2026-07-01 | 2026-07-09 | до відпустки |
| n1 / 1 | 1 | Оклад | Іваненко | 2026-07-23 | 2026-07-31 | після лікарняного |
| n1 / 2 | 0 | Відпустка | Іваненко | 2026-07-10 | 2026-07-13 | витіснена лікарняним з 14-го |
| n1 / 3 | 0 | Лікарняний | Іваненко | 2026-07-14 | 2026-07-22 | не витісняється нічим |
| n1 / 4 | 0 | Оклад | Петренко | 2026-07-01 | 2026-07-31 | витіснення відсутнє |
Один запис «Оклад» Іваненка → два інтервали — ілюстрація зв'язку 1:N у розділі 9.4.
Запит (дані графіка для розрахунку окладу):
ВИБРАТИ
Дані.Співробітник, Дані.НомерРядка,
Дані.ЗначенняФактичногоПеріодуДії ЯК ВідпрацьованоДнів,
Дані.ЗначенняПеріодуДії ЯК НормаДнів
З РегістрРозрахунку.ОсновніНарахування.ДаніГрафіка(
ВидРозрахунку = ЗНАЧЕННЯ(ПланВидівРозрахунку.ОсновніНарахування.Оклад)
І Реєстратор = &Реєстратор
) ЯК Дані
Результат: Іваненко — ВідпрацьованоДнів = 13 (робочі дні у 01–09 та 23–31), НормаДнів = 23 → Оклад = 18 000 × 13 / 23 = 10 173,91.
Запит (база для відпустки):
ВИБРАТИ База.Співробітник, База.НомерРядка, База.РезультатБаза ЯК Заробіток
З РегістрРозрахунку.ОсновніНарахування.БазаРозрахунку(
, /* вимірювання бази */
, /* вимірювання основного регістру */
Співробітник, /* розрізи */
ВидРозрахунку = ЗНАЧЕННЯ(ПланВидівРозрахунку.ОсновніНарахування.Відпустка)
) ЯК База
Д.7.2. Утримання — без періоду дії, база за періодом реєстрації
План видів розрахунку Утримання:
| Вид розрахунку | Період дії | Базові види розрахунку | Залежність |
|---|---|---|---|
| ПДФО | Ні | (усі з ПВР ОсновніНарахування) | За періодом реєстрації |
| Військовий збір | Ні | (усі з ПВР ОсновніНарахування) | За періодом реєстрації |
| Аліменти | Ні | Оклад, Премія, Відпустка | За періодом реєстрації |
| Профспілковий внесок | Ні | Оклад | За періодом реєстрації |
register:
name: Утримання
kind: Calculation
calculationTypesPlan: ПланВидівРозрахунку.Утримання
periodicity: Month
actionPeriod: false # ← утримання не має періоду дії
basePeriod: ByRegistrationPeriod # ← база береться за періодом реєстрації
# schedule: не задається — графік не потрібен
dimensions:
- { name: Співробітник, type: "Довідник.Співробітники", baseDimension: true, master: true, denyEmpty: true }
- { name: Підрозділ, type: "Довідник.Підрозділи" }
resources:
- { name: Результат, type: "Число(15,2)" }
- { name: БазаОподаткування, type: "Число(15,2)" }
recalculations:
- name: ПерерахунокУтримань
dimensions:
- { name: Співробітник, registerDimension: Співробітник }
recorders: [Документ.НарахуванняЗарплати]
Фізична схема (спрощена — немає полів періоду дії):
CREATE TABLE rg_c_0041 (
rec_t smallint NOT NULL,
rec_r uuid NOT NULL,
line_no int NOT NULL,
period timestamp(0) NOT NULL, -- період реєстрації
active boolean NOT NULL DEFAULT true,
reversal boolean NOT NULL DEFAULT false,
calc_type_r uuid NOT NULL,
base_begin timestamp(0) NOT NULL, -- ← є, бо basePeriod ≠ None
base_end timestamp(0) NOT NULL,
-- act_begin / act_end ВІДСУТНІ (actionPeriod: false)
d1_r uuid NOT NULL,
d2_r uuid NOT NULL,
r1 numeric(15,2) NOT NULL DEFAULT 0,
r2 numeric(15,2) NOT NULL DEFAULT 0,
CONSTRAINT pk_rg_c_0041 PRIMARY KEY (rec_t, rec_r, line_no)
);
-- Таблиця rg_c_0041_ap НЕ створюється (немає періоду дії → немає витіснення)
CREATE TABLE rg_c_0041_rc1 ( -- ПерерахунокУтримань
rec_t smallint NOT NULL, rec_r uuid NOT NULL,
calc_type_r uuid NOT NULL, d1_r uuid NOT NULL,
src_rec_t smallint NOT NULL, src_rec_r uuid NOT NULL,
created_at timestamp(0) NOT NULL DEFAULT now(),
CONSTRAINT pk_rg_c_0041_rc1 PRIMARY KEY (rec_t, rec_r, calc_type_r, d1_r)
);
Приклад даних:
| line | calc_type | period | base_begin | base_end | Співробітник | r2 (База) | r1 (Результат) |
|---|---|---|---|---|---|---|---|
| 1 | ПДФО | 2026-07-01 | 2026-07-01 | 2026-07-31 | Іваненко | 21 773.91 | 3 919.30 |
| 2 | Військовий збір | 2026-07-01 | 2026-07-01 | 2026-07-31 | Іваненко | 21 773.91 | 326.61 |
| 3 | Аліменти | 2026-07-01 | 2026-07-01 | 2026-07-31 | Іваненко | 17 528.00 | 4 382.00 |
Д.8. Наскрізний приклад: проведення видаткової накладної
Документ: Видаткова накладна №12 від 05.07.2026, ТОВ «Бета», 30 кг кави за 400 грн (12 000 грн з ПДВ), собівартість 7 000 грн.

class ВидатковаНакладна(Document):
"""Проведення по 5 регістрах в одній транзакції. Бюджет Python: <= 15 мс (АП-4)."""
def on_posting(self, ctx: PostingContext) -> None:
# --- 0. Блокування перед контролем залишків ---
with DataLock() as dl:
li = dl.add("РегістрНакопичення.ТовариНаСкладах.Залишки")
li.mode = DataLockMode.EXCLUSIVE
li.data_source = self.Товари.unload("Номенклатура", "Характеристика")
li.use_from_data_source("Номенклатура", "Номенклатура")
li.use_from_data_source("Характеристика", "Характеристика")
li.set_value("Склад", self.Склад)
self._movements_goods() # 1. ТовариНаСкладах
self._movements_ar() # 2. Взаєморозрахунки
self._movements_sales() # 3. Продажі
self._movements_reserve() # 4. ТовариВРезерві
self._movements_accounting() # 5. Госпрозрахунковий
self.movements.ТовариНаСкладах.write_now()
self._control_negative_balances(ctx)
# --- 1. РН ТовариНаСкладах: списання ---
def _movements_goods(self) -> None:
rs = self.movements.ТовариНаСкладах
rs.write = True
rs.clear()
rs.extend(
rs.new(Період=self.Дата,
ВидРуху=AccumulationMovementType.EXPENSE,
Номенклатура=r.Номенклатура, Характеристика=r.Характеристика,
Склад=self.Склад, Партія=r.Партія,
Кількість=r.Кількість, Сума=r.Собівартість)
for r in self.Товари
)
# --- 2. РН Взаєморозрахунки: дебіторка зростає ---
def _movements_ar(self) -> None:
rs = self.movements.ВзаєморозрахункиЗКонтрагентами
rs.write = True
rs.clear()
rs.append(rs.new(
Період=self.Дата,
ВидРуху=AccumulationMovementType.RECEIPT, # Прихід = борг зростає
Організація=self.Організація, Контрагент=self.Контрагент,
Договір=self.Договір, Замовлення=self.ЗамовленняПокупця,
Сума=self.СумаДокумента, СумаВал=self.СумаДокументаВал,
))
# --- 3. РН Продажі: оборот (ВидРуху відсутній!) ---
def _movements_sales(self) -> None:
rs = self.movements.Продажі
rs.write = True
rs.clear()
rs.extend(
rs.new(Період=self.Дата,
Номенклатура=r.Номенклатура, Контрагент=self.Контрагент,
Договір=self.Договір, Менеджер=self.Менеджер, Склад=self.Склад,
Кількість=r.Кількість, Сума=r.Сума,
СумаБезПДВ=r.СумаБезПДВ, Собівартість=r.Собівартість)
for r in self.Товари
)
# --- 4. РН ТовариВРезерві: зняття резерву ---
def _movements_reserve(self) -> None:
if self.ЗамовленняПокупця.is_empty():
return
rs = self.movements.ТовариВРезерві
rs.write = True
rs.clear()
rs.extend(
rs.new(Період=self.Дата, ВидРуху=AccumulationMovementType.EXPENSE,
Номенклатура=r.Номенклатура, Характеристика=r.Характеристика,
Склад=self.Склад, Замовлення=self.ЗамовленняПокупця,
Кількість=r.Кількість)
for r in self.Товари
)
# --- 5. РБ Госпрозрахунковий: 3 проводки на рядок ---
def _movements_accounting(self) -> None:
rs = self.movements.Госпрозрахунковий
rs.write = True
rs.clear()
for r in self.Товари:
e1 = rs.add_accounting_entry( # Дт 361 / Кт 702
Період=self.Дата, Організація=self.Організація,
РахунокДт=Accounts["361"], РахунокКт=Accounts["702"],
Сума=r.Сума, КількістьКт=r.Кількість)
e1.subconto_dt[ВидиСубконто.Контрагенти] = self.Контрагент
e1.subconto_dt[ВидиСубконто.Договори] = self.Договір
e1.subconto_kt[ВидиСубконто.Номенклатура] = r.Номенклатура
e2 = rs.add_accounting_entry( # Дт 702 / Кт 641 (ПДВ)
Період=self.Дата, Організація=self.Організація,
РахунокДт=Accounts["702"], РахунокКт=Accounts["641"],
Сума=r.СумаПДВ)
e2.subconto_dt[ВидиСубконто.Номенклатура] = r.Номенклатура
e2.subconto_kt[ВидиСубконто.ПодаткиІЗбори] = Taxes.ПДВ
e3 = rs.add_accounting_entry( # Дт 902 / Кт 281
Період=self.Дата, Організація=self.Організація,
РахунокДт=Accounts["902"], РахунокКт=Accounts["281"],
Сума=r.Собівартість,
КількістьДт=r.Кількість, КількістьКт=r.Кількість)
e3.subconto_dt[ВидиСубконто.Номенклатура] = r.Номенклатура
e3.subconto_kt[ВидиСубконто.Номенклатура] = r.Номенклатура
e3.subconto_kt[ВидиСубконто.Склади] = self.Склад
Що відбувається у БД (одна транзакція):
| Крок | Таблиця | Операція | Рядків |
|---|---|---|---|
| 1 | doc_0003 |
UPDATE posted = true | 1 |
| 2 | rg_a_0012 (ТовариНаСкладах) |
DELETE + INSERT | 1 |
| 3 | rg_a_0012_tb |
MERGE (MAXPERIOD + періоди > 07.2026) | 2 |
| 4 | rg_a_0013 (Взаєморозрахунки) |
DELETE + INSERT | 1 |
| 5 | rg_a_0013_tb |
MERGE | 2 |
| 6 | rg_a_0031 (Продажі) |
DELETE + INSERT | 1 |
| 7 | rg_a_0031_tt |
MERGE (тільки період 2026-07) | 1 |
| 8 | rg_a_0031_ag1 |
MERGE (realtime-агрегат) | 1 |
| 9 | rg_a_0014 (Резерв) |
DELETE + INSERT | 1 |
| 10 | rg_b_0020 (проводки) |
DELETE + INSERT | 3 |
| 11 | rg_b_0020_tb0 |
MERGE | 10 |
| 12 | rg_b_0020_tb1/tb2 |
MERGE | 8 |
| 13 | rg_b_0020_tt0..tt2 |
MERGE | 12 |
| 14 | rg_b_0020_tc |
MERGE | 3 |
| Разом | ≈ 47 рядків, 1 COMMIT | ||
Д.9. Наскрізний приклад: розрахунок зарплати
КРОК 1. Документ «Нарахування зарплати» за липень 2026, Іваненко.
Записуємо рухи в РР ОсновніНарахування (Результат = 0).
КРОК 2. Платформа автоматично будує фактичні періоди дії (rg_c_0040_ap):
Оклад → [01.07–09.07], [23.07–31.07]
Відпустка → [10.07–13.07]
Лікарняний → [14.07–22.07]
КРОК 3. Розрахунок «первинних» видів (без бази) — Оклад.
Запит до ДаніГрафіка:
НормаДнів = 23 (робочих днів у липні за П'ятиденкою)
ВідпрацьованоДнів = 13 (робочі дні у фактичних періодах дії)
Результат = 18 000 × 13 / 23 = 10 173,91
→ UPDATE rg_c_0040 SET r1 = 10173.91, r2 = 13, r3 = 23 WHERE line_no = 1
КРОК 4. Розрахунок видів із базою — Відпустка, Лікарняний.
Запит до БазаРозрахунку (базовий період 01.01–30.06, база = Оклад+Премія):
Заробіток за 6 міс. = 115 560,00
Календарних днів = 181
Середньоденний = 638,45
Відпустка: 638,45 × 4 дні (10–13.07) = 2 553,80
Лікарняний: 638,45 × 9 днів × 100 % = 5 746,05
→ UPDATE rg_c_0040 SET r1 = ... WHERE line_no IN (2, 3)
КРОК 5. Записуємо рухи в РР Утримання (базовий період = період реєстрації, липень).
База ПДФО = 10 173,91 + 2 553,80 + 5 746,05 = 18 473,76
ПДФО = 18 473,76 × 18 % = 3 325,28
ВЗ = 18 473,76 × 1,5 % = 277,11
КРОК 6. Проведення документа → рухи в:
РН ВитратиЗаСтаттями (Обороти): Стаття «ЗП і нарахування» = 18 473,76
РБ Госпрозрахунковий: Дт 92 / Кт 661 = 18 473,76
Дт 661 / Кт 641.ПДФО = 3 325,28
Дт 661 / Кт 642.ВЗ = 277,11
КРОК 7. Через тиждень бухгалтер змінює Оклад Іваненка за червень (заднім числом).
→ Платформа реєструє в rg_c_0040_rc1 (ПерерахунокЗаБазою):
(Реєстратор = НарахуванняЗП липня, ВидРозрахунку = Відпустка, Співробітник = Іваненко)
(Реєстратор = НарахуванняЗП липня, ВидРозрахунку = Лікарняний, Співробітник = Іваненко)
→ Звіт «Документи до перерахунку» показує липневий документ.
→ Бухгалтер перепроводить → база перераховується → записи перерахунку очищаються.
Д.10. Антипатерни проєктування регістрів
| ID | Антипатерн | Чому погано | Як правильно |
|---|---|---|---|
| А-1 | Дата як вимірювання у РН (замість стандартного Період) |
Таблиця підсумків множиться на кількість дат і зростає нескінченно; віртуальні таблиці Залишки/Обороти ігнорують це вимірювання і дають безглузді результати |
Використовувати стандартний реквізит Період. Виняток — календарі (Д.3.4), де це РВ, а не РН
|
| А-2 | РН «Залишки» там, де потрібна лише історія значення | Надлишкові таблиці підсумків, кумулятивні оновлення, вид руху без сенсу | РВ періодичний |
| А-3 | РН «Залишки» там, де залишок безглуздий (Продажі, Витрати) | Удвічі більший обсяг підсумків, кумулятивні MERGE на всі майбутні періоди | РН «Обороти» + агрегати (Д.5.1) |
| А-4 | Реквізит документа продубльовано у вимірювання «щоб фільтрувати у звіті» | Кардинальність підсумків × N; типовий випадок — Менеджер/Валюта/Коментар | Реквізит регістру + з'єднання з документом у звіті; або окремий регістр «Обороти» |
| А-5 | > 8 вимірювань в одному РН | Таблиця підсумків стає більшою за таблицю рухів; проведення сповільнюється в рази | Розділити на регістри за призначенням; частину аналітики — в «Обороти» з агрегатами |
| А-6 | Незалежний РВ «у межах секунди» як журнал подій | Два записи в ту саму секунду → другий мовчки замістить перший; втрата даних | Підпорядкований РВ (реєстратор у ключі) або додати вимірювання-дискримінатор |
| А-7 | Ресурс типу Рядок/Посилання у РН |
Підсумовування неможливе; підсумки не побудуються | Реквізит (не агрегується) або вимірювання (якщо це розріз) |
| А-8 | Прямий UPDATE таблиці підсумків із прикладного коду |
Розсинхронізація підсумків і рухів; діагностика — тижні | Тільки через набори записів. Заборона на рівні прав БД (розділ 12.2) |
| А-9 | Один «універсальний» регістр із вимірюванням ВидОбліку (БУ/ПУ/УО) замість трьох |
Втричі більша таблиця; конкуренція за блокування між незалежними обліками; неможливо окремо перерахувати підсумки | Окремі регістри. Виняток — РБ ПодатковийОблік (Д.6.2), де ВидРізниці — це справді розріз одного обліку
|
| А-10 | Функціонально залежне поле як вимірювання (Валюта при БанківськомуРахунку) | Надлишковість; ризик неузгоджених даних; зайвий розмір ключа підсумків | З'єднання з довідником у запиті (Д.4.4) |
| А-11 | РБ там, де достатньо РН | Замість 1 таблиці — 10; ~70 % вартості проведення (Д.8); складність субконто без потреби | РН. РБ — лише коли потрібні план рахунків, подвійний запис і кореспонденції |
| А-12 | Вимірювання з nullable без потреби |
Порожнє посилання — окреме значення ключа; підсумки «розмазуються» на два рядки замість одного | denyEmpty: true скрізь, де порожнє значення не має сенсу
|
| А-13 | Контроль залишків до запису рухів | Класична гонитва: два сеанси проходять контроль і обидва списують у мінус | Спершу DataLock → WriteNow() → потім контроль (Д.8, ФТ-Т-12)
|
| А-14 | periodicity: WithinSecond «про всяк випадок» скрізь |
Для РВ — неможливість замістити запис за день; для РН — марна точність, більші індекси | Обирати мінімально достатню: день для цін/курсів/виписок, секунда — для складських рухів |
| А-15 (Python) |
Запис рухів у циклі: for r in Товари: rs.add(...).write() |
Порушення АП-1: 100 round-trip'ів замість 1; проведення 300 мс → ~10 с. Невидиме на тестових даних, катастрофічне на продуктиві. Найчастіша помилка при ORM-мисленні | Наповнити набір повністю (rs.extend(генератор)), записати один раз. Детектор — Додаток Е.7
|
| А-16 (Python) |
Агрегація дельт підсумків у Python (defaultdict + цикл) замість MERGE |
Порушення АП-2: дані ганяються в застосунок і назад; на 105 рядків — десятки секунд | INSERT … ON CONFLICT DO UPDATE одним запитом (Е.3)
|
Д.11. Чек-лист рев'ю нового регістру
□ Тип регістру обґрунтований за деревом Д.1 □ Кожне вимірювання використовується у фільтрі АБО групуванні звітів □ Кожен ресурс має сенс при підсумовуванні □ Немає полів з антипатернів А-1, А-4, А-7, А-10, А-12 □ Оцінка кардинальності підсумків ≤ 10^8 (калькулятор конструктора, ФТ-К-03) □ Для вимірювань, де порожнє значення безглузде, стоїть denyEmpty □ Ведучі (master) вимірювання визначені — інакше «висячі» записи при видаленні об'єктів □ Склад реєстраторів повний; зайвих типів документів немає □ Угода про знак / вид руху зафіксована в методологічній документації □ Періодичність мінімально достатня (А-14) □ Для РН «Обороти» з >4 вимірюваннями визначено ≥1 агрегат □ Для РБ: балансові/небалансові вимірювання та ресурси розставлені свідомо □ Для РР: витісняючі та базові види розрахунку в ПВР заповнені; перерахунки визначені □ Партиціонування ввімкнене, якщо очікується >100 млн записів □ Написано тест на проведення + скасування + перерахунок підсумків
Додаток Е. Ескізи реалізації ядра на Python
Додаток демонструє, що метамодель розділу 6 і фізична модель розділу 8 реалізовні обраним стеком (розділ 4.3), і фіксує ключові технічні рішення для етапів Е1, Е3, Е5, Е7, Е11. Код — ілюстративний ескіз, а не остаточний API.
Е.1. Маніфест регістру як Pydantic-модель (етап Е1)
YAML-декларації розділу 6.7 і Додатка Д валідуються цією моделлю. Це безпосередньо реалізує ФТ-К-01 (валідація в реальному часі) і частину контролю антипатернів Д.10.
from __future__ import annotations
import warnings
from enum import StrEnum
from typing import Annotated, Literal, Self
from pydantic import BaseModel, ConfigDict, Field, model_validator
IDENT = r"^[^\W\d]\w{0,79}$" # Unicode-ідентифікатор (кирилиця дозволена), <= 80 симв.
class RegisterKind(StrEnum):
INFORMATION = "Information"
ACCUMULATION = "Accumulation"
ACCOUNTING = "Accounting"
CALCULATION = "Calculation"
class Periodicity(StrEnum):
NONPERIODICAL = "Nonperiodical"
WITHIN_SECOND = "WithinSecond"
WITHIN_DAY = "WithinDay"
WITHIN_MONTH = "WithinMonth"
WITHIN_QUARTER = "WithinQuarter"
WITHIN_YEAR = "WithinYear"
BY_RECORDER_POSITION = "ByRecorderPosition"
class WriteMode(StrEnum):
INDEPENDENT = "Independent"
SUBORDINATE = "SubordinateToRecorder"
class FieldRole(StrEnum):
DIMENSION = "Dimension"
RESOURCE = "Resource"
ATTRIBUTE = "Attribute"
class RegisterField(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
name: Annotated[str, Field(pattern=IDENT)]
role: FieldRole
type: str | list[str] # "Довідник.Номенклатура" | ["Документ.А", "Документ.Б"]
synonym: str | None = None
index: bool = False
master: bool = False # «Ведуче»
deny_empty: bool = False
nullable: bool = False
use_in_totals: bool = True
# РБ
balance: bool = False
accounting_flag: str | None = None
# РР
base_dimension: bool = False
@property
def is_composite(self) -> bool:
return isinstance(self.type, list) and len(self.type) > 1
class Register(BaseModel):
model_config = ConfigDict(extra="forbid")
name: Annotated[str, Field(pattern=IDENT)]
kind: RegisterKind
synonym: str | None = None
periodicity: Periodicity = Periodicity.NONPERIODICAL
write_mode: WriteMode = WriteMode.SUBORDINATE
dimensions: list[RegisterField] = []
resources: list[RegisterField] = []
attributes: list[RegisterField] = []
recorders: list[str] = []
# РН
accumulation_kind: Literal["Balance", "Turnover"] | None = None
enable_balance_totals: bool = True
enable_turnover_totals: bool = False
turnover_totals_period: Literal["Day", "Month"] | None = None
# РБ
chart_of_accounts: str | None = None
correspondence: bool = True
max_extdim_count: Annotated[int, Field(ge=0, le=5)] = 3
# РР
calculation_types_plan: str | None = None
action_period: bool = False
base_period: Literal["None", "ByActionPeriod", "ByRegistrationPeriod"] = "None"
# ---------- крос-полева валідація (розділи 6.2–6.6) ----------
@model_validator(mode="after")
def _validate(self) -> Self:
if self.write_mode is WriteMode.INDEPENDENT and self.kind is not RegisterKind.INFORMATION:
raise ValueError("Незалежний режим запису дозволено лише для регістру відомостей")
if self.kind is RegisterKind.ACCUMULATION:
if self.accumulation_kind is None:
raise ValueError("Для регістру накопичення обов'язковий accumulation_kind")
if self.periodicity is Periodicity.NONPERIODICAL:
raise ValueError("Регістр накопичення не може бути неперіодичним")
if self.accumulation_kind == "Turnover" and self.enable_balance_totals:
raise ValueError("Підсумки залишків несумісні з видом «Обороти»")
if self.kind is RegisterKind.ACCOUNTING and not self.chart_of_accounts:
raise ValueError("Для регістру бухгалтерії обов'язковий chart_of_accounts")
if self.kind is RegisterKind.CALCULATION:
if not self.calculation_types_plan:
raise ValueError("Для регістру розрахунку обов'язковий calculation_types_plan")
if not any(f.base_dimension for f in self.dimensions) and self.base_period != "None":
raise ValueError("Задано base_period, але жодне вимірювання не позначене base_dimension")
# ресурс має бути підсумовуваним (антипатерн А-7)
if self.kind is not RegisterKind.INFORMATION:
for r in self.resources:
if not _is_numeric(r.type):
raise ValueError(f"Ресурс «{r.name}»: тип {r.type} не підсумовується (А-7). "
f"Використайте роль Attribute або Dimension")
# обмеження розділу 6.6
if len(self.dimensions) > 20:
raise ValueError(f"Вимірювань {len(self.dimensions)} > 20 (розділ 6.6)")
# попередження (ФТ-К-01: warnings, а не помилки)
if len(self.dimensions) > 8:
warnings.warn(f"«{self.name}»: {len(self.dimensions)} вимірювань — "
f"ризик розростання таблиці підсумків (антипатерн А-5)")
for d in self.dimensions:
if d.type == "Дата" and self.kind is RegisterKind.ACCUMULATION:
warnings.warn(f"«{self.name}.{d.name}»: Дата як вимірювання РН — антипатерн А-1")
return self
Що це дає безкоштовно: валідацію YAML, JSON Schema для web-конструктора (Register.model_json_schema()), типізовані об'єкти для генератора DDL, детальні повідомлення про помилки з шляхом до поля.
Е.2. Генератор DDL на SQLAlchemy Core (етап Е1)
from collections.abc import Callable, Sequence
from sqlalchemy import (Boolean, Column, DateTime, Index, Integer, MetaData, Numeric,
PrimaryKeyConstraint, SmallInteger, String, Table, Uuid, text)
from sqlalchemy.types import TypeEngine
PRIMITIVE: dict[str, Callable[[RegisterField], TypeEngine]] = {
"Число": lambda f: Numeric(f.precision, f.scale),
"Рядок": lambda f: String(f.length),
"Дата": lambda f: DateTime(timezone=False),
"Булево": lambda f: Boolean(),
"УНІ": lambda f: Uuid(),
}
def columns_for_field(f: RegisterField, prefix: str) -> list[Column]:
"""Одне поле метаданих -> 1..6 фізичних колонок (розділ 8.2)."""
if f.is_composite: # складений тип: розділ 8.2.2
return [
Column(f"{prefix}_t", SmallInteger, nullable=False),
Column(f"{prefix}_b", Boolean),
Column(f"{prefix}_n", Numeric(38, 10)),
Column(f"{prefix}_s", String(f.max_string_len)),
Column(f"{prefix}_d", DateTime),
Column(f"{prefix}_r", Uuid),
]
if f.is_reference: # просте посилання
return [Column(f"{prefix}_r", Uuid, nullable=f.nullable)]
base = f.base_type
return [Column(prefix, PRIMITIVE[base](f), nullable=f.nullable)]
def build_movement_table(reg: Register, md: MetaData) -> Table:
"""Метаопис -> Table. Розділи 8.4–8.7."""
cols: list[Column] = []
if reg.write_mode is WriteMode.SUBORDINATE:
cols += [
Column("rec_t", SmallInteger, nullable=False),
Column("rec_r", Uuid, nullable=False),
Column("line_no", Integer, nullable=False),
Column("active", Boolean, nullable=False, server_default=text("true")),
]
if reg.periodicity is not Periodicity.NONPERIODICAL:
cols.append(Column("period", DateTime, nullable=False))
if reg.kind is RegisterKind.ACCUMULATION and reg.accumulation_kind == "Balance":
cols.append(Column("mvt", SmallInteger, nullable=False)) # ВидРуху
if reg.kind is RegisterKind.CALCULATION:
cols += [Column("calc_type_r", Uuid, nullable=False),
Column("reversal", Boolean, nullable=False, server_default=text("false"))]
if reg.action_period:
cols += [Column("act_begin", DateTime, nullable=False),
Column("act_end", DateTime, nullable=False)]
if reg.base_period != "None":
cols += [Column("base_begin", DateTime), Column("base_end", DateTime)]
for f in reg.all_fields: # dimensions + resources + attributes
cols += columns_for_field(f, reg.column_prefix(f)) # 'd3', 'r1', 'a2'
t = Table(reg.physical_table, md, *cols,
PrimaryKeyConstraint(*primary_key_columns(reg),
name=f"pk_{reg.physical_table}"),
postgresql_partition_by=partition_clause(reg))
for f in reg.dimensions:
if f.index:
Index(f"ix_{reg.physical_table}_{reg.column_prefix(f)}",
*index_columns(reg, f),
postgresql_where=t.c.active if "active" in t.c else None)
return t
НФВ-16 / НФВ-17 (дві СКБД, ізольований шар діалекту) — у три рядки:
from sqlalchemy.dialects import mssql, postgresql
from sqlalchemy.schema import CreateTable
md = MetaData()
t = build_movement_table(reg_goods, md)
print(CreateTable(t).compile(dialect=postgresql.dialect())) # -> CREATE TABLE rg_a_0012 (...)
print(CreateTable(t).compile(dialect=mssql.dialect())) # -> той самий об'єкт у T-SQL
Реалізація ФТ-К-02 («Показати SQL» у конструкторі) — той самий виклик CreateTable(t).compile(...), виведений у UI.
Е.3. Інкрементальне оновлення підсумків (етап Е3)
Реалізація алгоритму 10.2 з дотриманням АП-1 (один запит на весь набір) і ФТ-34 (детермінований порядок проти дедлоків).
from operator import itemgetter
from collections.abc import Sequence
from sqlalchemy import Connection, Table
from sqlalchemy.dialects.postgresql import insert as pg_insert
def merge_totals(conn: Connection, totals: Table, rows: list[dict],
key: Sequence[str], res: Sequence[str]) -> None:
"""АП-1 + АП-2: одна операція на весь набір дельт; складання виконує СКБД."""
if not rows:
return
rows.sort(key=itemgetter(*key)) # ФТ-34: детермінований порядок -> без дедлоків
stmt = pg_insert(totals).values(rows)
stmt = stmt.on_conflict_do_update(
index_elements=list(key),
set_={r: totals.c[r] + stmt.excluded[r] for r in res}, # r1 = t.r1 + EXCLUDED.r1
)
conn.execute(stmt) # 1 round-trip незалежно від len(rows)
Для MS SQL — той самий інтерфейс, інша реалізація (MERGE ... WHEN MATCHED THEN UPDATE) за діалектним диспетчером. Ізоляція — на рівні функції, прикладний код не знає про діалект.
Чому НЕ ORM: ORM-варіант (session.merge(obj) у циклі) дав би len(rows) round-trip'ів — пряме порушення АП-1 і зростання НФВ-04 на два порядки. Саме тому ORM заборонений у розділі 4.3.
Е.4. Транслятор віртуальних таблиць (етап Е5)
from functools import lru_cache
from lark import Lark, Transformer
from sqlalchemy import Select, func, literal, select, union_all
GRAMMAR = r"""
?query : "ВИБРАТИ"i select_list "З"i source [where] [group_by] [order_by]
source : virtual_table | plain_table
virtual_table : REGISTER_PATH "." VT_NAME "(" [vt_args] ")" ["ЯК"i alias]
VT_NAME : "Залишки"i | "Обороти"i | "ЗалишкиІОбороти"i
| "ЗрізОстанніх"i | "ЗрізПерших"i | "ОборотиДтКт"i
| "ДаніГрафіка"i | "ФактичнийПеріодДії"i | "БазаРозрахунку"i
vt_args : (expr | ) ("," (expr | ))* // позиційні, можуть бути порожні
...
"""
class QueryCompiler:
def __init__(self, schema: SchemaRegistry) -> None:
self._parser = Lark(GRAMMAR, parser="lalr", start="query")
self._schema = schema
@lru_cache(maxsize=2048) # АП-4: кеш скомпільованих запитів
def compile(self, text: str, param_signature: tuple[str, ...]) -> Select:
tree = self._parser.parse(text)
return _ToSQLAlchemy(self._schema).transform(tree)
def balances(self, reg: Register, period, conditions: list) -> Select:
"""Реалізація алгоритму 10.3 в термінах SQLAlchemy Core."""
tb = self._schema.totals_balance(reg)
mv = self._schema.movements(reg)
dims = [tb.c[c] for c in self._schema.dimension_columns(reg)]
boundary = (select(func.coalesce(func.max(tb.c.period), MIN_PERIOD))
.where(tb.c.period <= period)
.scalar_subquery())
base = (select(*dims, *[func.sum(tb.c[r]).label(r) for r in reg.resource_columns])
.where(tb.c.period == boundary, *conditions) # ФТ-13: push-down
.group_by(*dims))
signed = lambda c: case((mv.c.mvt == 0, mv.c[c]), else_=-mv.c[c])
tail = (select(*[mv.c[d.name] for d in dims],
*[func.sum(signed(r)).label(r) for r in reg.resource_columns])
.where(mv.c.active, mv.c.period > boundary, mv.c.period <= period, *conditions)
.group_by(*[mv.c[d.name] for d in dims]))
u = union_all(base, tail).subquery()
return (select(*[u.c[d.name] for d in dims],
*[func.sum(u.c[r]).label(f"{r}Залишок") for r in reg.resource_columns])
.group_by(*[u.c[d.name] for d in dims])
.having(or_(*[func.sum(u.c[r]) != 0 for r in reg.resource_columns])))
Ризик Р1 (немає мови запитів) на Python пом'якшується: навіть якщо повноцінна мова запитів не буде реалізована в Е5, QueryCompiler.balances() залишається придатним як програмний API — прикладний код викликає його напряму й отримує Select, який можна далі з'єднувати засобами SQLAlchemy Core. Це план «Б» для Р1, недоступний у мовах без подібної бібліотеки.
Е.5. Витіснення: гарячий шлях за інтерфейсом (етапи Е7, Е11)
Реалізація принципу АП-3 на найризикованішому вузлі (Р5 + Р10).
from collections import defaultdict
from collections.abc import Callable, Sequence
from datetime import date, timedelta
from typing import Protocol
from uuid import UUID
Interval = tuple[date, date]
ONE_DAY = timedelta(days=1)
class DisplacementEngine(Protocol):
"""АП-3. Бюджет (АП-4): <= 3 c на 10 тис. ключів × ~10 записів."""
def compute(self,
records: Sequence[CalcRecord],
displaces: Callable[[UUID, UUID], bool]) -> dict[RecordKey, list[Interval]]:
"""Повертає фактичні періоди дії для кожного запису (розділ 10.4)."""
...
def subtract(intervals: list[Interval], cut: Interval) -> list[Interval]:
"""Різниця множин інтервалів. Найгарячіша функція платформи."""
out: list[Interval] = []
cb, ce = cut
for b, e in intervals:
if ce < b or cb > e: # не перетинаються
out.append((b, e))
continue
if b < cb:
out.append((b, cb - ONE_DAY)) # «хвіст» зліва
if e > ce:
out.append((ce + ONE_DAY, e)) # «хвіст» справа
return out
class PyDisplacementEngine:
"""Еталонна реалізація. Заміна на Cython/Rust не змінює виклик (АП-3)."""
def compute(self, records, displaces):
by_key: dict[DimKey, list[CalcRecord]] = defaultdict(list)
for r in records:
if r.active:
by_key[r.dim_key].append(r)
result: dict[RecordKey, list[Interval]] = {}
for Z in by_key.values():
if len(Z) > 500: # розділ 10.4: перехід на sweep-line
result |= SweepLineEngine().compute(Z, displaces)
continue
Z.sort(key=lambda r: (r.priority, r.period, r.line_no))
for z in Z:
intervals = [(z.act_begin, z.act_end)]
for w in Z:
if w is z or not displaces(w.calc_type, z.calc_type):
continue
if not _registration_periods_intersect(w, z):
continue
intervals = subtract(intervals, (w.act_begin, w.act_end))
if not intervals:
break # повністю витіснений
result[z.key] = intervals
return result
Property-based тести (мітигація Р5 — найпомилковішого вузла):
from hypothesis import given
from hypothesis import strategies as st
dates = st.dates(min_value=date(2020, 1, 1), max_value=date(2030, 12, 31))
intervals = st.lists(st.tuples(dates, dates).map(lambda t: tuple(sorted(t))), max_size=20)
@given(ivs=intervals, cut=st.tuples(dates, dates).map(lambda t: tuple(sorted(t))))
def test_subtract_invariants(ivs: list[Interval], cut: Interval) -> None:
out = subtract(ivs, cut)
assert all(b <= e for b, e in out), "некоректні межі"
assert not any(_overlaps(i, cut) for i in out), "витіснений інтервал залишився"
# збереження міри: те, що прибрали, дорівнює перетину з cut
assert _measure(out) + _measure(_intersect_all(ivs, cut)) == _measure(ivs)
Бенчмарк як блокер приймання (АП-4):
@pytest.mark.benchmark(group="displacement")
def test_displacement_budget(benchmark, records_10k_keys) -> None:
"""Вводиться в CI з етапу Е7. Перевищення бюджету -> червоний білд."""
benchmark(PyDisplacementEngine().compute, records_10k_keys, displaces)
assert benchmark.stats.stats.mean < 3.0, (
f"Порушено бюджет АП-4: {benchmark.stats.stats.mean:.2f} c > 3.0 c. "
f"Розгляньте sweep-line, Cython або перенесення у SQL (daterange + GiST)."
)
Е.6. Генерація .pyi-стабів (ФТ-К-07, етап Е1)
Проблема: поля регістру доступні динамічно (rs.new(Номенклатура=…)), тому mypy не бачить одруків і помилок типів — це і є ризик Р11.
Рішення: конструктор генерує стаби з метаопису при кожній зміні конфігурації.
# k2/generated/registers/ТовариНаСкладах.pyi -- АВТОЗГЕНЕРОВАНО. Не редагувати.
from datetime import datetime
from decimal import Decimal
from k2.catalogs import Номенклатура, Склади, ХарактеристикиНоменклатури
from k2.documents import ПрихіднаНакладна, ВведенняЗалишків
from k2.registers import AccumulationMovementType, Record, RecordSet
class ТовариНаСкладахRecord(Record):
Період: datetime
ВидРуху: AccumulationMovementType
Номенклатура: Номенклатура
Склад: Склади
Характеристика: ХарактеристикиНоменклатури | None
Партія: ПрихіднаНакладна | ВведенняЗалишків | None # складений тип
Кількість: Decimal
Сума: Decimal
Коментар: str
class ТовариНаСкладахRecordSet(RecordSet[ТовариНаСкладахRecord]):
def new(self, *, Період: datetime, ВидРуху: AccumulationMovementType,
Номенклатура: Номенклатура, Склад: Склади,
Характеристика: ХарактеристикиНоменклатури | None = None,
Партія: ПрихіднаНакладна | ВведенняЗалишків | None = None,
Кількість: Decimal = ..., Сума: Decimal = ...,
Коментар: str = ...) -> ТовариНаСкладахRecord: ...
Що це ловить у CI ще до запуску:
| Помилка прикладного коду | Без стабів | Зі стабами |
|---|---|---|
rs.new(Номенклатур=x) — одрук у назві поля |
Виняток у рантаймі при проведенні на продуктиві | mypy: unexpected keyword argument
|
m.Кількість = "багато" |
InvalidOperation при записі в БД |
mypy: incompatible type str, expected Decimal
|
m.Склад = номенклатура — переплутані посилання |
FK-помилка або, гірше, тихо неправильні дані | mypy: incompatible type
|
| Видалили вимірювання з метаданих, код лишився | Виняток при проведенні | Червоний білд одразу після зміни конфігурації |
Автодоповнення в IDE — приємний побічний ефект, який частково компенсує відсутність повноцінного конфігуратора рівня 1С на ранніх етапах.
Е.7. Автоматичний детектор порушень АП-1 (етап Е3, CI)
Принцип АП-1 марний без механічної перевірки — розробник порушить його ненавмисно.
# tests/architecture/test_ap1_no_row_by_row.py
@pytest.mark.parametrize("scenario", POSTING_SCENARIOS)
def test_roundtrips_do_not_scale_with_rows(db_counter, scenario) -> None:
"""АП-1: кількість звернень до СКБД НЕ залежить від кількості рядків документа."""
with db_counter() as c10:
scenario.build(rows=10).post()
with db_counter() as c100:
scenario.build(rows=100).post()
assert c100.roundtrips <= c10.roundtrips + 2, (
f"{scenario.name}: звернень до БД зросло {c10.roundtrips} -> {c100.roundtrips} "
f"при зростанні рядків у 10 разів. Порушення АП-1 — див. розділ 4.4."
)
class RowByRowGuard:
"""Runtime-запобіжник для dev/staging: виняток при >N однакових запитів у транзакції."""
def __init__(self, threshold: int = 20) -> None:
self._counts: Counter[str] = Counter()
self._threshold = threshold
def before_execute(self, conn, clauseelement, *args) -> None:
fingerprint = _statement_fingerprint(clauseelement) # SQL без літералів
self._counts[fingerprint] += 1
if self._counts[fingerprint] == self._threshold:
raise ArchitectureViolation(
f"Один і той самий запит виконано {self._threshold} разів у межах транзакції.\n"
f"Ймовірне порушення АП-1 («жодного рядка за раз»).\n{fingerprint}"
)
Це — найдешевша страховка проєкту. Порушення АП-1 не видно на тестових даних (10 рядків — усе швидко) і катастрофічне на продуктиві (100 рядків × 5 регістрів). Детектор вводиться з етапу Е3, а не за результатами НТ-03.
Е.8. Структура репозиторію ядра
k2-registers/ ├── src/k2/ │ ├── metadata/ # Е.1: Pydantic-моделі, репозиторій, валідація │ │ ├── models.py # Register, RegisterField, ... │ │ ├── repository.py # md_* таблиці │ │ └── validators.py # ФТ-К-01, антипатерни Д.10 │ ├── schema/ # Е.2: генерація DDL │ │ ├── builder.py # метаопис -> SQLAlchemy Table │ │ ├── dialects/ # НФВ-17: postgres.py, mssql.py │ │ └── restructure.py # Е10: Alembic-інтеграція │ ├── runtime/ # набори записів, проведення, блокування │ │ ├── recordset.py │ │ ├── posting.py # розділ 7.1 │ │ ├── totals.py # Е.3: merge_totals │ │ └── locks.py # ФТ-32 │ ├── query/ # Е.4: lark-граматика + компілятор │ │ ├── grammar.lark │ │ ├── compiler.py │ │ └── virtual/ # balances.py, turnovers.py, slices.py, calc_base.py │ ├── calculation/ # Е.5: витіснення (гарячий шлях) │ │ ├── protocols.py # DisplacementEngine │ │ ├── engine_py.py # еталонна реалізація │ │ └── engine_native/ # [резерв] Cython/Rust — тільки за результатом бенчмарку │ ├── codegen/ # Е.6: генерація .pyi-стабів │ └── diagnostics/ # Е.7: RowByRowGuard, перевірка цілісності (ФТ-41) ├── tests/ │ ├── unit/ │ ├── property/ # hypothesis: інтервальна арифметика (Р5) │ ├── architecture/ # Е.7: детектор АП-1 │ ├── benchmarks/ # АП-4: бюджети гарячих шляхів (gate у CI) │ └── integration/ # testcontainers: PostgreSQL + MS SQL (Р7) └── pyproject.toml # uv, ruff, mypy --strict
Кінець документа. Версія 1.2. Підлягає узгодженню з архітектором платформи K2 ERP до затвердження.