Files
state-corp-backend/src/apps/form_6/services.py
Aleksandr Meshchriakov 697ecb7d1c
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 3m50s
CI/CD Pipeline / Run Tests (push) Successful in 3m57s
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
Implement exchange imports and frontend reporting APIs
2026-04-07 16:31:04 +02:00

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