405 lines
15 KiB
Python
405 lines
15 KiB
Python
"""
|
||
Сервисы для работы с формой Ф-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)
|