Перейти до вмісту

Регістри в K2 ERP

Матеріал з K2 ERP Wiki

1. Паспорт документа

Атрибут Значення
Назва Технічне завдання на розробку підсистеми «Регістри» платформи K2 ERP
Шифр K2-ERP-TZ-REG-001
Версія 1.1 (чернетка на узгодження; +Додаток Д «Каталог прикладів»)
Статус На розгляді
Замовник [підрозділ-замовник]
Виконавець [команда платформи K2 ERP]
Тип документа Технічне завдання (ГОСТ 34.602 адаптовано)
Аналог / прототип Механізм регістрів платформи «1С:Підприємство 8.3»
Мова розробки Delphi | Java]
СКБД PostgreSQL 14+ (основна), MS SQL Server 2019+ (додаткова)

1.1. Необхідні розширення MediaWiki для перегляду

Документ використовує теги діаграм. Для коректного рендерингу потрібні:

  • Extension:GraphViz — тег <graphviz>
  • Extension:PlantUML — тег <plantuml>
  • Extension:SyntaxHighlight_GeSHi — тег <syntaxhighlight>

Також використовується шаблон {{Note|текст}}. Якщо його немає у вашій вікі — створіть сторінку Template:Note з вмістом:

<div style="border-left:4px solid #36c; background:#eaf3ff; padding:8px 12px; margin:8px 0;">
'''ⓘ''' {{{1}}}
</div>

Якщо розширення недоступні — блоки діаграм відображатимуться як текстовий вихідний код; дублюючі ASCII-схеми наведені в тілі документа.

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

Шаблон:Note

Припущення Наслідок для ТЗ
П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).

Обмеження:

  • О1. Зміна складу вимірювань/ресурсів існуючого регістру — операція реструктуризації з простоєм; онлайн-міграція не гарантується для таблиць > 100 млн записів.
  • О2. Підсистема не гарантує коректність прикладної логіки проведення — лише цілісність механізму.
  • О3. Мінімальна дискретність періоду — 1 секунда (тип timestamp(0)) [уточнити: чи потрібна мілісекундна].

5. Класифікація регістрів

5.1. Дерево типів

5.2. Порівняльна матриця типів

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

6. Метамодель: як регістр задається

6.1. Місце в репозиторії метаданих

Конфігурація K2 ERP
├── Довідники
├── Документи
├── Плани рахунків              ← використовується РБ
├── Плани видів характеристик   ← використовується як тип субконто / вимірювання
├── Плани видів розрахунку      ← використовується РР
└── Регістри                    ◄── НОВИЙ РОЗДІЛ
    ├── Регістри відомостей
    │   └── КурсиВалют
    │       ├── Вимірювання      → Валюта
    │       ├── Ресурси          → Курс, Кратність
    │       ├── Реквізити        →
    │       └── Форми            → ФормаСписку, ФормаЗапису
    ├── Регістри накопичення
    │   └── ТовариНаСкладах
    │       ├── Вимірювання      → Номенклатура, Склад, Партія
    │       ├── Ресурси          → Кількість, Сума
    │       ├── Реквізити        → Коментар
    │       └── Реєстратори      → [ПрихіднаНакладна, ВидатковаНакладна, ...]
    ├── Регістри бухгалтерії
    │   └── Госпрозрахунковий
    │       ├── Вимірювання      → Організація, Підрозділ(небаланс.)
    │       ├── Ресурси          → Сума(баланс.), Кількість(небаланс.), ВалютнаСума
    │       ├── Реквізити        → Зміст, № журналу
    │       └── Реєстратори      → [...]
    └── Регістри розрахунку
        └── Нарахування
            ├── Вимірювання      → Співробітник(баз.), Підрозділ
            ├── Ресурси          → Результат, ВідпрацьованоДнів
            ├── Реквізити        → Графік
            ├── Перерахунки      → ПерерахунокЗаБазою
            └── Реєстратори      → [НарахуванняЗП]

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: [Документ.НарахуванняЗарплати]

Шаблон:Note

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. Порівняння/об'єднання конфігурацій має коректно обробляти об'єкти-регістри.

7. Функціональні вимоги

7.1. Набір записів і проведення документа

ФТ-01. Платформа надає прикладний об'єкт НабірЗаписів для кожного регістру з властивостями: Відбір, Записувати, і методами Прочитати(), Записати(Заміщати), Очистити(), ЗаблокуватиДляЗміни(), Вивантажити(), Завантажити().

ФТ-02. Для документа доступна колекція Рухи з наборами записів усіх регістрів, де документ вказано реєстратором.

ФТ-03. Сценарій проведення:

<plantuml> @startuml skinparam monochrome false skinparam shadowing false actor Користувач participant "Форма\nдокумента" as UI participant "Об'єкт\nДокумент" as Doc participant "Менеджер\nпроведення" as PM participant "Набори\nзаписів" as RS database "СКБД" as DB

