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

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() → Decimalacquisition_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]

  1. Якщо asset.status != 'active' — return None.
  2. Якщо вже існує DepreciationEntry(asset, year, month) — повертає його (idempotent).
  3. Обчислює amount = asset.monthly_depreciation(). Якщо ≤ 0 — return None.
  4. Створює PostingGroup (document_type='Depreciation', document_id=asset.pk) на дату кінця місяця.
  5. Створює два PostingEntry:
  6. Дт expense_account / Ct=0department копіюється з активу)
  7. Дт=0 / Кт asset_account
  8. Створює DepreciationEntry з посиланням на PostingGroup.
  9. Інкрементує 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:

{ module: "essentials", apiPath: "depreciation-entries", entityCode: "depreciationEntries" }

Розташування: 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.pyTestFixedAssetMath, 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.