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