Files
state-corp-backend/src/apps/form_2/services.py
Aleksandr Meshchriakov d112d1715f
All checks were successful
CI/CD Pipeline / Code Quality Checks (push) Successful in 1m36s
CI/CD Pipeline / Run Tests (push) Successful in 2m12s
CI/CD Pipeline / Build and Push Dev Images (push) Successful in 37s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 24s
fix: create organizations from form uploads
2026-06-02 00:28:09 +02:00

405 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Сервисы для работы с формой Ф-2.
Содержит:
- FormF2Service - CRUD операции
- FormF2Parser - парсинг Excel
"""
import logging
from typing import Any
from apps.core.excel import (
BaseExcelParser,
ColumnMapping,
ParseResult,
RowData,
)
from apps.core.reporting import ReportingPeriodParserMixin, VersionedReportServiceMixin
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,
VersionedReportServiceMixin[FormF2Record],
BaseService[FormF2Record],
):
"""
Сервис для работы с записями формы Ф-2.
Методы:
get_by_batch: Получить записи по номеру загрузки
get_next_batch_id: Получить следующий номер загрузки
get_batches: Получить список загрузок
"""
model = FormF2Record
@classmethod
def get_by_organization(cls, organization_id):
"""Получить записи организации."""
return cls.get_queryset().filter(organization_id=organization_id)
@classmethod
def get_by_batch(cls, batch_id: int):
"""Получить записи по номеру загрузки."""
return cls.get_queryset().filter(load_batch=batch_id)
@classmethod
def get_by_load_batch(cls, batch_id: int):
"""Совместимость со старым API сервиса."""
return cls.get_by_batch(batch_id)
@classmethod
def delete_by_load_batch(cls, batch_id: int) -> int:
"""Удалить записи по номеру загрузки."""
deleted_count, _ = cls.get_queryset().filter(load_batch=batch_id).delete()
return deleted_count
@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(ReportingPeriodParserMixin, 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 | dict[str, Any],
batch_id: int | None = None,
) -> FormF2Record:
"""Создать запись формы Ф-2."""
row_data = self._normalize_row_data(row_data)
batch_id = batch_id or getattr(self, "load_batch", self.get_next_batch_id())
org = OrganizationService.get_or_create_from_form_identifiers(
name=row_data.organization_name,
inn=row_data.inn,
ogrn=row_data.ogrn,
kpp=row_data.kpp,
okpo=row_data.okpo,
)
record = FormF2Service.create_versioned_record(
organization=org,
load_batch=batch_id,
report_year=self.report_year,
report_quarter=self.report_quarter,
**row_data.fields,
)
return record
def parse_form_f2_file(
file,
*,
report_year: int,
report_quarter: int | None = None,
) -> ParseResult:
"""
Парсит Excel файл формы Ф-2.
Args:
file: Загруженный файл
Returns:
ParseResult с результатами парсинга
"""
parser = FormF2Parser(report_year=report_year, report_quarter=report_quarter)
return parser.parse(file)