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

@@ -7,6 +7,7 @@ from apps.parsers.models import (
InspectionRecord,
ManufacturerRecord,
ParserLoadLog,
ProcurementRecord,
Proxy,
)
from django.contrib import admin
@@ -385,3 +386,137 @@ class InspectionRecordAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
"""Запретить редактирование записей."""
return False
@admin.register(ProcurementRecord)
class ProcurementRecordAdmin(admin.ModelAdmin):
"""Admin для государственных закупок."""
list_display = [
"purchase_number",
"purchase_name_short",
"customer_inn",
"customer_name_short",
"max_price",
"law_type",
"status_badge",
"publish_date",
"load_batch",
]
list_filter = [
"law_type",
"status",
"region_code",
"load_batch",
"created_at",
]
search_fields = [
"purchase_number",
"purchase_name",
"customer_inn",
"customer_ogrn",
"customer_name",
]
readonly_fields = ["created_at", "updated_at", "load_batch"]
ordering = ["-created_at"]
list_per_page = 100
date_hierarchy = "created_at"
fieldsets = (
(
"Закупка",
{
"fields": (
"purchase_number",
"purchase_name",
"purchase_object_info",
"law_type",
"status",
)
},
),
(
"Заказчик",
{
"fields": (
"customer_name",
"customer_inn",
"customer_kpp",
"customer_ogrn",
)
},
),
(
"Финансы",
{"fields": ("max_price", "currency_code", "placement_method")},
),
(
"Сроки",
{"fields": ("publish_date", "end_date")},
),
(
"Дополнительно",
{"fields": ("region_code", "href"), "classes": ("collapse",)},
),
(
"Системное",
{
"fields": (
"load_batch",
"data_year",
"data_month",
"created_at",
"updated_at",
),
"classes": ("collapse",),
},
),
)
def purchase_name_short(self, obj):
"""Сокращённое наименование закупки."""
name = obj.purchase_name or ""
return name[:50] + "..." if len(name) > 50 else name
purchase_name_short.short_description = "Наименование"
purchase_name_short.admin_order_field = "purchase_name"
def customer_name_short(self, obj):
"""Сокращённое наименование заказчика."""
name = obj.customer_name or ""
return name[:30] + "..." if len(name) > 30 else name
customer_name_short.short_description = "Заказчик"
customer_name_short.admin_order_field = "customer_name"
def status_badge(self, obj):
"""Цветной бейдж статуса."""
status = obj.status or ""
status_lower = status.lower()
if "опублик" in status_lower or "подача" in status_lower:
color = "#28a745"
elif "завершен" in status_lower or "состоял" in status_lower:
color = "#17a2b8"
elif "отменен" in status_lower or "не состоял" in status_lower:
color = "#dc3545"
else:
color = "#6c757d"
return format_html(
'<span style="color: white; background: {}; padding: 2px 8px; '
'border-radius: 3px; font-size: 11px;">{}</span>',
color,
status[:20] if len(status) > 20 else status,
)
status_badge.short_description = "Статус"
status_badge.admin_order_field = "status"
def has_add_permission(self, request):
"""Запретить создание записей вручную."""
return False
def has_change_permission(self, request, obj=None):
"""Запретить редактирование записей."""
return False