Add initial implementations for forms and organization apps with serializers, factories, and admin configurations
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 5m5s
CI/CD Pipeline / Run Tests (push) Failing after 5m5s
CI/CD Pipeline / Build Docker Images (push) Has been skipped
CI/CD Pipeline / Push to Gitea Registry (push) Has been skipped
CI/CD Pipeline / Deploy to Server (push) Has been skipped

This commit is contained in:
2026-02-17 09:26:08 +01:00
parent fd2adf9ab4
commit 8ed3e1175c
119 changed files with 9091 additions and 0 deletions

188
src/apps/form_2/services.py Normal file
View File

@@ -0,0 +1,188 @@
"""
Сервисы для работы с формой Ф-2.
Содержит:
- FormF2Service - CRUD операции
- FormF2Parser - парсинг Excel
"""
import logging
from typing import Any
from apps.core.excel import (
BaseExcelParser,
ColumnMapping,
ParseResult,
RowData,
)
from apps.core.services import BaseService, BulkOperationsMixin
from apps.form_2.models import FormF2Record
from apps.organization.services import OrganizationService
from django.db import transaction
from django.db.models import Count, Max
logger = logging.getLogger(__name__)
class FormF2Service(BulkOperationsMixin, BaseService[FormF2Record]):
"""
Сервис для работы с записями формы Ф-2.
Методы:
get_by_batch: Получить записи по номеру загрузки
get_next_batch_id: Получить следующий номер загрузки
get_batches: Получить список загрузок
"""
model = FormF2Record
@classmethod
def get_by_batch(cls, batch_id: int):
"""Получить записи по номеру загрузки."""
return cls.get_queryset().filter(load_batch=batch_id)
@classmethod
def get_next_batch_id(cls) -> int:
"""Получить следующий номер загрузки."""
max_batch = cls.model.objects.aggregate(max_batch=Max("load_batch"))
return (max_batch["max_batch"] or 0) + 1
@classmethod
def get_batches(cls) -> list[dict[str, Any]]:
"""Получить список загрузок с количеством записей."""
return list(
cls.model.objects.values("load_batch")
.annotate(
count=Count("id"),
created_at=Max("created_at"),
)
.order_by("-load_batch")
)
class FormF2Parser(BaseExcelParser[FormF2Record]):
"""
Парсер Excel файла формы Ф-2 (Бухгалтерский баланс).
Колонки:
0: Наименование организации
1: ОКПО
2: ОГРН
3: ИНН
4+: Данные баланса
"""
ORG_NAME_COLUMN = 0
OKPO_COLUMN = 1
OGRN_COLUMN = 2
INN_COLUMN = 3
def get_column_mappings(self) -> list[ColumnMapping]:
"""Маппинг колонок Excel на поля модели."""
return [
# I. Внеоборотные активы
ColumnMapping(4, "Нематериальные активы", "intangible_assets", field_type="decimal"),
ColumnMapping(5, "Результаты исследований и разработок", "rd_results", field_type="decimal"),
ColumnMapping(6, "Нематериальные поисковые активы", "intangible_search_assets", field_type="decimal"),
ColumnMapping(7, "Материальные поисковые активы", "tangible_search_assets", field_type="decimal"),
ColumnMapping(8, "Основные средства", "fixed_assets", field_type="decimal"),
ColumnMapping(9, "Доходные вложения в материальные ценности", "profitable_investments", field_type="decimal"),
ColumnMapping(10, "Финансовые вложения (внеоборотные)", "financial_investments_non_current", field_type="decimal"),
ColumnMapping(11, "Отложенные налоговые активы", "deferred_tax_assets", field_type="decimal"),
ColumnMapping(12, "Прочие внеоборотные активы", "other_non_current_assets", field_type="decimal"),
ColumnMapping(13, "Итого внеоборотные активы", "total_non_current_assets", field_type="decimal"),
# II. Оборотные активы
ColumnMapping(14, "Запасы", "inventories", field_type="decimal"),
ColumnMapping(15, "НДС по приобретённым ценностям", "vat_on_acquired_assets", field_type="decimal"),
ColumnMapping(16, "Дебиторская задолженность", "receivables", field_type="decimal"),
ColumnMapping(17, "Финансовые вложения (оборотные)", "financial_investments_current", field_type="decimal"),
ColumnMapping(18, "Денежные средства и эквиваленты", "cash_and_equivalents", field_type="decimal"),
ColumnMapping(19, "Прочие оборотные активы", "other_current_assets", field_type="decimal"),
ColumnMapping(20, "Итого оборотные активы", "total_current_assets", field_type="decimal"),
ColumnMapping(21, "Баланс (актив)", "total_assets", field_type="decimal"),
# III. Капитал и резервы
ColumnMapping(22, "Уставный капитал", "authorized_capital", field_type="decimal"),
ColumnMapping(23, "Собственные акции, выкупленные у акционеров", "own_shares_bought_back", field_type="decimal"),
ColumnMapping(24, "Переоценка внеоборотных активов", "revaluation_of_non_current_assets", field_type="decimal"),
ColumnMapping(25, "Добавочный капитал", "additional_capital", field_type="decimal"),
ColumnMapping(26, "Резервный капитал", "reserve_capital", field_type="decimal"),
ColumnMapping(27, "Нераспределённая прибыль", "retained_earnings", field_type="decimal"),
ColumnMapping(28, "Итого капитал и резервы", "total_equity", field_type="decimal"),
# IV. Долгосрочные обязательства
ColumnMapping(29, "Заёмные средства (долгосрочные)", "borrowings_non_current", field_type="decimal"),
ColumnMapping(30, "Отложенные налоговые обязательства", "deferred_tax_liabilities", field_type="decimal"),
ColumnMapping(31, "Оценочные обязательства (долгосрочные)", "estimated_liabilities_non_current", field_type="decimal"),
ColumnMapping(32, "Прочие обязательства (долгосрочные)", "other_liabilities_non_current", field_type="decimal"),
ColumnMapping(33, "Итого долгосрочные обязательства", "total_non_current_liabilities", field_type="decimal"),
# V. Краткосрочные обязательства
ColumnMapping(34, "Заёмные средства (краткосрочные)", "borrowings_current", field_type="decimal"),
ColumnMapping(35, "Кредиторская задолженность", "payables", field_type="decimal"),
ColumnMapping(36, "Доходы будущих периодов", "deferred_income", field_type="decimal"),
ColumnMapping(37, "Оценочные обязательства (краткосрочные)", "estimated_liabilities_current", field_type="decimal"),
ColumnMapping(38, "Прочие обязательства (краткосрочные)", "other_liabilities_current", field_type="decimal"),
ColumnMapping(39, "Итого краткосрочные обязательства", "total_current_liabilities", field_type="decimal"),
ColumnMapping(40, "Баланс (пассив)", "total_liabilities", field_type="decimal"),
# Отчёт о финансовых результатах
ColumnMapping(41, "Выручка", "revenue", field_type="decimal"),
ColumnMapping(42, "Себестоимость продаж", "cost_of_sales", field_type="decimal"),
ColumnMapping(43, "Валовая прибыль", "gross_profit", field_type="decimal"),
ColumnMapping(44, "Коммерческие расходы", "selling_expenses", field_type="decimal"),
ColumnMapping(45, "Управленческие расходы", "administrative_expenses", field_type="decimal"),
ColumnMapping(46, "Прибыль от продаж", "profit_from_sales", field_type="decimal"),
ColumnMapping(47, "Проценты к получению", "interest_receivable", field_type="decimal"),
ColumnMapping(48, "Проценты к уплате", "interest_payable", field_type="decimal"),
ColumnMapping(49, "Прочие доходы", "other_income", field_type="decimal"),
ColumnMapping(50, "Прочие расходы", "other_expenses", field_type="decimal"),
ColumnMapping(51, "Прибыль до налогообложения", "profit_before_tax", field_type="decimal"),
ColumnMapping(52, "Текущий налог на прибыль", "income_tax", field_type="decimal"),
ColumnMapping(53, "Чистая прибыль", "net_profit", field_type="decimal"),
# Дополнительные показатели
ColumnMapping(54, "EBITDA", "ebitda", field_type="decimal"),
ColumnMapping(55, "Амортизация", "depreciation", field_type="decimal"),
ColumnMapping(56, "Оборотный капитал", "working_capital", field_type="decimal"),
ColumnMapping(57, "Чистый долг", "net_debt", field_type="decimal"),
# Прошлый период
ColumnMapping(58, "Баланс (актив) - прошлый период", "total_assets_prev", field_type="decimal"),
ColumnMapping(59, "Баланс (пассив) - прошлый период", "total_liabilities_prev", field_type="decimal"),
ColumnMapping(60, "Выручка - прошлый период", "revenue_prev", field_type="decimal"),
ColumnMapping(61, "Чистая прибыль - прошлый период", "net_profit_prev", field_type="decimal"),
]
def get_next_batch_id(self) -> int:
"""Получить следующий номер загрузки."""
return FormF2Service.get_next_batch_id()
@transaction.atomic
def create_record(self, row_data: RowData, batch_id: int) -> FormF2Record:
"""Создать запись формы Ф-2."""
org, _ = OrganizationService.get_or_create_by_inn(
inn=row_data.inn,
defaults={
"name": row_data.organization_name,
"ogrn": row_data.ogrn or "",
"okpo": row_data.okpo or "",
"kpp": row_data.kpp or "",
},
)
record = FormF2Record.objects.create(
organization=org,
load_batch=batch_id,
**row_data.fields,
)
return record
def parse_form_f2_file(file) -> ParseResult:
"""
Парсит Excel файл формы Ф-2.
Args:
file: Загруженный файл
Returns:
ParseResult с результатами парсинга
"""
parser = FormF2Parser()
return parser.parse(file)