""" Сервисы для работы с формой Ф-6. Содержит: - FormF6Service - CRUD операции - FormF6Parser - парсинг Excel """ import logging from typing import Any from django.db import transaction from django.db.models import Count, Max 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_6.models import FormF6Record from apps.organization.services import OrganizationService logger = logging.getLogger(__name__) class FormF6Service( BulkOperationsMixin, VersionedReportServiceMixin[FormF6Record], BaseService[FormF6Record], ): """Сервис для работы с записями формы Ф-6.""" model = FormF6Record @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 FormF6Parser(ReportingPeriodParserMixin, BaseExcelParser[FormF6Record]): """Парсер Excel файла формы Ф-6 (Возрастная структура оборудования).""" ORG_NAME_COLUMN = 0 OKPO_COLUMN = 1 OGRN_COLUMN = 2 INN_COLUMN = 3 def get_column_mappings(self) -> list[ColumnMapping]: return [ # Категоризация ColumnMapping(4, "Код строки", "row_code", field_type="str"), ColumnMapping(5, "Категория оборудования", "category", field_type="str"), # Общие данные ColumnMapping(6, "Всего оборудования", "total_equipment", field_type="int"), ColumnMapping( 7, "Отечественное оборудование", "domestic_equipment", field_type="int" ), ColumnMapping( 8, "Импортное оборудование", "imported_equipment", field_type="int" ), # Возрастная структура ColumnMapping(9, "До 5 лет", "age_under_5", field_type="int"), ColumnMapping(10, "5-10 лет", "age_5_10", field_type="int"), ColumnMapping(11, "10-15 лет", "age_10_15", field_type="int"), ColumnMapping(12, "15-20 лет", "age_15_20", field_type="int"), ColumnMapping(13, "Свыше 20 лет", "age_over_20", field_type="int"), # С ЧПУ ColumnMapping(14, "С ЧПУ всего", "cnc_total", field_type="int"), ColumnMapping(15, "С ЧПУ до 5 лет", "cnc_under_5", field_type="int"), ColumnMapping(16, "С ЧПУ 5-10 лет", "cnc_5_10", field_type="int"), ColumnMapping(17, "С ЧПУ 10-15 лет", "cnc_10_15", field_type="int"), ColumnMapping(18, "С ЧПУ 15-20 лет", "cnc_15_20", field_type="int"), ColumnMapping(19, "С ЧПУ свыше 20 лет", "cnc_over_20", field_type="int"), # Показатели ColumnMapping( 20, "Средняя сменность работы", "avg_shift_work", field_type="decimal" ), ColumnMapping( 21, "Коэффициент загрузки", "utilization_rate", field_type="decimal" ), ColumnMapping( 22, "Физический износ, %", "physical_wear_percent", field_type="decimal" ), # Потребности ColumnMapping( 23, "Рабочие места без оборудования", "workplaces_without_equipment", field_type="int", ), ColumnMapping( 24, "Оборудование к замене", "equipment_to_replace", field_type="int" ), ] def get_next_batch_id(self) -> int: return FormF6Service.get_next_batch_id() @transaction.atomic def create_record( self, row_data: RowData | dict[str, Any], batch_id: int | None = None, ) -> FormF6Record: 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_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 "", }, ) return FormF6Service.create_versioned_record( organization=org, load_batch=batch_id, report_year=self.report_year, report_quarter=self.report_quarter, **row_data.fields, ) def parse_form_f6_file( file, *, report_year: int, report_quarter: int | None = None, ) -> ParseResult: parser = FormF6Parser(report_year=report_year, report_quarter=report_quarter) return parser.parse(file)