Skip to content

Модуль податків

Модуль зберігає правила оподаткування, поширює їх від груп товарів на окремі позиції, обчислює суму податку при проведенні документа й блокує проведення, якщо набір податків не відповідає правилам. Нормативна база і порівняння з ЄС — у 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), а нетто обчислюється зворотним ходом при проведенні документу:

НЕТТО = БРУТТО / (1 + сума ставок)

Звідси й назви формул бази нарахування: 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 (сертифікат) проходять без перевірки податків.

Зміна ставки

Ставки версіоновані, тож стара ставка не видаляється, а закривається датою:

  1. У поточного рядка ставки виставити VALID_TO = нова_дата − 1.
  2. Додати новий рядок: VALID_FROM = нова дата, нове значення.

Тригер R_TAX_RATES_BIU_OVERLAP заблокує збереження, якщо періоди дії перекриваються або попередній рядок не закрито.

Фіскальна літера ПРРО

Кожен податок має власну літеру — поле R_TAXES.FISCAL_LETTER (А/Б/В/… ; NULL = у чек не виноситься). У фіскальному чеку:

  • рядок товару несе конкатенацію літер усіх своїх податків (ПДВ перший, потім акциз) — напр. <LETTERS>АГ</LETTERS>;
  • у блоці <CHECKTAX>окремий рядок на кожен податок (TYPE 0=ПДВ, 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 технічно існують, але не використовуються до запровадження роботи з ЄС.