feat(parsers): add proverki.gov.ru parser with sync_inspections task
- Add InspectionRecord model with is_federal_law_248, data_year, data_month fields - Add ProverkiClient with Playwright support for JS-rendered portal - Add streaming XML parser for large files (>50MB) - Add sync_inspections task with incremental loading logic - Starts from 01.01.2025 if DB is empty - Loads both FZ-294 and FZ-248 inspections - Stops after 2 consecutive empty months - Add InspectionService methods: get_last_loaded_period, has_data_for_period - Add Minpromtorg parsers (certificates, manufacturers) - Add Django Admin for parser models - Update README with parsers documentation and changelog
This commit is contained in:
363
src/apps/parsers/models.py
Normal file
363
src/apps/parsers/models.py
Normal file
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
Модели для приложения парсеров.
|
||||
|
||||
Используют миксины из apps.core для стандартных полей и поведения.
|
||||
"""
|
||||
|
||||
from apps.core.mixins import TimestampMixin
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ParserLoadLog(TimestampMixin, models.Model):
|
||||
"""
|
||||
Лог загрузок парсеров.
|
||||
|
||||
Хранит информацию о каждой загрузке данных из внешнего источника.
|
||||
"""
|
||||
|
||||
class Source(models.TextChoices):
|
||||
INDUSTRIAL = "industrial", _("Промышленное производство")
|
||||
MANUFACTURES = "manufactures", _("Реестр производителей")
|
||||
INSPECTIONS = "inspections", _("Единый реестр проверок")
|
||||
|
||||
batch_id = models.PositiveIntegerField(
|
||||
_("ID пакета"),
|
||||
db_index=True,
|
||||
help_text=_("Уникальный идентификатор пакета загрузки"),
|
||||
)
|
||||
source = models.CharField(
|
||||
_("источник"),
|
||||
max_length=50,
|
||||
choices=Source.choices,
|
||||
db_index=True,
|
||||
help_text=_("Источник данных"),
|
||||
)
|
||||
records_count = models.PositiveIntegerField(
|
||||
_("количество записей"),
|
||||
default=0,
|
||||
help_text=_("Количество загруженных записей"),
|
||||
)
|
||||
status = models.CharField(
|
||||
_("статус"),
|
||||
max_length=20,
|
||||
default="success",
|
||||
help_text=_("Статус загрузки"),
|
||||
)
|
||||
error_message = models.TextField(
|
||||
_("сообщение об ошибке"),
|
||||
blank=True,
|
||||
help_text=_("Текст ошибки при неудачной загрузке"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "parsers_load_log"
|
||||
verbose_name = _("лог загрузки")
|
||||
verbose_name_plural = _("логи загрузок")
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["source", "batch_id"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Load #{self.batch_id} ({self.source}) - {self.records_count} records"
|
||||
|
||||
|
||||
class IndustrialCertificateRecord(TimestampMixin, models.Model):
|
||||
"""
|
||||
Сертификат промышленного производства РФ.
|
||||
|
||||
Данные загружаются из Минпромторга.
|
||||
"""
|
||||
|
||||
load_batch = models.PositiveIntegerField(
|
||||
_("ID пакета загрузки"),
|
||||
db_index=True,
|
||||
help_text=_("Идентификатор пакета загрузки"),
|
||||
)
|
||||
issue_date = models.CharField(
|
||||
_("дата выдачи"),
|
||||
max_length=15,
|
||||
blank=True,
|
||||
help_text=_("Дата выдачи сертификата"),
|
||||
)
|
||||
certificate_number = models.CharField(
|
||||
_("номер сертификата"),
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text=_("Номер сертификата"),
|
||||
)
|
||||
expiry_date = models.CharField(
|
||||
_("дата окончания"),
|
||||
max_length=15,
|
||||
blank=True,
|
||||
help_text=_("Дата окончания действия"),
|
||||
)
|
||||
certificate_file_url = models.TextField(
|
||||
_("URL файла"),
|
||||
blank=True,
|
||||
help_text=_("Ссылка на файл сертификата"),
|
||||
)
|
||||
organisation_name = models.TextField(
|
||||
_("наименование организации"),
|
||||
help_text=_("Название организации"),
|
||||
)
|
||||
inn = models.CharField(
|
||||
_("ИНН"),
|
||||
max_length=20,
|
||||
db_index=True,
|
||||
help_text=_("ИНН организации"),
|
||||
)
|
||||
ogrn = models.CharField(
|
||||
_("ОГРН"),
|
||||
max_length=20,
|
||||
db_index=True,
|
||||
help_text=_("ОГРН организации"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "parsers_industrial_certificate"
|
||||
verbose_name = _("сертификат промпроизводства")
|
||||
verbose_name_plural = _("сертификаты промпроизводства")
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["inn", "certificate_number"]),
|
||||
models.Index(fields=["load_batch", "inn"]),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["certificate_number"],
|
||||
name="unique_certificate_number",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.certificate_number} - {self.organisation_name[:50]}"
|
||||
|
||||
|
||||
class ManufacturerRecord(TimestampMixin, models.Model):
|
||||
"""
|
||||
Производитель из реестра Минпромторга.
|
||||
|
||||
Данные загружаются из Минпромторга.
|
||||
"""
|
||||
|
||||
load_batch = models.PositiveIntegerField(
|
||||
_("ID пакета загрузки"),
|
||||
db_index=True,
|
||||
help_text=_("Идентификатор пакета загрузки"),
|
||||
)
|
||||
full_legal_name = models.TextField(
|
||||
_("полное наименование"),
|
||||
help_text=_("Полное юридическое наименование организации"),
|
||||
)
|
||||
inn = models.CharField(
|
||||
_("ИНН"),
|
||||
max_length=15,
|
||||
db_index=True,
|
||||
help_text=_("ИНН организации"),
|
||||
)
|
||||
ogrn = models.CharField(
|
||||
_("ОГРН"),
|
||||
max_length=15,
|
||||
db_index=True,
|
||||
help_text=_("ОГРН организации"),
|
||||
)
|
||||
address = models.TextField(
|
||||
_("адрес"),
|
||||
blank=True,
|
||||
help_text=_("Юридический адрес организации"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "parsers_manufacturer"
|
||||
verbose_name = _("производитель")
|
||||
verbose_name_plural = _("производители")
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["load_batch", "inn"]),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["inn"],
|
||||
name="unique_manufacturer_inn",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.inn} - {self.full_legal_name[:50]}"
|
||||
|
||||
|
||||
class Proxy(TimestampMixin, models.Model):
|
||||
"""
|
||||
Прокси-сервер для парсеров.
|
||||
|
||||
Хранит список доступных прокси для использования в клиентах.
|
||||
"""
|
||||
|
||||
address = models.CharField(
|
||||
_("адрес"),
|
||||
max_length=255,
|
||||
unique=True,
|
||||
help_text=_("Адрес прокси (например: http://proxy:8080)"),
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
_("активен"),
|
||||
default=True,
|
||||
db_index=True,
|
||||
help_text=_("Доступен ли прокси для использования"),
|
||||
)
|
||||
last_used_at = models.DateTimeField(
|
||||
_("последнее использование"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Время последнего использования"),
|
||||
)
|
||||
fail_count = models.PositiveIntegerField(
|
||||
_("количество ошибок"),
|
||||
default=0,
|
||||
help_text=_("Количество неудачных попыток подключения"),
|
||||
)
|
||||
description = models.CharField(
|
||||
_("описание"),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text=_("Описание прокси (провайдер, локация и т.д.)"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "parsers_proxy"
|
||||
verbose_name = _("прокси")
|
||||
verbose_name_plural = _("прокси")
|
||||
ordering = ["fail_count", "-last_used_at"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
status = "active" if self.is_active else "inactive"
|
||||
return f"{self.address} ({status})"
|
||||
|
||||
|
||||
class InspectionRecord(TimestampMixin, models.Model):
|
||||
"""
|
||||
Проверка из Единого реестра проверок (proverki.gov.ru).
|
||||
|
||||
Данные загружаются из ФГИС "Единый реестр проверок" (Генпрокуратура РФ).
|
||||
Поддерживает два типа проверок:
|
||||
- ФЗ-294 (традиционные проверки)
|
||||
- ФЗ-248 (новые проверки с 2021 года)
|
||||
"""
|
||||
|
||||
load_batch = models.PositiveIntegerField(
|
||||
_("ID пакета загрузки"),
|
||||
db_index=True,
|
||||
help_text=_("Идентификатор пакета загрузки"),
|
||||
)
|
||||
registration_number = models.CharField(
|
||||
_("учётный номер"),
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text=_("Учётный номер проверки в реестре"),
|
||||
)
|
||||
inn = models.CharField(
|
||||
_("ИНН"),
|
||||
max_length=20,
|
||||
db_index=True,
|
||||
help_text=_("ИНН проверяемого лица"),
|
||||
)
|
||||
ogrn = models.CharField(
|
||||
_("ОГРН"),
|
||||
max_length=20,
|
||||
db_index=True,
|
||||
blank=True,
|
||||
help_text=_("ОГРН проверяемого лица"),
|
||||
)
|
||||
organisation_name = models.TextField(
|
||||
_("наименование организации"),
|
||||
help_text=_("Наименование проверяемого лица"),
|
||||
)
|
||||
control_authority = models.TextField(
|
||||
_("контрольный орган"),
|
||||
blank=True,
|
||||
help_text=_("Наименование контрольного (надзорного) органа"),
|
||||
)
|
||||
inspection_type = models.CharField(
|
||||
_("тип проверки"),
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text=_("Тип проверки (плановая/внеплановая)"),
|
||||
)
|
||||
inspection_form = models.CharField(
|
||||
_("форма проверки"),
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text=_("Форма проверки (документарная/выездная)"),
|
||||
)
|
||||
start_date = models.CharField(
|
||||
_("дата начала"),
|
||||
max_length=20,
|
||||
blank=True,
|
||||
help_text=_("Дата начала проверки"),
|
||||
)
|
||||
end_date = models.CharField(
|
||||
_("дата окончания"),
|
||||
max_length=20,
|
||||
blank=True,
|
||||
help_text=_("Дата окончания проверки"),
|
||||
)
|
||||
status = models.CharField(
|
||||
_("статус"),
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text=_("Статус проверки"),
|
||||
)
|
||||
legal_basis = models.CharField(
|
||||
_("правовое основание"),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text=_("Правовое основание проверки (ФЗ-294, ФЗ-248)"),
|
||||
)
|
||||
result = models.TextField(
|
||||
_("результат"),
|
||||
blank=True,
|
||||
help_text=_("Результат проверки"),
|
||||
)
|
||||
is_federal_law_248 = models.BooleanField(
|
||||
_("по ФЗ-248"),
|
||||
default=False,
|
||||
db_index=True,
|
||||
help_text=_("Проверка по ФЗ-248 (новые проверки с 2021 года)"),
|
||||
)
|
||||
data_year = models.PositiveSmallIntegerField(
|
||||
_("год данных"),
|
||||
db_index=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Год, за который загружены данные"),
|
||||
)
|
||||
data_month = models.PositiveSmallIntegerField(
|
||||
_("месяц данных"),
|
||||
db_index=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Месяц, за который загружены данные"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "parsers_inspection"
|
||||
verbose_name = _("проверка")
|
||||
verbose_name_plural = _("проверки")
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["inn", "registration_number"]),
|
||||
models.Index(fields=["load_batch", "inn"]),
|
||||
models.Index(fields=["is_federal_law_248", "data_year", "data_month"]),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["registration_number"],
|
||||
name="unique_inspection_registration_number",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
org_name = self.organisation_name[:50] if self.organisation_name else ""
|
||||
return f"{self.registration_number} - {org_name}"
|
||||
Reference in New Issue
Block a user