Модуль податків
Модуль зберігає правила оподаткування, поширює їх від груп товарів на
окремі позиції, обчислює суму податку при проведенні документа й блокує
проведення, якщо набір податків не відповідає правилам. Нормативна база
і порівняння з ЄС — у taxes_ua_law.md.
Базові поняття
Перш ніж читати решту, варто розуміти основі поняття — на них тримається вся логіка модуля.
ПДВ і акциз
- ПДВ (податок на додану вартість) — універсальний податок майже на всі товари за єдиною ставкою: загальна 20%, пільгові 7% і 0%, або звільнення. Хліб, одяг, побутова техніка — це ПДВ.
- Акциз — додатковий податок лише на окремі групи: алкоголь, тютюн, пальне, тощо. Підакцизний товар обкладається і ПДВ, і акцизом одночасно. Пляшка горілки несе обидва податки; буханець хліба — лише ПДВ.
В Україні діють два різні акцизи, і це важливо:
- Роздрібний акциз 5% — відсоток від роздрібної вартості, який магазин сплачує з кожного продажу.
- Питомий (специфічний) акциз — фіксована сума у гривнях за фізичну одиницю (грн/літр, грн/1000 шт), яку виробник чи імпортер уже заклав у ціну.
Включений і нарахований податок
- Включений (вкладений) податок уже «сидить» усередині ціни на ціннику. Покупець бачить ціну 120 грн, з них 20 грн — це ПДВ. Для роздрібної торгівлі в Україні це норма: ціна на полиці завжди кінцева, з усіма податками.
- Нарахований (накладений) податок додається понад ціну: 100 грн + 20 грн ПДВ = 120 грн до сплати. Так працюють оптові та B2B-документи.
Поле PRICE_MODE документа задає трактування ціни рядка:
PRICE_MODE |
Сенс | Як рахується податок |
|---|---|---|
'G' (gross / брутто) |
податок включений у ціну | податок «витягується» з ціни зворотним ходом |
'N' (net / нетто) |
податок нараховується понад ціну | податок додається до ціни |
За замовчуванням усі документи створюються як 'G' - це відповідає українській роздрібній торгівлі.
Ціна нетто і брутто
- Нетто (база) — «чиста» ціна без податків.
- Брутто — кінцева ціна з усіма податками, яку платить покупець.
У роздрібній торгівлі зберігається саме брутто-ціна (SLL_PRICE), а нетто обчислюється зворотним ходом при проведенні документу:
Звідси й назви формул бази нарахування: NET, NET_PLUS (нетто плюс включені податки), GROSS.
Як це працює загалом
flowchart TD
G["Групи товарів<br/>(правила C_GROUP_TAXES)"] --> EFF
T["Окремий товар<br/>(перевизначення C_TOVAR_TAXES)"] --> EFF
EFF["Ефективний набір податків<br/>TAX_EFFECTIVE<br/>(одне правило на категорію)"] --> RATE
RATE["Ставки на дату<br/>TAX_EFFECTIVE_AT"] --> CALC
CALC["Обчислення суми<br/>(з урахуванням PRICE_MODE)"] --> SNAP
SNAP["Знімок у *_BODY_TAXES<br/>(незмінний)"] --> VAL
VAL{"Перевірка<br/>TAX_VALIDATE_*"}
VAL -- "усе збігається" --> OK["Проведення дозволено"]
VAL -- "missing / extra" --> BLOCK["Проведення заблоковано"]
Ключова ідея: правила задаються на групах і товарах, а в документ потрапляє незмінний знімок реально застосованих податків і ставок. Навіть якщо завтра ставку змінять, старий документ збереже те, що було на момент проведення.
Довідники (ключові таблиці)
| Таблиця | Призначення |
|---|---|
R_TAX_CATEGORIES |
Категорії для взаємовиключення: VAT, RETAIL_EXCISE, UNIT_EXCISE. У межах однієї категорії на товар лишається лише один податок. |
R_TAX_BASES |
Формула бази: NET, NET_PLUS, GROSS. |
R_TAXES |
Каталог податків. KIND (1 = %, 2 = роздрібний акциз, 3 = питомий), CATEGORY_ID, REQUIRES_MARK, REPORT_CODE. |
R_TAX_BASE_DEPS |
Для NET_PLUS — які податки входять у базу (напр. ПДВ 20% рахується на нетто плюс роздрібний акциз). |
R_TAX_RATES |
Версіонована історія ставок (VALID_FROM/VALID_TO). Для питомих MEASURE_ID — це одиниця ставки (грн за що саме), а не одиниця товару. |
R_TAXES.FISCAL_LETTER |
Літера податку у чеку ПРРО (одна на податок; NULL = не виноситься). |
C_GROUP_TAXES |
Правило на групу: ACTION 1 = прикріпити, 2 = відключити; IS_REQUIRED; IS_DETACHABLE. |
C_TOVAR_TAXES |
Перевизначення на рівні товару: ACTION 1 = додати, 2 = відключити. |
R_EXCISE_MARKS |
Акцизні марки (алкоголь, тютюн). Життєвий цикл нижче. |
Поширення правил: ефективний набір
TAX_EFFECTIVE(TOVAR_ID) визначає, які податки реально застосовуються до
товару, у три кроки:
flowchart TD
A["Обхід дерева груп товару<br/>(R_GROUPS.PARENT, знизу вгору)"] --> B
B["Для кожного податку береться правило<br/>найближчого предка"] --> C
C["Застосовуються перевизначення товару<br/>(додати / відключити)"] --> D
D["У межах кожної категорії лишається<br/>лише одне правило (найменша глибина)"] --> E["Ефективний набір"]
Перевизначення товару не може відключити правило групи, позначене
IS_DETACHABLE = 0 (обов'язкове до сплати на рівні групи).
TAX_EFFECTIVE_AT(TOVAR_ID, ON_DATE) — те саме плюс ставка з
R_TAX_RATES, чинна на вказану дату. Саме цю процедуру викликає клієнт
при проведенні.
Запис знімка (тригери)
Розрахунок і запис податків повністю на боці бази. На кожній із трьох
таблиць рядків документів є тригер AFTER INSERT OR UPDATE, який при
збереженні рядка автоматично заповнює відповідну таблицю *_BODY_TAXES.
Клієнтський код у цьому не бере участі — будь-який шлях запису рядка
(Каса, Склад, копіювання документа, імпорт, майбутній REST) одразу
отримує знімок.
sequenceDiagram
autonumber
participant Клієнт
participant Рядок as Рядок документа
participant Тригер as Тригер *_BODY (AFTER I/U)
participant Знімок as *_BODY_TAXES
Клієнт->>Рядок: INSERT / UPDATE (товар, ціна, к-сть)
Рядок->>Тригер: спрацьовує автоматично
Тригер->>Тригер: TAX_COMPUTE_LINE<br/>(PRICE_MODE із заголовка, дата)
Тригер->>Знімок: видалити старі + записати нові рядки
Серцевина обчислення — процедури:
TAX_COMPUTE_LINE(товар, дата, ціна, к-сть, режим_ціни)— повертає по рядку на кожен застосовний податок із розрахованими базою та сумою;TAX_AMOUNT_COEF(...)— рекурсивний помічник, що розв'язує суму податку як лінійну функціюNETз урахуванням залежностей бази (R_TAX_BASE_DEPS).
У режимі 'G' (брутто) NET обчислюється зворотним ходом із ціни; у
'N' (нетто) ціна вже є базою.
Валідація при проведенні
Запис знімка нічого не блокує — він лише фіксує податки. Блокування
виконується в базі при проведенні: на заголовках OUT_CHECK,
INC_DELIVERY, OUT_DELIVERY є тригер BEFORE UPDATE, який спрацьовує у
момент переходу COMMITED з 0 на 1, перевіряє кожен рядок відповідною
процедурою і зриває проведення винятком E_TAX_DOC_INVALID з
переліком проблемних товарів, якщо є розбіжності. Оскільки перевірка в
базі, її не обійти жодним шляхом запису.
sequenceDiagram
autonumber
participant Клієнт
participant Тригер as Тригер заголовка (BEFORE UPDATE)
participant Перев as TAX_VALIDATE_*
Клієнт->>Тригер: UPDATE заголовка (COMMITED 0→1)
loop кожен рядок
Тригер->>Перев: TAX_VALIDATE_*(рядок)
end
alt усе збігається
Тригер-->>Клієнт: проведення дозволено
else є missing / extra
Тригер-->>Клієнт: E_TAX_DOC_INVALID → проведення заблоковано
end
Розбіжність зазвичай означає, що правила змінили вже після введення рядка, тож зафіксований знімок більше не відповідає чинному набору.
Процедури перевірки за типом документа:
| Процедура | Для |
|---|---|
TAX_VALIDATE_INC_LINE(BODY_ID) |
Прихідна накладна |
TAX_VALIDATE_OUT_CHECK_LINE(BODY_ID) |
Чек |
TAX_VALIDATE_OUT_DLV_LINE(BODY_ID) |
Розхідна накладна |
Кожна повертає рядки (PROBLEM, TAX_ID, TAX_NAME):
'missing'— податок обов'язковий, але його немає у знімку;'extra'— податок є у знімку, але його немає в ефективному наборі.
Будь-який повернений рядок блокує проведення.
Виняток: позиції з TTYPE 5 (тара), 6 (валюта), 8 (сертифікат)
проходять без перевірки податків.
Зміна ставки
Ставки версіоновані, тож стара ставка не видаляється, а закривається датою:
- У поточного рядка ставки виставити
VALID_TO = нова_дата − 1. - Додати новий рядок:
VALID_FROM = нова дата, нове значення.
Тригер R_TAX_RATES_BIU_OVERLAP заблокує збереження, якщо періоди
дії перекриваються або попередній рядок не закрито.
Фіскальна літера ПРРО
Кожен податок має власну літеру — поле R_TAXES.FISCAL_LETTER
(А/Б/В/… ; NULL = у чек не виноситься). У фіскальному чеку:
- рядок товару несе конкатенацію літер усіх своїх податків (ПДВ
перший, потім акциз) — напр.
<LETTERS>АГ</LETTERS>; - у блоці
<CHECKTAX>— окремий рядок на кожен податок (TYPE0=ПДВ, 1=акциз;NAME;LETTER;PRC;TURNOVER;SOURCESUM;SUM).
Усе це будує процедура RRO_CHECK із знімків OUT_CHECKBODY_TAXES. Якщо
у позиції податків немає (неплатник ПДВ, неакцизний товар) — літера не
виводиться взагалі. Літери реєструються продавцем у ДПС, тож типові
значення у FISCAL_LETTER коригуються під кожну інсталяцію. Зразки —
у .dev/PRRO/.
Акцизні марки
R_TAXES.REQUIRES_MARK = 1 означає, що товар із таким податком вимагає
марку на кожну одиницю.
| Податок | Потребує марку |
|---|---|
EXCISE_SPIRITS, EXCISE_WINE, EXCISE_CIGARETTES |
Так |
EXCISE_BEER, EXCISE_FUEL, EXCISE_RETAIL_5 |
Ні |
Де живе марка
Марка зберігається як непрозорий рядок у новому полі рядка документа
*_BODY.EXCISE_MARK (OUT_CHECKBODY, INC_DELIVERYBODY,
OUT_DELIVERYBODY). На цьому етапі формат марки (datamatrix, серія/номер)
не розбирається — клієнт лише передає відсканований рядок, а база
веде реєстр і перевіряє. Розбір формату — окреме завдання.
R_EXCISE_MARKS — реєстр марок. Кожен запис має RAW_BARCODE (той самий
рядок, унікальний), STATUS і зворотні посилання на рядок документа
(INC_BODY_ID / OUT_CHECK_BODY_ID / OUT_DLV_BODY_ID).
Життєвий цикл (R_EXCISE_MARKS.STATUS)
stateDiagram-v2
[*] --> На_складі: прихідна (реєстрація)
На_складі --> Продано: продаж (AMOUNT ≥ 0)
Продано --> Повернено: повернення (AMOUNT < 0)
На_складі --> Списано
На_складі --> Пошкоджено
- Прихід — рядок прихідної з
EXCISE_MARKреєструє марку вR_EXCISE_MARKSзі статусом «на складі». Повторна реєстрація тієї самої марки — помилка. - Продаж (чек або розхідна, кількість
AMOUNT ≥ 0) — марка має вже існувати й бути «на складі»; інакше проведення блокується. Після запису рядка марка переходить у «продано» і прив'язується до позиції. - Повернення (
AMOUNT < 0) — марка має бути раніше продана; переходить у «повернено».
Перевірка на рівні рядка (не на заголовку)
На відміну від податків (які звіряються на заголовку при проведенні),
марки перевіряються тригерами рівня рядка BEFORE INSERT OR UPDATE
(OUT_CHECKBODY_BI_EXMARK, OUT_DELIVERYBODY_BI_EXMARK): саме там живуть
реальні дані позиції. Якщо для підакцизного товару марки немає або вона
недоступна — рядок не зберігається (виняток E_MARK_INVALID).
AFTER-тригери атомарно «забирають» марку зі складу (race-safe через
ROW_COUNT) або повертають її при зміні/видаленні рядка. Реєстрацію при
прихід виконує INC_DELIVERYBODY_AI_EXMARK.
Старі процедури підрахунку
MARK_VALIDATE_INC_LINE/MARK_VALIDATE_OUT_CHECK_LINE/MARK_VALIDATE_OUT_DLV_LINEналежали до попередньої моделі (звірка кількості на заголовку) і більше не викликаються — їх лишено в схемі як кандидатів на видалення.
У фіскальному чеку
RRO_CHECK / RRO_CHECKRETURN виводять <EXCISELABELS> для позиції
безпосередньо з її EXCISE_MARK (одна марка на рядок). Якщо поле порожнє
— блок не емітується. Тег «підакцизний» на товарі — лише для
відображення, у логіці не бере участі.
Налаштування через довідник «Податки»
Довідник відкривається з меню «Довідники → Податки» у складській програмі. На практиці більшість користувачів не створюють податки — 9 українських податків уже занесені при встановленні. Типова робота — прив'язати потрібні податки до групи товарів (напр. «Алкоголь» → ПДВ 20% + роздрібний акциз) і за потреби перевизначити окремий товар.
Що можна зробити з форми:
- Каталог податків — перелік усіх податків; додавання, м'яке видалення й відновлення, показ видалених.
- Властивості податку — код, назва, категорія, база нарахування, вид, країна, код звітності, ознака «потребує марку».
- Ставки — версіонований список ставок податку. Для відсоткових заповнюється ставка у %, для питомих — сума за одиницю та одиниця виміру.
- Прив'язки до груп — у яких групах товарів діє податок, з якою дією (прикріпити/відключити) та чи є обов'язковим.
- Перевизначення на рівні товару — додати або відключити податок для конкретного товару (товар обирається скануванням штрих-коду або через стандартний вибір товару).
- Перевірка ефективного набору — обрати товар і дату й побачити, які
податки та ставки реально застосуються (через
TAX_EFFECTIVE_AT). Зручно для контролю після зміни правил.
Контекстні вкладки «Включені у базу», «Фіскальні літери», «Категорії» та «Бази» показуються вибірково — лише там, де вони мають сенс для обраного податку.
Не завершено / майбутні доробки
- Розбір формату акцизної марки. Зараз марка зберігається просто як рядок
- Питомі акцизи мають ставку
0.0000. Перед введенням в експлуатацію замінити даними з чинної редакції ст. 215 ПКУ (і щороку звіряти, бо ставки коригуються з 1 січня). - Перетворення одиниць виміру (літр напою у літр 100% спирту) ще не реалізована — питомий акциз рахується від кількості як є.
REVERSE_CHARGEтаREPORT_CODEтехнічно існують, але не використовуються до запровадження роботи з ЄС.