Fixed Assets — основні засоби¶
Статус: ✅ Реалізовано (MVP, 2026-04-22)
Облік основних засобів (транспорт, обладнання, будівлі, ІТ-техніка) з трьома методами амортизації, idempotent monthly run, реєстром проведених амортизаційних операцій.
Призначення¶
Будь-який актив із вартістю > 0 і строком корисного використання > 1 місяця має поступово переноситися в собівартість через амортизацію. До цього MVP у DOP можна було вести лише первинні активи через Item/StockTransaction, але без амортизації — вся вартість списувалася разом, що спотворювало P&L.
Тепер:
- Купили автомобіль за 600 000 ₴ зі строком 5 років → монтуємо як FixedAsset зі useful_life_months=60.
- Кожного місяця натискаємо «Run Monthly Depreciation» → автоматично проводиться Дт 6810 / Кт 1100 на 10 000 ₴.
- Через 5 років accumulated_depreciation == acquisition_cost, net_book_value == 0.
- Після dispose актив виводиться з активного fleet (status='disposed').
Моделі¶
FixedAsset (MasterDataModel)¶
| Поле | Тип | Опис |
|---|---|---|
code, name, name_ua |
str | З MasterDataModel |
category |
str | Free-form (vehicle / machine / building / equipment / IT / other) |
acquisition_date |
date | Дата придбання |
acquisition_cost |
Decimal | Початкова вартість (Дт asset_account при оприбуткуванні) |
salvage_value |
Decimal | Залишкова вартість на кінець строку (default 0) |
useful_life_months |
int | Строк корисного використання у місяцях (default 60) |
depreciation_method |
enum | linear / accelerated (×2 linear) / none |
accumulated_depreciation |
Decimal | Накопичена амортизація — поступово зростає |
asset_account |
FK ChartOfAccounts | Балансовий рахунок активу (PCG class 1: 1010, 1020, 1100…) |
expense_account |
FK ChartOfAccounts | Рахунок витрат на амортизацію (default: 6810) |
organization |
FK Organization | Опціонально — поділ за юр. особою |
department |
FK Department | Опціонально — поділ за підрозділом |
status |
enum | active / disposed |
disposal_date |
date | Дата вибуття (заповнюється при dispose) |
Helper-методи:
- net_book_value() → Decimal — acquisition_cost − accumulated_depreciation.
- monthly_depreciation() → Decimal — повертає скільки треба нарахувати цього місяця згідно з методом, з кепом за залишковою вартістю (не пере-амортизує понад acquisition_cost − salvage_value).
DepreciationEntry (RegisterModel)¶
Один запис на (asset, period_year, period_month) — UNIQUE constraint забезпечує idempotency.
| Поле | Тип | Опис |
|---|---|---|
asset |
FK FixedAsset | Об'єкт амортизації |
period_year |
int | Рік періоду |
period_month |
int | Місяць (1..12) |
amount |
Decimal | Сума амортизації за період |
posting_group |
FK PostingGroup | Посилання на створену проводку (для drill-down) |
date |
date | Кінець місяця (як у RegisterModel) |
Сервіси¶
essentials/services/depreciation.py
post_depreciation(asset, year, month) → Optional[DepreciationEntry]¶
- Якщо
asset.status != 'active'— return None. - Якщо вже існує
DepreciationEntry(asset, year, month)— повертає його (idempotent). - Обчислює
amount = asset.monthly_depreciation(). Якщо ≤ 0 — return None. - Створює
PostingGroup(document_type='Depreciation', document_id=asset.pk) на дату кінця місяця. - Створює два
PostingEntry: Дт expense_account / Ct=0(зdepartmentкопіюється з активу)Дт=0 / Кт asset_account- Створює
DepreciationEntryз посиланням наPostingGroup. - Інкрементує
asset.accumulated_depreciation.
run_monthly_depreciation(tenant, year, month) → List[DepreciationEntry]¶
Цикл по всіх FixedAsset(tenant, status='active', deletion_mark=False) з викликом post_depreciation. Idempotent — повторний запуск не створює дублів. Повертає лише ті записи, що були реально створені (None-результати фільтруються).
API¶
| Метод | URL | Опис |
|---|---|---|
GET/POST/PATCH/DELETE |
/api/v1/essentials/fixed-assets/ |
CRUD активів |
POST |
/api/v1/essentials/fixed-assets/{id}/depreciate/ |
Body: {year, month} — провести амортизацію конкретного активу за один період |
POST |
/api/v1/essentials/fixed-assets/run-monthly/ |
Body: {year, month} — масовий run для tenant'а |
POST |
/api/v1/essentials/fixed-assets/{id}/dispose/ |
Body: {disposal_date?} — позначити як disposed |
GET |
/api/v1/essentials/depreciation-entries/?asset={id}&period_year={Y}&period_month={M} |
Read-only журнал |
FixedAssetSerializer повертає обчислювані поля: net_book_value, monthly_depreciation, months_remaining.
Frontend¶
Картка активу¶
Через generic MasterDataPage у конфізі essentials.ts зареєстровано fixedAssets (ItemType.MASTERDATA, plugin: "accounting"). UI підтримує всі стандартні CRUD-операції автоматично через EntityRegistry.
Розташування у sidebar: Essentials → Fixed Assets → Master & Registers → Fixed Assets.
Журнал амортизації¶
depreciationEntries зареєстровано як ItemType.JOURNAL з componentProps:
Розташування: Essentials → Fixed Assets → Master & Registers → Depreciation History.
Process page для запуску amortization¶
Амортизацію також можна запустити як крок процесу Month Closing — обидва варіанти викликають один і той самий сервіс
run_monthly_depreciation()і є ідемпотентними.
components/Essentials/FixedAsset/DepreciationRunner.tsx — спеціальна сторінка з:
- Селекти року + місяця (default: поточний).
- Кнопка «Run depreciation» → POST run-monthly/.
- Таблиця проведених DepreciationEntry за обраний період (count, total amount, окремі рядки з asset code/name/amount).
- Інфо-alert про idempotency.
Розташування: Essentials → Fixed Assets → Processes → Run Monthly Depreciation. Зареєстровано в ProcessPanel.tsx як case 'depreciationRun'.
Workflow¶
[active] FixedAsset створено, accumulated_depreciation = 0
│
│ Щомісяця: POST /fixed-assets/run-monthly/ {year, month}
│ ─ для кожного активу: PostingGroup (Дт 6810 / Кт asset_account)
│ ─ accumulated_depreciation += monthly_depreciation
│ ─ DepreciationEntry створюється раз на (asset, period)
▼
[active] через N місяців: net_book_value → 0
│
│ POST /fixed-assets/{id}/dispose/ {disposal_date}
▼
[disposed] не бере участі в run-monthly
Покриття тестами¶
backend/essentials/tests/test_journal_entry_and_fixed_asset.py — TestFixedAssetMath, TestPostDepreciation, TestFixedAssetEndpoint:
- Лінійний метод:
cost / months. - Прискорений: ×2 від лінійного.
noneповертає 0.salvage_valueзменшує depreciable base.- Кеп: останній місяць не може перевищити залишкову вартість.
- Idempotency: повторний
post_depreciation(asset, 2026, 1)повертає той самий entry, не створює другий. - Disposed-актив пропускається.
- Через 12 місяців 12-місячного активу:
accumulated == cost,net_book_value == 0. Подальші виклики повертають None. - API:
depreciate,run-monthly,dispose— всі статус 200, side-effects відображаються.
Обмеження MVP¶
| Що відсутнє | Як обходимо | Trigger для розширення |
|---|---|---|
| Перенесення між підрозділами / організаціями | Update FK напряму через PATCH; історія не зберігається | Запит на disposal/transfer history |
| Часткове списання (partial write-down) | Тільки повне dispose |
Бухгалтерський сценарій частково знеціненого активу |
| Переоцінка (impairment / revaluation) | Не реалізовано | Запит ПСБО / IFRS-облікової політики |
| Multi-currency cost | Cost і depreciation у валюті tenant'а | Імпорт активу зарубіжного придбання |
| Auto-run cron | Тільки manual button через UI / API | Production deploy із Celery beat |
| Generation of fixed-asset cards (інвентарний номер, штрих-код, друкована форма) | Не реалізовано | Запит обліку матеріальних цінностей |
| Інвентаризація з GAP-аналізом | Не реалізовано | Підготовка до інвентаризаційної кампанії |
🔮 Deferred / Ideas¶
Auto-monthly cron¶
Мотивація: ручний клік щомісяця — easy to forget. Чому відкладено: потребує task-scheduler (Celery beat або APScheduler) і error-recovery. Trigger: перший production deploy.
Component-level depreciation¶
Мотивація: двигун автомобіля має свій ресурс і амортизується окремо від кузова. Чому відкладено: ускладнює модель (FixedAsset + FixedAssetComponent), а в практиці клієнти зазвичай ведуть весь автомобіль як один актив. Trigger: запит на componentized PP&E з виробничого підприємства.
Тax depreciation register (паралельний облік)¶
Мотивація: податкова амортизація може йти за іншими ставками, ніж бухгалтерська.
Чому відкладено: UA-податок — окрема локалізація (див. accounting-ua у accounting-tax/README.md).
Trigger: разом з UA-локалізацією бухгалтерії.
Friendly disposal — sale transaction¶
Мотивація: disposal через продаж має створити проводку Дт 5310/4100 / Кт asset_account + проводку прибутку/збитку від вибуття.
Чому відкладено: MVP — лише статус-маркер; реальна проводка робиться вручну через JournalEntry.
Trigger: запит автоматизації disposal-flow.