Користувач -> UI: Провести UI -> Doc: Записати(РежимЗапису.Проведення) activate Doc Doc -> PM: НачатьТранзакцию() PM -> DB: BEGIN Doc -> Doc: ПередЗаписью() Doc -> DB: UPDATE doc SET Проведений=1 Doc -> Doc: ОбработкаПроведения(Отказ, Режим) note right of Doc

 Прикладний код:
 Рухи.ТовариНаСкладах.Записувати = Істина;
 Рух = Рухи.ТовариНаСкладах.Добавить();
 Рух.ВидРуху = ВидРухуНакопичення.Витрата;
 ...
 // контроль залишків (оперативне проведення)

end note Doc -> RS: (формування рухів у пам'яті) Doc -> PM: ЗаписатьДвижения() PM -> RS: для кожного НЗ з Записувати=Істина RS -> DB: DELETE FROM rg_a_0012 WHERE rec_ref = :doc RS -> DB: INSERT INTO rg_a_0012 (...) VALUES (...) RS -> PM: ОновитиПідсумки(delta) PM -> DB: MERGE rg_a_0012_tb (див. розд. 10.2) Doc -> Doc: ПриЗаписи() / ПослеЗаписи() alt Відмова = Хиба

 PM -> DB: COMMIT
 PM --> UI: Успіх

else Відмова = Істина або виключення

 PM -> DB: ROLLBACK
 PM --> UI: Помилка "Документ не проведено: ..."

end deactivate Doc @enduml </plantuml>

ФТ-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 на інтервал перетину.

Період реєстрації: Січень 2026.   Пріоритет витіснення: Лікарняний > Відпустка > Оклад

Заявлені періоди дії:
Оклад        01.01 ├══════════════════════════════════════════════┤ 31.01
Відпустка              10.01 ├════════════┤ 17.01
Лікарняний                     14.01 ├════════════┤ 22.01

Фактичні періоди дії (rg_c_XXXX_ap):
Оклад        01.01 ├═══════┤ 09.01            23.01 ├════════════┤ 31.01
Відпустка              10.01 ├═══┤ 13.01
Лікарняний                     14.01 ├════════════┤ 22.01

⇒ у таблиці фактичних періодів «Оклад» породжує ДВА інтервали (запис — один).

ФТ-23. Фактичні періоди дії матеріалізуються у службовій таблиці rg_c_<код>_ap під час запису набору записів. ФТ-24. Витіснення обчислюється в межах збігу усіх вимірювань регістру та перетину періодів реєстрації, визначених ПВР.

7.5.2. Базовий період і база розрахунку

ФТ-25. Віртуальна таблиця БазаРозрахунку повертає суму ресурсів записів базових видів розрахунку, зіставлених за базовими вимірюваннями, з урахуванням режиму ЗалежністьВідБази:

  • ЗаПеріодДії — беруться записи, чий період дії перетинається з базовим періодом поточного запису; результат пропорційно розподіляється за часткою перетину (за графіком, якщо заданий);
  • ЗаПеріодРеєстрації — беруться записи, чий період реєстрації потрапляє в базовий період.

7.5.3. Перерахунки

ФТ-26. При записі/зміні/видаленні запису базового виду розрахунку платформа автоматично формує записи у таблиці перерахунку для всіх залежних записів (за збігом вимірювань перерахунку та потраплянням у базовий період). ФТ-27. Прикладний код читає перерахунок як набір записів, перепроводить відповідні документи та очищує оброблені записи. ФТ-28. Перерахунок доступний як об'єкт для звіту «Документи до перерахунку».

7.5.4. Графік часу

ФТ-29. Графік — це РВ з обов'язковим вимірюванням типу Дата та числовим ресурсом. Віртуальна таблиця ДаніГрафіка повертає суму значень графіка за відповідний період (дії / реєстрації / базовий / фактичний період дії). ФТ-30. Якщо у графіку є додаткові вимірювання (напр. ВидГрафіка), вони зіставляються з однойменними реквізитами/вимірюваннями РР.

7.6. Прикладний API

// --- Регістр відомостей: незалежний запис ---
var mgr = Registers.Information["КурсиВалют"];

// точковий запис
var rec = mgr.CreateRecordManager();
rec.Період = new DateTime(2026, 7, 17);
rec.Валюта = usd;
rec.Курс = 41.85m;
rec.Кратність = 1;
rec.Write(replace: true);          // INSERT ... ON CONFLICT DO UPDATE

// зріз останніх
var slice = mgr.GetLast(DateTime.Today, new Filter { ["Валюта"] = usd });
decimal курс = slice.Курс;

// --- Регістр накопичення: рухи документа ---
protected override void OnPosting(PostingContext ctx)
{
    var rs = Movements["ТовариНаСкладах"];
    rs.Write = true;              // набір буде записано наприкінці транзакції
    rs.Clear();

    foreach (var row in this.Товари)
    {
        var m = rs.Add();
        m.Період       = this.Дата;
        m.MovementType = AccumulationMovementType.Expense;   // Витрата
        m.Номенклатура = row.Номенклатура;
        m.Склад        = this.Склад;
        m.Партія       = row.Партія;
        m.Кількість    = row.Кількість;
        m.Сума         = row.Сума;
    }

    // керовані блокування перед контролем залишків
    var lock_ = new DataLock();
    var item = lock_.Add("РегістрНакопичення.ТовариНаСкладах.Залишки");
    item.Mode = DataLockMode.Exclusive;
    item.DataSource = rs.Unload(new[] { "Номенклатура", "Склад" });
    item.UseFromDataSource("Номенклатура", "Номенклатура");
    item.UseFromDataSource("Склад", "Склад");
    lock_.Lock();

    rs.WriteNow();                // примусовий запис до контролю
    ControlBalances(ctx);         // прикладна перевірка на від'ємні залишки
}

// --- Регістр бухгалтерії ---
var rs = Movements["Госпрозрахунковий"];
var m  = rs.AddAccountingEntry();
m.Період    = this.Дата;
m.РахунокДт = accounts["281"];
m.РахунокКт = accounts["631"];
m.SubcontoDt[ВидиСубконто.Номенклатура] = row.Номенклатура;
m.SubcontoDt[ВидиСубконто.Склади]       = this.Склад;
m.SubcontoKt[ВидиСубконто.Контрагенти]  = this.Контрагент;
m.Сума      = row.Сума;
m.КількістьДт = row.Кількість;

// --- Регістр розрахунку ---
var rs = Movements["Нарахування"];
var m = rs.Add();
m.ВидРозрахунку     = calcTypes["Оклад"];
m.Період            = this.Дата;                 // період реєстрації
m.ПеріодДіїПочаток  = new DateTime(2026,7,1);
m.ПеріодДіїКінець   = new DateTime(2026,7,31);
m.БазовийПеріодПочаток = new DateTime(2026,6,1);
m.БазовийПеріодКінець  = new DateTime(2026,6,30);
m.Співробітник      = row.Співробітник;
m.Результат         = 0;                          // заповниться після розрахунку
rs.WriteNow();

// отримання даних графіка та бази — запитом

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

Шаблон:Note

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;

Шаблон:Note

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. Зведення: які таблиці породжує кожен тип регістру

РВ (незалежний)          →  rg_i_<id>
РВ (підпорядкований)     →  rg_i_<id>

РН (Залишки)             →  rg_a_<id>
                            rg_a_<id>_tb        (якщо ДозволитиПідсумкиЗалишків)
                            rg_a_<id>_tt        (якщо ДозволитиПідсумкиОборотів)
                            rg_a_<id>_opt

РН (Обороти)             →  rg_a_<id>
                            rg_a_<id>_tt
                            rg_a_<id>_ag1..agN  (агрегати)
                            rg_a_<id>_opt

РБ                       →  rg_b_<id>
                            rg_b_<id>_tb0 .. _tbK   (K = МаксКількістьСубконто)
                            rg_b_<id>_tt0 .. _ttK
                            rg_b_<id>_tc
                            rg_b_<id>_opt

РР                       →  rg_c_<id>
                            rg_c_<id>_ap
                            rg_c_<id>_rc1 .. _rcM   (перерахунки)

Оцінка: для РБ з МаксКількістьСубконто = 3 генерується 10 фізичних таблиць на один регістр.

9. ER-моделі

9.1. ER-модель метаданих (репозиторій)

<plantuml> @startuml hide circle skinparam linetype ortho skinparam shadowing false

entity "md_type" as T {

 * id : smallint <<PK>>
 --
 * kind : varchar(20)
 * name : varchar(80)
 table_name : varchar(63)

}

entity "md_register" as R {

 * id : smallint <<PK,FK→md_type>>
 --
 * name : varchar(80) <>
 * kind : varchar(12)
 * write_mode : varchar(24)
 * periodicity : varchar(24)
 accumulation_kind : varchar(10)
 enable_bal_totals : boolean
 enable_tno_totals : boolean
 tno_totals_period : varchar(6)
 split_totals : boolean
 chart_of_accounts_id : smallint <<FK>>
 correspondence : boolean
 max_extdim_count : smallint
 calc_types_plan_id : smallint <<FK>>
 use_action_period : boolean
 base_period_mode : varchar(24)
 schedule_register_id : smallint <<FK>>
 * physical_table : varchar(63)
 * struct_version : int

}

entity "md_register_field" as F {

 * register_id : smallint <<PK,FK>>
 * id : smallint <<PK>>
 --
 * name : varchar(80) <>
 * role : Dimension|Resource|Attribute
 * data_type_expr : text
 is_composite : boolean
 index_mode : varchar(28)
 is_master : boolean
 is_balance : boolean
 is_base_dimension : boolean
 accounting_flag : varchar(80)
 * column_prefix : varchar(8)

}

entity "md_register_recorder" as RR {

 * register_id : smallint <<PK,FK>>
 * doc_type_id : smallint <<PK,FK>>

}

entity "md_register_aggregate" as AG {

 * register_id : smallint <<PK,FK>>
 * id : smallint <<PK>>
 --
 * name : varchar(80)
 * periodicity : Day|Month|Quarter|Year
 is_realtime : boolean
 actual_upto : timestamp

}

entity "md_register_aggregate_dim" as AGD {

 * register_id : smallint <<PK,FK>>
 * aggregate_id : smallint <<PK,FK>>
 * field_id : smallint <<PK,FK>>

}

entity "md_register_recalc" as RC {

 * register_id : smallint <<PK,FK>>
 * id : smallint <<PK>>
 --
 * name : varchar(80)
 * physical_table : varchar(63)

}

entity "md_register_recalc_dim" as RCD {

 * register_id : smallint <<PK,FK>>
 * recalc_id : smallint <<PK,FK>>
 * name : varchar(80) <<PK>>
 --
 * reg_field_id : smallint <<FK>>

}

T ||--o| R  : "є регістром" T ||--o{ RR  : "тип документа" R ||--|{ F  : "має поля" R ||--o{ RR  : "реєстратори" R ||--o{ AG  : "агрегати" AG ||--|{ AGD  : "склад вимірювань" F ||--o{ AGD  : "вимірювання" R ||--o{ RC  : "перерахунки" RC ||--|{ RCD  : "вимірювання перерахунку" F ||--o{ RCD  : "відповідає" R }o--o| T  : "план рахунків" R }o--o| T  : "план видів розрахунку" R }o--o| R  : "графік часу" @enduml </plantuml>

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

<plantuml> @startuml hide circle skinparam linetype ortho skinparam shadowing false

entity "doc_0002\n«ПрихіднаНакладна»" as D1 {

 * ref : uuid <<PK>>
 --
 number : varchar
 date : timestamp
 posted : boolean

} entity "doc_0003\n«ВидатковаНакладна»" as D2 {

 * ref : uuid <<PK>>
 --
 number : varchar
 date : timestamp
 posted : boolean

} entity "cat_0001\n«Номенклатура»" as C1 {

 * ref : uuid <<PK>>
 --
 code, name

} entity "cat_0004\n«Склади»" as C2 {

 * ref : uuid <<PK>>
 --
 code, name

}

entity "rg_a_0012\n«ТовариНаСкладах» (рухи)" as M {

 * rec_t : smallint <<PK>>
 * rec_r : uuid <<PK>>
 * line_no : int <<PK>>
 --
 * period : timestamp(0)
 * active : boolean
 * mvt : smallint  «0=Прихід,1=Витрата»
 * d1_r : uuid  «Номенклатура» <<FK>>
 * d2_r : uuid  «Склад» <<FK>>
 * d3_t : smallint «Партія:тип»
 * d3_r : uuid  «Партія:посилання»
 * d4_r : uuid  «Характеристика» <<FK>>
 * r1 : numeric(15,3) «Кількість»
 * r2 : numeric(15,2) «Сума»
 a1 : varchar(200) «Коментар»

}

entity "rg_a_0012_tb\nпідсумки залишків" as TB {

 * period : timestamp(0) <<PK>>
 * d1_r : uuid <<PK>>
 * d2_r : uuid <<PK>>
 * d3_t : smallint <<PK>>
 * d3_r : uuid <<PK>>
 * d4_r : uuid <<PK>>
 * splitter : smallint <<PK>>
 --
 * r1 : numeric(20,3) «Кількість (±)»
 * r2 : numeric(20,2) «Сума (±)»

}

entity "rg_a_0012_tt\nпідсумки оборотів" as TT {

 * period : timestamp(0) <<PK>>
 * d1_r : uuid <<PK>>
 * d2_r : uuid <<PK>>
 * d3_t : smallint <<PK>>
 * d3_r : uuid <<PK>>
 * d4_r : uuid <<PK>>
 * splitter : smallint <<PK>>
 --
 r1_in, r1_out : numeric(20,3)
 r2_in, r2_out : numeric(20,2)

}

entity "rg_a_0012_opt" as OPT {

 * id : smallint = 1 <<PK>>
 --
 * totals_actual_to : timestamp(0)
 use_bal_totals : boolean
 use_tno_totals : boolean
 min_period : timestamp(0)

}

D1 ||..o{ M : "реєстратор\n(поліморфний, без FK)" D2 ||..o{ M : "реєстратор\n(поліморфний, без FK)" C1 ||--o{ M : "d1_r" C2 ||--o{ M : "d2_r" M }o..|| TB : "агрегується в\n(підтримується платформою)" M }o..|| TT : "агрегується в" OPT ||..|| TB : "ТА підсумків"

note bottom of M

 PK = (rec_t, rec_r, line_no)
 → усі рухи одного документа
   фізично суміжні
 Партиціонування: RANGE(period)

end note

note bottom of TB

 period = MAXPERIOD ('5999-11-01')
 → рядок «залишок за всі періоди»

end note @enduml </plantuml>

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

<plantuml> @startuml hide circle skinparam linetype ortho skinparam shadowing false

entity "coa_0005\n«План рахунків»" as A {

 * ref : uuid <<PK>>
 --
 parent_ref : uuid <<FK>>
 * code : varchar(20) <>
 order_code : varchar(40)
 * name : varchar(150)
 * acc_type : smallint «Акт/Пас/АП»
 off_balance : boolean
 ed_count : smallint
 fl_qty, fl_cur : boolean

} entity "coa_0005_ed\n«Види субконто рахунку»" as AE {

 * ref : uuid <<PK,FK>>
 * line_no : int <<PK>>
 --
 * ed_type_r : uuid <<FK>>
 only_tno : boolean
 sum_flag : boolean

} entity "cvt_0009\n«ПВХ.ВидиСубконто»" as VT {

 * ref : uuid <<PK>>
 --
 code, name
 value_type : text

}

entity "rg_b_0020\n«Госпрозрахунковий» (проводки)" as J {

 * rec_t : smallint <<PK>>
 * rec_r : uuid <<PK>>
 * line_no : int <<PK>>
 --
 * period : timestamp(0)
 * active : boolean
 * acc_dt_r : uuid <<FK>>
 * acc_kt_r : uuid <<FK>>
 * d1_r : uuid «Організація (балансове)»
 * d2_dt_r : uuid «Підрозділ Дт»
 * d2_kt_r : uuid «Підрозділ Кт»
 ed_dt1_t/_r .. ed_dt3_t/_r : «субконто Дт»
 ed_dt_h : bigint
 ed_kt1_t/_r .. ed_kt3_t/_r : «субконто Кт»
 ed_kt_h : bigint
 * r1 : numeric(20,2) «Сума (балансовий)»
 r2_dt, r2_kt : numeric(20,3) «Кількість»
 r3_dt, r3_kt : numeric(20,2) «ВалютнаСума»
 a1 : varchar(255) «Зміст»
 a2_r : uuid «Валюта»

}

entity "rg_b_0020_tb0\nзалишки, рівень 0" as B0 {

 * period <<PK>>
 * acc_r <<PK>>
 * d1_r <<PK>>
 * splitter <<PK>>
 --
 r1, r2, r3 : numeric «знакові»

} entity "rg_b_0020_tb1..tb3\nзалишки, рівні 1..3" as B1 {

 * period <<PK>>
 * acc_r <<PK>>
 * d1_r <<PK>>
 * ed1_t, ed1_r <<PK>>
 * (ed2_t, ed2_r) <<PK>>
 * (ed3_t, ed3_r) <<PK>>
 * splitter <<PK>>
 --
 r1, r2, r3 : numeric

} entity "rg_b_0020_tt0..tt3\nобороти за рівнями" as T0 {

 * period <<PK>>
 * acc_r <<PK>>
 * d1_r <<PK>>
 * (ed…) <<PK>>
 --
 r1_dt, r1_kt
 r2_dt, r2_kt
 r3_dt, r3_kt

} entity "rg_b_0020_tc\nобороти Дт↔Кт" as TC {

 * period <<PK>>
 * acc_dt_r <<PK>>
 * acc_kt_r <<PK>>
 * d1_r <<PK>>
 --
 r1 : numeric(24,2)

}

A ||--o{ AE : "види субконто" VT ||--o{ AE : "тип субконто" A ||--o{ J  : "acc_dt_r" A ||--o{ J  : "acc_kt_r" A ||--o{ B0 : "acc_r" A ||--o{ B1 : "acc_r" J }o..|| B0 : "агрегується (Дт: +, Кт: −)" J }o..|| B1 : "агрегується, якщо ed_count ≥ k" J }o..|| T0 : "агрегується" J }o..|| TC : "агрегується"

note right of J

 Балансовий ресурс r1 — одне значення,
 контроль Σ(Дт) = Σ(Кт) у межах реєстратора.
 Небалансові ресурси — пара Дт/Кт.
 Субконто — слоти (див. Рішення №2).

end note @enduml </plantuml>

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

<plantuml> @startuml hide circle skinparam linetype ortho skinparam shadowing false

entity "pct_0011\n«ПВР.ОсновніНарахування»" as P {

 * ref : uuid <<PK>>
 --
 code, name
 use_action_period : boolean
 base_dependency : varchar(24)

} entity "pct_0011_base\n«Базові види розрахунку»" as PB {

 * ref <<PK,FK>>
 * line_no <<PK>>
 --
 * base_type_r : uuid <<FK>>

} entity "pct_0011_displace\n«Витісняючі види розрахунку»" as PD {

 * ref <<PK,FK>>
 * line_no <<PK>>
 --
 * displacing_type_r : uuid <<FK>>

} entity "pct_0011_lead\n«Ведучі види розрахунку»" as PL {

 * ref <<PK,FK>>
 * line_no <<PK>>
 --
 * leading_type_r : uuid <<FK>>

}

entity "rg_c_0040\n«Нарахування» (рухи)" as CR {

 * rec_t : smallint <<PK>>
 * rec_r : uuid <<PK>>
 * line_no : int <<PK>>
 --
 * period : timestamp(0) «період реєстрації»
 * active : boolean
 * reversal : boolean «Сторно»
 * calc_type_r : uuid <<FK>>
 * act_begin : timestamp(0)
 * act_end : timestamp(0)
 base_begin : timestamp(0)
 base_end : timestamp(0)
 * d1_r : uuid «Співробітник (базове)»
 * d2_r : uuid «Підрозділ»
 r1 : numeric «Результат»
 r2 : numeric «ВідпрацьованоДнів»
 r3 : numeric «НормаДнів»

}

entity "rg_c_0040_ap\nфактичні періоди дії" as AP {

 * rec_t <<PK,FK>>
 * rec_r <<PK,FK>>
 * line_no <<PK,FK>>
 * interval_no : smallint <<PK>>
 --
 * calc_type_r : uuid
 * d1_r : uuid
 * fact_begin : timestamp(0)
 * fact_end : timestamp(0)

}

entity "rg_c_0040_rc1\n«ПерерахунокЗаБазою»" as RC {

 * rec_t <<PK>>
 * rec_r <<PK>>
 * calc_type_r <<PK>>
 * d1_r <<PK>>
 --
 src_rec_t, src_rec_r
 created_at

}

entity "rg_i_0015\n«ГрафікРоботи» (РВ)" as SCH {

 * d1_r : uuid <<PK>> «ВидГрафіка»
 * d2_d : timestamp(0) <<PK>> «ДатаГрафіка»
 --
 r1 : numeric «Значення»

}

entity "doc_0007\n«НарахуванняЗарплати»" as DOC {

 * ref : uuid <<PK>>
 --
 number, date, posted

}

P ||--o{ CR  : "calc_type_r" P ||--o{ PB  : "базові" P ||--o{ PD  : "витісняючі" P ||--o{ PL  : "ведучі" DOC ||..o{ CR : "реєстратор" CR ||--|{ AP : "1 запис → 0..N\nфактичних інтервалів" CR }o..o{ RC : "потребує перерахунку" SCH }o..o{ CR : "ДаніГрафіка\n(віртуальна таблиця)" CR }o..o{ CR : "БазаРозрахунку\n(self-join через PB\nта базові вимірювання)" @enduml </plantuml>

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. Запис набору записів (режим «Заміщати»)

ЗАПИСАТИ_НАБІР(регістр R, відбір S, записи N[]):
  1. Перевірити права (Додавання/Зміна)
  2. Валідація: тип реєстратора ∈ md_register_recorder;
                вимірювання з deny_empty ≠ порожнє;
                для РБ — Σ(r1 де балансовий) по Дт = по Кт у межах реєстратора
  3. У транзакції:
     3.1. СТАРІ := SELECT * FROM rg_x WHERE <відбір S> FOR UPDATE
     3.2. Δ := АГРЕГУВАТИ(N, +1) ⊕ АГРЕГУВАТИ(СТАРІ, −1)   // тільки active
     3.3. DELETE FROM rg_x WHERE <відбір S>
     3.4. INSERT INTO rg_x SELECT ... FROM N
     3.5. ОНОВИТИ_ПІДСУМКИ(R, Δ)                          // див. 10.2
     3.6. ЯКЩО R.kind = Calculation:
              ПЕРЕРАХУВАТИ_ФАКТИЧНІ_ПЕРІОДИ(R, зачеплені вимірювання)  // 10.4
              ЗАРЕЄСТРУВАТИ_ПЕРЕРАХУНКИ(R, N ∪ СТАРІ)                  // 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';

Шаблон:Note

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. Розрахунок фактичних періодів дії (витіснення)

ПЕРЕРАХУВАТИ_ФАКТИЧНІ_ПЕРІОДИ(регістр R, множина ключів K):
  ДЛЯ кожного ключа k ∈ K:            // k = кортеж значень усіх вимірювань R
    Z := SELECT * FROM rg_c_X
         WHERE вимірювання = k AND active
         ORDER BY <пріоритет виду розрахунку за ПВР>, period, line_no
    // пріоритет: A витісняється B, якщо B ∈ ПВР[A].ВитісняючіВидиРозрахунку

    DELETE FROM rg_c_X_ap WHERE (rec,line) ∈ Z

    ДЛЯ кожного запису z ∈ Z (у порядку зростання пріоритету, тобто найслабші — першими):
      I := { [z.act_begin, z.act_end] }
      ДЛЯ кожного w ∈ Z, де ВидРозрахунку(w) витісняє ВидРозрахунку(z)
                         І перетинаються періоди реєстрації за правилами ПВР:
        I := I \ [w.act_begin, w.act_end]        // різниця множин інтервалів
      n := 0
      ДЛЯ кожного інтервалу i ∈ I (упорядковано):
        INSERT INTO rg_c_X_ap
          VALUES (z.rec_t, z.rec_r, z.line_no, n, z.calc_type_r, z.d1_r, i.begin, i.end)
        n := n + 1

Складність: O(|Z|² ) на ключ. Обмеження: якщо |Z| > 500 для одного ключа — попередження в журналі та перехід на алгоритм замітання (sweep line) O(|Z| log |Z|).

10.5. Реєстрація перерахунків

ЗАРЕЄСТРУВАТИ_ПЕРЕРАХУНКИ(регістр R, змінені записи Z):
  ДЛЯ кожного перерахунку P ∈ R.Перерахунки:
    ДЛЯ кожного z ∈ Z:
      // знайти всі записи, для яких z є базовим
      залежні := SELECT DISTINCT rec_t, rec_r, calc_type_r, <вимірювання P>
                 FROM   rg_c_X q
                 JOIN   pct_base b ON b.ref = q.calc_type_r
                                  AND b.base_type_r = z.calc_type_r
                 WHERE  q.active
                   AND  <вимірювання P у q> = <вимірювання P у z>
                   AND  ( (R.base_period_mode = 'ByActionPeriod'
                            AND [z.act_begin, z.act_end] ∩ [q.base_begin, q.base_end] ≠ ∅)
                       OR (R.base_period_mode = 'ByRegistrationPeriod'
                            AND z.period BETWEEN q.base_begin AND q.base_end) )
                   AND  (q.rec_t, q.rec_r) ≠ (z.rec_t, z.rec_r)
      INSERT INTO rg_c_X_rc<n> ... ON CONFLICT DO NOTHING

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 + конструктор (базовий) Створення регістру будь-якого типу з генерацією схеми 4 Е0
Е2 Регістр відомостей: рантайм, набори записів, зрізи, форми РВ у продуктиві 2,5 Е1
Е3 Регістр накопичення: рухи, підсумки, ТА, віртуальні таблиці РН у продуктиві 5 Е2
Е4 Механізм проведення документів, блокування, оперативне/неоперативне проведення Транзакційне проведення 2,5 Е3
Е5 Розширення мови запитів (віртуальні таблиці, транслятор, push-down) Запити до ВТ 4 Е3
Е6 Регістр бухгалтерії + план рахунків + субконто + підсумки за рівнями РБ у продуктиві 6 Е4, Е5
Е7 Регістр розрахунку + ПВР + витіснення + база + графіки + перерахунки РР у продуктиві 6 Е4, Е5
Е8 Агрегати, розділення підсумків, порадник агрегатів Оптимізація 2,5 Е3
Е9 Права доступу, RLS, аудит, сервіси цілісності Безпека та адміністрування 2 Е6, Е7
Е10 Реструктуризація, міграція, інструменти звірки Інструменти міграції 2,5 Е6, Е7
Е11 Навантажувальне тестування, оптимізація, документація Приймальні випробування 3 усі
РАЗОМ 41,5

Шаблон:Note

Мінімальний життєздатний обсяг (MVP): Е0–Е5 (РВ + РН + проведення + запити) ≈ 19,5 чол.-міс. — покриває ~70 % прикладних сценаріїв K2 ERP.

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 та документації.

Шаблон:Note

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

Додаток Д. Каталог прикладів регістрів

Додаток містить прикладні приклади регістрів усіх типів і підвидів. Для кожного наведено: декларацію (DSL), пояснення проєктних рішень, приклад даних у фізичних таблицях і типовий запит. Приклади призначені для: (а) перевірки повноти метамоделі на етапі Е0; (б) формування набору приймальних тестів; (в) навчання прикладних розробників.

Д.1. Як обрати тип регістру (дерево рішень)

Контрольні питання при проєктуванні будь-якого регістру:

  1. Чи можна відповісти на всі прикладні питання лише полями цього регістру, без з'єднання з документами? Якщо ні — бракує реквізиту (не вимірювання!).
  2. Яка кардинальність таблиці підсумків = ∏(кількість значень вимірювань) × кількість періодів? Якщо > 108 — переглянути склад вимірювань.
  3. Чи потрібно фільтрувати/групувати за цим полем у звітах? Так → вимірювання. Ні, лише показувати → реквізит.
  4. Чи підсумовується поле? Так → ресурс. Ні → реквізит або вимірювання.

Д.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

Запит (курс на дату документа):

ВИБРАТИ Курси.Курс, Курси.Кратність
З РегістрВідомостей.КурсиВалют.ЗрізОстанніх(&ДатаДокумента, Валюта = &Валюта) ЯК Курси
// або через менеджер
var курс = Registers.Information["КурсиВалют"]
                    .GetLast(doc.Дата, new Filter { ["Валюта"] = 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

Шаблон:Note

Д.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) ЯК Доступно
З РегістрНакопичення.ТовариНаСкладах.Залишки(&Період, Склад = &Склад) ЯК Зал
ПОВНЕ З'ЄДНАННЯ РегістрНакопичення.ТовариВРезерві.Залишки(&Період, Склад = &Склад) ЯК Рез
    ЗА Зал.Номенклатура = Рез.Номенклатура І Зал.Склад = Рез.Склад

Шаблон:Note

Д.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

Згортка tb2tb0: 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)
);

Шаблон:Note

Приклад даних:

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 грн.

protected override void OnPosting(PostingContext ctx)
{
    // --- 0. Блокування перед контролем залишків ---
    var dl = new DataLock();
    var li = dl.Add("РегістрНакопичення.ТовариНаСкладах.Залишки");
    li.Mode = DataLockMode.Exclusive;
    li.DataSource = this.Товари.Unload(new[] { "Номенклатура", "Характеристика" });
    li.UseFromDataSource("Номенклатура", "Номенклатура");
    li.UseFromDataSource("Характеристика", "Характеристика");
    li.SetValue("Склад", this.Склад);
    dl.Lock();

    // --- 1. РН ТовариНаСкладах: списання ---
    var rsGoods = Movements["ТовариНаСкладах"];  rsGoods.Write = true;  rsGoods.Clear();
    foreach (var row in this.Товари) {
        var m = rsGoods.Add();
        m.Період = this.Дата;
        m.MovementType = AccumulationMovementType.Expense;
        m.Номенклатура = row.Номенклатура;  m.Характеристика = row.Характеристика;
        m.Склад = this.Склад;               m.Партія = row.Партія;
        m.Кількість = row.Кількість;        m.Сума = row.Собівартість;
    }

    // --- 2. РН Взаєморозрахунки: дебіторка зростає ---
    var rsAR = Movements["ВзаєморозрахункиЗКонтрагентами"];  rsAR.Write = true;  rsAR.Clear();
    var ar = rsAR.Add();
    ar.Період = this.Дата;
    ar.MovementType = AccumulationMovementType.Receipt;      // Прихід = борг зростає
    ar.Організація = this.Організація;  ar.Контрагент = this.Контрагент;
    ar.Договір = this.Договір;          ar.Замовлення = this.ЗамовленняПокупця;
    ar.Сума = this.СумаДокумента;       ar.СумаВал = this.СумаДокументаВал;

    // --- 3. РН Продажі: оборот (без ВидРуху!) ---
    var rsSales = Movements["Продажі"];  rsSales.Write = true;  rsSales.Clear();
    foreach (var row in this.Товари) {
        var s = rsSales.Add();
        s.Період = this.Дата;
        s.Номенклатура = row.Номенклатура;  s.Контрагент = this.Контрагент;
        s.Договір = this.Договір;           s.Менеджер = this.Менеджер;
        s.Склад = this.Склад;
        s.Кількість = row.Кількість;        s.Сума = row.Сума;
        s.СумаБезПДВ = row.СумаБезПДВ;      s.Собівартість = row.Собівартість;
    }

    // --- 4. РН ТовариВРезерві: зняття резерву ---
    if (!this.ЗамовленняПокупця.IsEmpty()) { /* ... Витрата ... */ }

    // --- 5. РБ Госпрозрахунковий: проводки ---
    var rsAcc = Movements["Госпрозрахунковий"];  rsAcc.Write = true;  rsAcc.Clear();
    foreach (var row in this.Товари) {
        var e1 = rsAcc.AddAccountingEntry();       // Дт 361 / Кт 702
        e1.Період = this.Дата;
        e1.РахунокДт = Accounts["361"];  e1.РахунокКт = Accounts["702"];
        e1.SubcontoDt[ВидиСубконто.Контрагенти] = this.Контрагент;
        e1.SubcontoDt[ВидиСубконто.Договори]    = this.Договір;
        e1.SubcontoKt[ВидиСубконто.Номенклатура] = row.Номенклатура;
        e1.Організація = this.Організація;
        e1.Сума = row.Сума;  e1.КількістьКт = row.Кількість;

        var e2 = rsAcc.AddAccountingEntry();       // Дт 702 / Кт 641
        /* ... ПДВ ... */

        var e3 = rsAcc.AddAccountingEntry();       // Дт 902 / Кт 281
        /* ... собівартість ... */
    }

    rsGoods.WriteNow();                            // примусовий запис до контролю
    ControlNegativeBalances(ctx);                  // прикладна перевірка
}

Що відбувається у БД (одна транзакція):

Крок Таблиця Операція Рядків
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

Шаблон:Note

Д.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 Контроль залишків до запису рухів Класична гонитва: два сеанси проходять контроль і обидва списують у мінус Спершу DataLockWriteNow() → потім контроль (Д.8, ФТ-Т-12)
А-14 periodicity: WithinSecond «про всяк випадок» скрізь Для РВ — неможливість замістити запис за день; для РН — марна точність, більші індекси Обирати мінімально достатню: день для цін/курсів/виписок, секунда — для складських рухів

Д.11. Чек-лист рев'ю нового регістру

□ Тип регістру обґрунтований за деревом Д.1
□ Кожне вимірювання використовується у фільтрі АБО групуванні звітів
□ Кожен ресурс має сенс при підсумовуванні
□ Немає полів з антипатернів А-1, А-4, А-7, А-10, А-12
□ Оцінка кардинальності підсумків ≤ 10^8 (калькулятор конструктора, ФТ-К-03)
□ Для вимірювань, де порожнє значення безглузде, стоїть denyEmpty
□ Ведучі (master) вимірювання визначені — інакше «висячі» записи при видаленні об'єктів
□ Склад реєстраторів повний; зайвих типів документів немає
□ Угода про знак / вид руху зафіксована в методологічній документації
□ Періодичність мінімально достатня (А-14)
□ Для РН «Обороти» з >4 вимірюваннями визначено ≥1 агрегат
□ Для РБ: балансові/небалансові вимірювання та ресурси розставлені свідомо
□ Для РР: витісняючі та базові види розрахунку в ПВР заповнені; перерахунки визначені
□ Партиціонування ввімкнене, якщо очікується >100 млн записів
□ Написано тест на проведення + скасування + перерахунок підсумків

Кінець документа. Версія 1.1. Підлягає узгодженню з архітектором платформи K2 ERP до затвердження.