""" Модели для приложения парсеров. Используют миксины из 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}"