""" Модели для приложения парсеров. Используют миксины из apps.core для стандартных полей и поведения. """ from apps.core.mixins import TimestampMixin from django.db import models from django.utils.translation import gettext_lazy as _ VACANCY_RECORD_SOURCES = ("trudvsem", "hh", "superjob") class ParserLoadLog(TimestampMixin, models.Model): """ Лог загрузок парсеров. Хранит информацию о каждой загрузке данных из внешнего источника. """ class Source(models.TextChoices): INDUSTRIAL = "industrial", _("Сертификаты промышленного производства") INDUSTRIAL_PRODUCTS = "industrial_products", _("Реестр промышленной продукции") MANUFACTURES = "manufactures", _("Реестр производителей") INSPECTIONS = "inspections", _("Единый реестр проверок") PROCUREMENTS = "procurements", _("Единая информационная система закупок") FNS_REPORTS = "fns_reports", _("Бухгалтерская отчетность ФНС") PROCUREMENTS_44FZ = "procurements_44fz", _("Закупки 44-ФЗ") PROCUREMENTS_223FZ = "procurements_223fz", _("Закупки 223-ФЗ") CONTRACTS = "contracts", _("Контракты ЕИС") UNFAIR_SUPPLIERS = "unfair_suppliers", _("Недобросовестные поставщики") FAS_GOZ = "fas_goz", _("Уклонение от ГОЗ") ARBITRATION = "arbitration", _("Арбитражные дела") FEDRESURS_BANKRUPTCY = "fedresurs_bankruptcy", _("Банкротства Федресурс") FSTEC = "fstec", _("Реестры ФСТЭК") TRUDVSEM = "trudvsem", _("Вакансии Работа России") class Status(models.TextChoices): SUCCESS = "success", _("Успешно") FAILED = "failed", _("Ошибка") IN_PROGRESS = "in_progress", _("В процессе") PENDING = "pending", _("В очереди") SKIPPED = "skipped", _("Пропущено") 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, choices=Status.choices, default=Status.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 ParserBatchSequence(TimestampMixin, models.Model): """Атомарный счётчик batch_id по источнику парсера.""" source = models.CharField( _("источник"), max_length=50, choices=ParserLoadLog.Source.choices, unique=True, help_text=_("Источник данных"), ) next_batch_id = models.PositiveIntegerField( _("следующий ID пакета"), default=1, help_text=_("Следующий batch_id для источника"), ) class Meta: db_table = "parsers_batch_sequence" verbose_name = _("счётчик пакетов парсера") verbose_name_plural = _("счётчики пакетов парсеров") ordering = ["source"] def __str__(self) -> str: return f"{self.source}: next batch {self.next_batch_id}" class CheckoCollectionAttempt(TimestampMixin, models.Model): """Monthly Checko collection claim for one organization and source.""" class Source(models.TextChoices): ARBITRATION = "arbitration", _("Арбитражные дела") BANKRUPTCY = "bankruptcy", _("Банкротства") CONTRACTS = "contracts", _("Контракты") INSPECTIONS = "inspections", _("Проверки") PROCUREMENTS_44FZ = "procurements_44fz", _("Закупки 44-ФЗ") PROCUREMENTS_223FZ = "procurements_223fz", _("Закупки 223-ФЗ") UNFAIR_SUPPLIERS = "unfair_suppliers", _("Недобросовестные поставщики") class Status(models.TextChoices): IN_PROGRESS = "in_progress", _("В процессе") SUCCESS = "success", _("Успешно") FAILED = "failed", _("Ошибка") organization = models.ForeignKey( "organizations.Organization", on_delete=models.CASCADE, related_name="checko_collection_attempts", verbose_name=_("организация"), ) source = models.CharField( _("источник"), max_length=32, choices=Source.choices, ) period_month = models.DateField( _("месяц сбора"), help_text=_("Первый день календарного месяца, за который занята попытка"), ) status = models.CharField( _("статус"), max_length=20, choices=Status.choices, default=Status.IN_PROGRESS, ) records_count = models.PositiveIntegerField(_("количество записей"), default=0) error_message = models.TextField(_("сообщение об ошибке"), blank=True) class Meta: db_table = "parsers_checko_collection_attempt" verbose_name = _("попытка сбора Checko") verbose_name_plural = _("попытки сбора Checko") ordering = ["-period_month", "source", "organization_id"] constraints = [ models.UniqueConstraint( fields=["organization", "source", "period_month"], name="unique_checko_collection_per_org_source_month", ), ] indexes = [ models.Index(fields=["source", "period_month"]), ] def __str__(self) -> str: return f"{self.organization_id}: {self.source} ({self.period_month:%Y-%m})" GENERIC_RECORD_SOURCE_CHOICES = [ *ParserLoadLog.Source.choices, ("hh", _("Вакансии HeadHunter")), ("superjob", _("Вакансии SuperJob")), ] 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=_("Дата выдачи сертификата"), ) issue_date_normalized = models.DateField( _("дата выдачи (нормализованная)"), null=True, blank=True, db_index=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=_("Дата окончания действия"), ) expiry_date_normalized = models.DateField( _("дата окончания (нормализованная)"), null=True, blank=True, db_index=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=_("ОГРН организации"), ) registry_organization = models.ForeignKey( "organizations.Organization", on_delete=models.SET_NULL, null=True, blank=True, related_name="industrial_certificate_records", verbose_name=_("организация"), 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=_("Юридический адрес организации"), ) registry_organization = models.ForeignKey( "organizations.Organization", on_delete=models.SET_NULL, null=True, blank=True, related_name="manufacturer_records", verbose_name=_("организация"), 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 IndustrialProductRecord(TimestampMixin, models.Model): """ Промышленная продукция из реестра Минпромторга. Данные загружаются из Минпромторга. """ load_batch = models.PositiveIntegerField( _("ID пакета загрузки"), db_index=True, help_text=_("Идентификатор пакета загрузки"), ) full_organisation_name = models.TextField( _("полное наименование организации"), help_text=_("Полное наименование организации"), ) ogrn = models.CharField( _("ОГРН"), max_length=15, db_index=True, help_text=_("ОГРН организации"), ) inn = models.CharField( _("ИНН"), max_length=12, db_index=True, help_text=_("ИНН организации"), ) registry_number = models.CharField( _("регистрационный номер"), max_length=100, db_index=True, help_text=_("Регистрационный номер записи"), ) product_name = models.TextField( _("наименование продукции"), help_text=_("Наименование продукции"), ) product_model = models.CharField( _("модель или модификация"), max_length=255, blank=True, help_text=_("Модель или модификация продукции"), ) okpd2_code = models.CharField( _("код ОКПД2"), max_length=255, blank=True, help_text=_("Код по ОКПД2"), ) tnved_code = models.CharField( _("код ТН ВЭД"), max_length=255, blank=True, help_text=_("Код по ТН ВЭД"), ) regulatory_document = models.CharField( _("нормативный документ"), max_length=500, blank=True, help_text=_("Наименование нормативного документа"), ) registry_organization = models.ForeignKey( "organizations.Organization", on_delete=models.SET_NULL, null=True, blank=True, related_name="industrial_product_records", verbose_name=_("организация"), help_text=_("Связь с верхнеуровневой организацией"), ) class Meta: db_table = "parsers_industrial_product" verbose_name = _("запись реестра промышленной продукции") verbose_name_plural = _("Реестр промышленной продукции") ordering = ["-created_at"] indexes = [ models.Index(fields=["load_batch", "inn"]), models.Index(fields=["inn", "registry_number"]), ] def __str__(self) -> str: return f"{self.registry_number} - {self.product_name[:50]}" class GenericParserRecord(TimestampMixin, models.Model): """Универсальная запись для разнородных официальных источников.""" load_batch = models.PositiveIntegerField( _("ID пакета загрузки"), db_index=True, help_text=_("Идентификатор пакета загрузки"), ) source = models.CharField( _("источник"), max_length=50, choices=GENERIC_RECORD_SOURCE_CHOICES, db_index=True, help_text=_("Источник данных"), ) external_id = models.CharField( _("внешний ID"), max_length=255, db_index=True, help_text=_("Стабильный идентификатор записи во внешнем источнике"), ) inn = models.CharField( _("ИНН"), max_length=20, blank=True, db_index=True, help_text=_("ИНН организации, если есть в источнике"), ) ogrn = models.CharField( _("ОГРН"), max_length=20, blank=True, db_index=True, help_text=_("ОГРН организации, если есть в источнике"), ) organisation_name = models.TextField( _("наименование организации"), blank=True, help_text=_("Наименование организации из источника"), ) title = models.TextField( _("заголовок"), blank=True, help_text=_("Краткое описание записи"), ) record_date = models.CharField( _("дата записи"), max_length=255, blank=True, db_index=True, help_text=_("Дата записи в формате источника"), ) amount = models.DecimalField( _("сумма"), max_digits=20, decimal_places=2, null=True, blank=True, help_text=_("Сумма, если источник её содержит"), ) status = models.CharField( _("статус"), max_length=255, blank=True, help_text=_("Статус записи во внешнем источнике"), ) url = models.TextField( _("URL"), blank=True, help_text=_("Ссылка на карточку/документ во внешнем источнике"), ) payload = models.JSONField( _("исходные данные"), default=dict, blank=True, help_text=_("Нормализованный исходный документ"), ) registry_organization = models.ForeignKey( "organizations.Organization", on_delete=models.SET_NULL, null=True, blank=True, related_name="generic_parser_records", verbose_name=_("организация"), help_text=_("Связь с верхнеуровневой организацией"), ) class Meta: db_table = "parsers_generic_record" verbose_name = _("запись внешнего источника") verbose_name_plural = _("записи внешних источников") ordering = ["-created_at"] indexes = [ models.Index(fields=["source", "load_batch"]), models.Index(fields=["source", "inn"]), models.Index(fields=["source", "ogrn"]), models.Index(fields=["source", "record_date"]), ] constraints = [ models.UniqueConstraint( fields=["source", "external_id"], name="unique_generic_source_external_id", ), ] def __str__(self) -> str: label = self.organisation_name or self.title or self.external_id return f"{self.source}: {label[:80]}" 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=_("Описание прокси (провайдер, локация и т.д.)"), ) source = models.CharField( _("источник"), max_length=50, default="manual", db_index=True, help_text=_("Источник прокси (например: manual, proxy-tools)"), ) country_code = models.CharField( _("код страны"), max_length=2, default="RU", db_index=True, help_text=_("ISO-3166 код страны прокси, например RU"), ) 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=_("Дата начала проверки"), ) start_date_normalized = models.DateField( _("дата начала (нормализованная)"), null=True, blank=True, db_index=True, help_text=_("Нормализованная дата начала проверки"), ) end_date = models.CharField( _("дата окончания"), max_length=20, blank=True, help_text=_("Дата окончания проверки"), ) end_date_normalized = models.DateField( _("дата окончания (нормализованная)"), null=True, blank=True, db_index=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=_("Месяц, за который загружены данные"), ) registry_organization = models.ForeignKey( "organizations.Organization", on_delete=models.SET_NULL, null=True, blank=True, related_name="inspection_records", verbose_name=_("организация"), 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=_("Начальная (максимальная) цена контракта"), ) max_price_amount = models.DecimalField( _("НМЦ (число)"), max_digits=20, decimal_places=2, null=True, blank=True, db_index=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=_("Дата публикации извещения"), ) publish_date_normalized = models.DateField( _("дата публикации (нормализованная)"), null=True, blank=True, db_index=True, help_text=_("Нормализованная дата публикации извещения"), ) end_date = models.CharField( _("дата окончания"), max_length=30, blank=True, help_text=_("Дата окончания подачи заявок"), ) end_date_normalized = models.DateField( _("дата окончания (нормализованная)"), null=True, blank=True, db_index=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=_("Месяц, за который загружены данные"), ) registry_organization = models.ForeignKey( "organizations.Organization", on_delete=models.SET_NULL, null=True, blank=True, related_name="procurement_records", verbose_name=_("организация"), 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=_("ОГРН организации"), ) registry_organization = models.ForeignKey( "organizations.Organization", on_delete=models.SET_NULL, null=True, blank=True, related_name="financial_reports", verbose_name=_("организация"), 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}" class ParsingSettings(TimestampMixin, models.Model): """Singleton-настройки периодичности обновления источников парсинга.""" class Frequency(models.TextChoices): DAILY = "daily", _("Ежедневно") WEEKLY = "weekly", _("Еженедельно") MONTHLY = "monthly", _("Ежемесячно") YEARLY = "yearly", _("Ежегодно") singleton_key = models.PositiveSmallIntegerField( _("ключ singleton-записи"), default=1, unique=True, editable=False, ) manufacturers_and_products = models.CharField( _("производители и продукция"), max_length=20, choices=Frequency.choices, default=Frequency.DAILY, ) public_procurements = models.CharField( _("госзакупки"), max_length=20, choices=Frequency.choices, default=Frequency.DAILY, ) defense_unreliable_suppliers = models.CharField( _("недобросовестные поставщики ОПК"), max_length=20, choices=Frequency.choices, default=Frequency.WEEKLY, ) planned_inspections = models.CharField( _("плановые проверки"), max_length=20, choices=Frequency.choices, default=Frequency.MONTHLY, ) arbitration_cases = models.CharField( _("арбитражные дела"), max_length=20, choices=Frequency.choices, default=Frequency.DAILY, ) bankruptcy_procedures = models.CharField( _("банкротные процедуры"), max_length=20, choices=Frequency.choices, default=Frequency.DAILY, ) information_security_registries = models.CharField( _("реестры информационной безопасности"), max_length=20, choices=Frequency.choices, default=Frequency.YEARLY, ) class Meta: db_table = "parsers_parsing_settings" verbose_name = _("настройки парсинга") verbose_name_plural = _("настройки парсинга") def __str__(self) -> str: return "Настройки парсинга"