Add initial implementations for forms and organization apps with serializers, factories, and admin configurations
Some checks failed
CI/CD Pipeline / Run Tests (push) Failing after 45s
CI/CD Pipeline / Code Quality Checks (push) Failing after 48s
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

This commit is contained in:
2026-03-28 18:23:06 +01:00
parent 8ed3e1175c
commit 345b1d0cc8
201 changed files with 15097 additions and 6691 deletions

View File

@@ -15,6 +15,7 @@ from apps.core.excel import (
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
@@ -24,15 +25,35 @@ from django.db.models import Count, Max
logger = logging.getLogger(__name__)
class FormF6Service(BulkOperationsMixin, BaseService[FormF6Record]):
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"))
@@ -47,7 +68,7 @@ class FormF6Service(BulkOperationsMixin, BaseService[FormF6Record]):
)
class FormF6Parser(BaseExcelParser[FormF6Record]):
class FormF6Parser(ReportingPeriodParserMixin, BaseExcelParser[FormF6Record]):
"""Парсер Excel файла формы Ф-6 (Возрастная структура оборудования)."""
ORG_NAME_COLUMN = 0
@@ -62,8 +83,12 @@ class FormF6Parser(BaseExcelParser[FormF6Record]):
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(
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"),
@@ -78,19 +103,38 @@ class FormF6Parser(BaseExcelParser[FormF6Record]):
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(
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"),
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, batch_id: int) -> FormF6Record:
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={
@@ -100,13 +144,20 @@ class FormF6Parser(BaseExcelParser[FormF6Record]):
"kpp": row_data.kpp or "",
},
)
return FormF6Record.objects.create(
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) -> ParseResult:
parser = FormF6Parser()
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)