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

Manual JournalEntry — ручні проводки

Статус: ✅ Реалізовано (MVP, 2026-04-22)

Бухгалтер / фінансовий контролер може вручну зробити будь-яку валідну проводку Дт/Кт без створення документа-джерела. Використовується для коригуючих проводок, вступних залишків, місячних/річних закриттів, переоцінок, переносів між рахунками.


Призначення

Документ-джерело (Invoice, GoodsReceipt, …) автоматично генерує PostingGroup через BusinessOperation-шаблон. Але є випадки, коли проводка потрібна поза цим flow: - помилка минулого періоду — потрібна сторнуюча проводка; - вступні залишки при міграції з іншої системи; - місячні корекції резервів, амортизації, нарахувань; - переоцінка валютних активів; - розподіл загальногосподарських витрат за фінансовими вимірами; - закриття періоду / року; - проводки, що не мають первинного документа в системі (наприклад, страхове відшкодування, рішення суду); - зарплата без модуля HRM — згорнутими проводками (нарахування + утримання + виплата) без per-employee розкладки. Див. розділ нижче.

JournalEntry дозволяє створити таку проводку напряму, з валідацією Σ Дт = Σ Кт перед проведенням.


Модель даних

JournalEntry (TransactionModel)

Поле Тип Опис
number str Номер документа (як у решти TransactionModel)
date date Дата проводки (потрапляє у PostingGroup.date)
state enum draft / posted / marked
organization FK Organization Опціонально, для поділу по юр. особах
currency FK Currency Валюта групи (record-keeping; реальні суми в Decimal)
description text Опис документа (потрапляє у PostingGroup.description)

JournalEntryLine (TenantAwareModel)

Поле Тип Опис
entry FK JournalEntry Посилання на header
account FK ChartOfAccounts Рахунок (PROTECT — не видалити, якщо є посилання)
debit Decimal Сума Дт (взаємовиключно з credit)
credit Decimal Сума Кт
description str Опис рядка (опціонально)
partner FK Client Аналітика — контрагент
project FK BusinessDirection Аналітика — напрямок
department FK Department Аналітика — підрозділ
expense_item FK ExpenseItem Аналітика — стаття витрат

При проведенні всі аналітики копіюються 1:1 у відповідні PostingEntry.


Сервіси

essentials/services/journal_entry.py

post_journal_entry(je) → PostingGroup

  1. Завантажує всі JournalEntryLine з select_related для аналітик.
  2. Перевіряє Σ debit == Σ credit — інакше підіймає JournalEntryNotBalanced.
  3. Idempotent: видаляє попередню PostingGroup з document_type='JournalEntry', document_id=je.pk, щоб повторне проведення не дублювало.
  4. Створює нову PostingGroup (amount=Σ debit, manual_edit=True, document_type='JournalEntry').
  5. Створює один PostingEntry на кожен JournalEntryLine з усіма аналітиками.

unpost_journal_entry(je) → int

Видаляє PostingGroup (та всі PostingEntry через CASCADE). Повертає кількість видалених рядків. Виклик автоматично з ViewSet при unpost_document.

get_posting_group(je) → Optional[PostingGroup]

Повертає поточну PostingGroup, якщо JE проведений.


API

Метод URL Опис
GET/POST /api/v1/essentials/journal-entries/ CRUD header
PATCH/DELETE /api/v1/essentials/journal-entries/{id}/
GET/POST/PATCH/DELETE /api/v1/essentials/journal-entry-lines/ CRUD рядків
POST /api/v1/essentials/journal-entries/{id}/post_document/ Провести (валідує + створює PostingGroup; 400 якщо unbalanced)
POST /api/v1/essentials/journal-entries/{id}/unpost_document/ Відмінити проведення

Серіалізатор повертає додаткові поля: total_debit, total_credit, is_balanced, account_code/name, partner_name, project_name тощо.


Frontend

components/Essentials/JournalEntry/JournalEntryPage.tsx

Дві view:

Список (без :id у URL): - Таблиця: Number · Date · Description · Amount · State · Balanced (✓/⚠). - Клік на рядок → відкриває form. - Кнопка «Нова проводка».

Форма (/essentials/.../journalEntries/{id} або new): - Header: Number · Date · Organization · Currency · Description. - Таблиця рядків: Account · Debit · Credit · Description · ✕ (видалити). - Footer: Σ Debit, Σ Credit, дисбаланс (підсвічено червоним якщо ≠ 0). - Кнопки: - Save — зберегти header + sync рядків (PATCH/POST/DELETE кожного рядка проти існуючих). - Post — увімкнено лише якщо balanced; викликає /post_document/. - Unpost — для posted-документа; викликає /unpost_document/ і повертає в draft. - Posted-стан робить усі поля read-only (треба unpost щоб редагувати).

Зареєстровано в config/essentials.ts як journalEntries (ItemType.TRANSACTIONDATA, plugin: "accounting"), у групі Operations & Postings поряд з documentOperations.


Workflow

