Some checks failed
CI/CD Pipeline / Run Tests (push) Failing after 37s
CI/CD Pipeline / Code Quality Checks (push) Failing after 43s
CI/CD Pipeline / Build & Push Images (push) Has been skipped
CI/CD Pipeline / Deploy (dev) (push) Has been skipped
CI/CD Pipeline / Deploy (prod) (push) Has been skipped
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 0s
CI/CD Pipeline / Run Tests (pull_request) Failing after 0s
CI/CD Pipeline / Build & Push Images (pull_request) Has been skipped
CI/CD Pipeline / Deploy (dev) (pull_request) Has been skipped
CI/CD Pipeline / Deploy (prod) (pull_request) Has been skipped
- Обновлены клиенты парсеров (checko, fns, minpromtorg, proverki, zakupki) - Добавлены новые миграции для моделей - Расширено покрытие тестами - Обновлены конфигурации и настройки проекта - Добавлены утилиты для тестирования Co-Authored-By: Warp <agent@warp.dev>
666 lines
23 KiB
Python
666 lines
23 KiB
Python
"""
|
||
Модели для приложения парсеров.
|
||
|
||
Используют миксины из 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", _("Единый реестр проверок")
|
||
PROCUREMENTS = "procurements", _("Государственные закупки")
|
||
FNS_REPORTS = "fns_reports", _("Бухгалтерская отчетность ФНС")
|
||
|
||
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"]),
|
||
]
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["source", "batch_id"],
|
||
name="unique_load_batch_per_source",
|
||
),
|
||
]
|
||
|
||
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}"
|
||
|
||
|
||
class ProcurementRecord(TimestampMixin, models.Model):
|
||
"""
|
||
Государственная закупка из ЕИС zakupki.gov.ru.
|
||
|
||
Данные загружаются из Единой информационной системы в сфере закупок.
|
||
Поддерживает закупки по 44-ФЗ и 223-ФЗ.
|
||
"""
|
||
|
||
load_batch = models.PositiveIntegerField(
|
||
_("ID пакета загрузки"),
|
||
db_index=True,
|
||
help_text=_("Идентификатор пакета загрузки"),
|
||
)
|
||
purchase_number = models.CharField(
|
||
_("реестровый номер"),
|
||
max_length=100,
|
||
db_index=True,
|
||
help_text=_("Реестровый номер закупки"),
|
||
)
|
||
purchase_name = models.TextField(
|
||
_("наименование закупки"),
|
||
help_text=_("Наименование закупки"),
|
||
)
|
||
customer_inn = models.CharField(
|
||
_("ИНН заказчика"),
|
||
max_length=20,
|
||
db_index=True,
|
||
help_text=_("ИНН заказчика"),
|
||
)
|
||
customer_kpp = models.CharField(
|
||
_("КПП заказчика"),
|
||
max_length=20,
|
||
blank=True,
|
||
help_text=_("КПП заказчика"),
|
||
)
|
||
customer_ogrn = models.CharField(
|
||
_("ОГРН заказчика"),
|
||
max_length=20,
|
||
db_index=True,
|
||
blank=True,
|
||
help_text=_("ОГРН заказчика"),
|
||
)
|
||
customer_name = models.TextField(
|
||
_("наименование заказчика"),
|
||
help_text=_("Наименование заказчика"),
|
||
)
|
||
max_price = models.CharField(
|
||
_("НМЦ"),
|
||
max_length=50,
|
||
blank=True,
|
||
help_text=_("Начальная (максимальная) цена контракта"),
|
||
)
|
||
currency_code = models.CharField(
|
||
_("валюта"),
|
||
max_length=10,
|
||
default="RUB",
|
||
help_text=_("Код валюты"),
|
||
)
|
||
placement_method = models.CharField(
|
||
_("способ определения"),
|
||
max_length=255,
|
||
blank=True,
|
||
help_text=_("Способ определения поставщика"),
|
||
)
|
||
publish_date = models.CharField(
|
||
_("дата публикации"),
|
||
max_length=30,
|
||
blank=True,
|
||
help_text=_("Дата публикации извещения"),
|
||
)
|
||
end_date = models.CharField(
|
||
_("дата окончания"),
|
||
max_length=30,
|
||
blank=True,
|
||
help_text=_("Дата окончания подачи заявок"),
|
||
)
|
||
status = models.CharField(
|
||
_("статус"),
|
||
max_length=100,
|
||
blank=True,
|
||
help_text=_("Статус закупки"),
|
||
)
|
||
law_type = models.CharField(
|
||
_("тип закона"),
|
||
max_length=20,
|
||
blank=True,
|
||
db_index=True,
|
||
help_text=_("Тип закона (44-ФЗ, 223-ФЗ)"),
|
||
)
|
||
purchase_object_info = models.TextField(
|
||
_("объект закупки"),
|
||
blank=True,
|
||
help_text=_("Информация об объекте закупки"),
|
||
)
|
||
href = models.URLField(
|
||
_("ссылка"),
|
||
blank=True,
|
||
max_length=500,
|
||
help_text=_("Ссылка на страницу закупки"),
|
||
)
|
||
region_code = models.CharField(
|
||
_("код региона"),
|
||
max_length=10,
|
||
blank=True,
|
||
db_index=True,
|
||
help_text=_("Код региона"),
|
||
)
|
||
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_procurement"
|
||
verbose_name = _("закупка")
|
||
verbose_name_plural = _("закупки")
|
||
ordering = ["-created_at"]
|
||
indexes = [
|
||
models.Index(fields=["customer_inn", "purchase_number"]),
|
||
models.Index(fields=["load_batch", "customer_inn"]),
|
||
models.Index(fields=["law_type", "data_year", "data_month"]),
|
||
]
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["purchase_number"],
|
||
name="unique_procurement_purchase_number",
|
||
),
|
||
]
|
||
|
||
def __str__(self) -> str:
|
||
name = self.purchase_name[:50] if self.purchase_name else ""
|
||
return f"{self.purchase_number} - {name}"
|
||
|
||
|
||
class FinancialReport(TimestampMixin, models.Model):
|
||
"""
|
||
Бухгалтерская отчетность организации из ФНС.
|
||
|
||
Данные загружаются из Excel файлов, полученных от ФНС.
|
||
Имя файла: fin_{external_id}_{ogrn}.xlsx
|
||
"""
|
||
|
||
class Status(models.TextChoices):
|
||
PENDING = "pending", _("Ожидает обработки")
|
||
PROCESSING = "processing", _("Обрабатывается")
|
||
SUCCESS = "success", _("Успешно")
|
||
FAILED = "failed", _("Ошибка")
|
||
|
||
class SourceType(models.TextChoices):
|
||
FILE_WATCH = "file_watch", _("Мониторинг папки")
|
||
API = "api", _("API загрузка")
|
||
|
||
external_id = models.CharField(
|
||
_("внешний ID"),
|
||
max_length=50,
|
||
unique=True,
|
||
db_index=True,
|
||
help_text=_("Уникальный идентификатор из имени файла"),
|
||
)
|
||
ogrn = models.CharField(
|
||
_("ОГРН"),
|
||
max_length=15,
|
||
db_index=True,
|
||
help_text=_("ОГРН организации"),
|
||
)
|
||
file_name = models.CharField(
|
||
_("имя файла"),
|
||
max_length=255,
|
||
help_text=_("Оригинальное имя загруженного файла"),
|
||
)
|
||
file_hash = models.CharField(
|
||
_("хеш файла"),
|
||
max_length=64,
|
||
unique=True,
|
||
help_text=_("SHA256 хеш файла для дедупликации"),
|
||
)
|
||
load_batch = models.PositiveIntegerField(
|
||
_("ID пакета загрузки"),
|
||
db_index=True,
|
||
help_text=_("Идентификатор пакета загрузки"),
|
||
)
|
||
status = models.CharField(
|
||
_("статус"),
|
||
max_length=20,
|
||
choices=Status.choices,
|
||
default=Status.PENDING,
|
||
db_index=True,
|
||
help_text=_("Статус обработки файла"),
|
||
)
|
||
error_message = models.TextField(
|
||
_("сообщение об ошибке"),
|
||
blank=True,
|
||
help_text=_("Текст ошибки при неудачной обработке"),
|
||
)
|
||
source = models.CharField(
|
||
_("источник"),
|
||
max_length=20,
|
||
choices=SourceType.choices,
|
||
help_text=_("Источник загрузки файла"),
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "parsers_financial_report"
|
||
verbose_name = _("финансовый отчет")
|
||
verbose_name_plural = _("финансовые отчеты")
|
||
ordering = ["-created_at"]
|
||
indexes = [
|
||
models.Index(fields=["ogrn", "status"]),
|
||
models.Index(fields=["load_batch", "status"]),
|
||
]
|
||
|
||
def __str__(self) -> str:
|
||
return f"{self.external_id} (ОГРН: {self.ogrn}) - {self.status}"
|
||
|
||
|
||
class FinancialReportLine(models.Model):
|
||
"""
|
||
Строка бухгалтерской отчетности.
|
||
|
||
Хранит значения по конкретной строке формы отчетности за конкретный год.
|
||
Формы: №1 (Баланс), №2 (ОФР), №3 (Изменение капитала),
|
||
№4 (Движение денежных средств), №6 (Целевое использование).
|
||
"""
|
||
|
||
report = models.ForeignKey(
|
||
FinancialReport,
|
||
on_delete=models.CASCADE,
|
||
related_name="lines",
|
||
verbose_name=_("отчет"),
|
||
)
|
||
form_code = models.CharField(
|
||
_("код формы"),
|
||
max_length=10,
|
||
db_index=True,
|
||
help_text=_("Номер формы отчетности (1, 2, 3, 4, 6)"),
|
||
)
|
||
line_code = models.CharField(
|
||
_("код строки"),
|
||
max_length=10,
|
||
db_index=True,
|
||
help_text=_("Код строки отчетности (например: 1100, 2110)"),
|
||
)
|
||
line_name = models.CharField(
|
||
_("наименование строки"),
|
||
max_length=255,
|
||
help_text=_("Наименование строки отчетности"),
|
||
)
|
||
year = models.PositiveSmallIntegerField(
|
||
_("год"),
|
||
db_index=True,
|
||
help_text=_("Отчетный год"),
|
||
)
|
||
period_start = models.BigIntegerField(
|
||
_("на начало периода"),
|
||
null=True,
|
||
blank=True,
|
||
help_text=_("Значение на начало периода (тыс. руб.)"),
|
||
)
|
||
period_end = models.BigIntegerField(
|
||
_("на конец периода"),
|
||
null=True,
|
||
blank=True,
|
||
help_text=_("Значение на конец периода (тыс. руб.)"),
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "parsers_financial_report_line"
|
||
verbose_name = _("строка финансового отчета")
|
||
verbose_name_plural = _("строки финансовых отчетов")
|
||
indexes = [
|
||
models.Index(fields=["report", "form_code", "line_code"]),
|
||
models.Index(fields=["year", "line_code"]),
|
||
]
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["report", "form_code", "line_code", "year"],
|
||
name="unique_report_line_year",
|
||
),
|
||
]
|
||
|
||
def __str__(self) -> str:
|
||
return f"{self.line_code} ({self.line_name[:30]}) - {self.year}"
|