feat(parsers): добавлен парсер zakupki.gov.ru с SOAP API интеграцией

Реализована полная интеграция с ЕИС Закупки через SOAP API
(FTP доступ закрыт с 01.01.2025).

Добавлено:
- ZakupkiClient с поддержкой SOAP методов getDocsByOrgRegionRequest
  и getDocsByReestrNumberRequest
- Модель ProcurementRecord (18 полей, 3 индекса)
- ProcurementService и ParserLoadLogService для бизнес-логики
- Celery задачи parse_procurements и sync_procurements
- Админка с цветовой индикацией статусов и фильтрами
- 71 тест (unit + E2E с RUN_E2E_TESTS=1)

Требования: токен SOAP API через Госуслуги

🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
2026-01-27 16:01:28 +01:00
parent 199d871923
commit c6483d8427
16 changed files with 3405 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ class ParserLoadLog(TimestampMixin, models.Model):
INDUSTRIAL = "industrial", _("Промышленное производство")
MANUFACTURES = "manufactures", _("Реестр производителей")
INSPECTIONS = "inspections", _("Единый реестр проверок")
PROCUREMENTS = "procurements", _("Государственные закупки")
batch_id = models.PositiveIntegerField(
_("ID пакета"),
@@ -361,3 +362,147 @@ class InspectionRecord(TimestampMixin, models.Model):
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}"