Files
mostovik-backend/src/apps/parsers/models.py
Aleksandr Meshchriakov c344bef777
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 28s
CI/CD Pipeline / Build and Push Images (push) Successful in 16s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 23s
feat: restore registry procurement collection
2026-07-19 17:46:15 +02:00

1152 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Модели для приложения парсеров.
Используют миксины из 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 "Настройки парсинга"