[draft]  редагується вільно, рядки додаються/видаляються
   │  POST /post_document/
   │  ─ валідація Σ Дт = Σ Кт
   │  ─ створення PostingGroup + PostingEntry × N
[posted] read-only; PostingGroup присутній у регістрах і звітах
   │  POST /unpost_document/
   │  ─ видалення PostingGroup
[draft]  знову редагується

Покриття тестами

backend/essentials/tests/test_journal_entry_and_fixed_asset.pyTestPostJournalEntry, TestJournalEntryEndpoint:

  • balanced entry створює PostingGroup з amount=Σ debit і двома PostingEntry;
  • unbalanced entry підіймає JournalEntryNotBalanced;
  • repost замінює попередню групу (idempotent);
  • unpost видаляє групу;
  • API /post_document/ повертає 200 для balanced, 400 для unbalanced;
  • після post — je.state == 'posted'.

Відмінність від DocumentOperation

Аспект DocumentOperation JournalEntry
Призначення Шаблонна групова проводка з заздалегідь визначеними BusinessOperation-парами Вільна одиночна проводка без шаблону
Лінії Кожна лінія — посилання на BusinessOperation (Дт/Кт визначаються шаблоном) Кожна лінія — явний account + debit/credit
Use-case "Видача готівки касою": завжди Дт 5310 / Кт 5311 "Сторно дублюючої накладної №42 від 2026-01-15"
Аналітики Залежать від BusinessOperation Вказуються вручну на кожному рядку

Обидва записують у PostingGroup + PostingEntry, тому в звітах виглядають однаково.


Use-case: зарплата без модуля HRM

Якщо HRM-модуль не встановлено (community-збірка / ранній клієнт без потреби в per-employee обліку), зарплата відбивається згорнутими проводками через JournalEntry або шаблонний DocumentOperation — за аналогією з 1С «Бухгалтерська довідка» / SAP «FI Document» / Odoo «Miscellaneous Operation».

Типовий flow за місяць (UA, спрощено):

# Проводка Дт Кт Сума
1 Нарахування ЗП 92 / 93 / 23 (за центром витрат) 661 «Розрахунки з оплати праці» gross
2 Утримання ПДФО (18%) 661 6411 «ПДФО» gross × 0.18
3 Утримання військового збору (1.5%) 661 6412 «Військовий збір» gross × 0.015
4 Нарахування ЄСВ роботодавцем (22%) 92 / 93 / 23 6510 «ЄСВ» gross × 0.22
5 Виплата net на картку 661 311 «Поточний рахунок» net
6 Сплата ПДФО / ВЗ / ЄСВ 6411 / 6412 / 6510 311 відповідні суми

Кроки 1-4 — один JournalEntry (4 рядки). Кроки 5-6 — окремі OutgoingPayment (касовий/банківський flow), або теж згорнутий JE, якщо банк-виписка ще не імпортована.

Аналітики на рядках: department, expense_item, project — для P&L drill-down. Контрагент (partner=Person) можна не заповнювати — це і є «згорнутість».

Що втрачається vs HRM: - ❌ Per-employee облік → залишок на 661 не розкладається по співробітниках. - ❌ Розрахункові листи / друкована форма для працівника. - ❌ Авто-розрахунок ставок (треба самому рахувати gross/tax/net). - ❌ Bulk-операції (один JE на місяць замість N slip-документів).

Що залишається доступним: - ✅ P&L / BalanceSheet / TrialBalance — коректно показують 661/6411/6412/6510 та витрати на 92/93/23. - ✅ CashFlow Direct Method — виплати потрапляють у «Грошові потоки від операційної діяльності». - ✅ PartyLedger — можна вести по Person (контрагент-фізособа), якщо заповнити partner на рядку.

Коли переходити на HRM: перший клієнт, що вимагає (a) per-employee розкладки залишку 661, (b) автоматичного 4-leg розрахунку UA-податків, (c) друкованої форми розрахункового листа, (d) інтеграції з табелем робочого часу. Див. hrm-payroll/README.md.


🔮 Deferred / Ideas

Шаблони ручних проводок

Мотивація: часто повторювані ручні проводки (напр. місячне нарахування резерву) дублюються вручну. Чому відкладено: для типових сценаріїв є DocumentOperation з BusinessOperation — він закриває цю потребу. Trigger: запит від користувача з конкретним сценарієм, який не вписується у DocumentOperation-flow.

Multi-currency lines

Мотивація: проводка з валютними рядками (наприклад, переоцінка USD-залишку). Чому відкладено: MVP оперує однією валютою на JE; складніше — окрема ітерація разом із dual-currency PostingEntry. Trigger: перший випадок переоцінки валютного активу клієнтом.

Reverse-on-date scheduling

Мотивація: автоматичний контр-сторно через N місяців (для нарахувань резервів). Чому відкладено: потребує task-scheduler (Celery/Cron). MVP — лише ручне сторнування. Trigger: поява потреби у регулярних reverse-проводках.