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

@@ -0,0 +1,67 @@
# Generated by Django 3.2.25 on 2026-01-27 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parsers', '0005_add_inspection_fz248_fields'),
]
operations = [
migrations.CreateModel(
name='ProcurementRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True, help_text='Дата и время создания записи', verbose_name='создано')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего обновления', verbose_name='обновлено')),
('load_batch', models.PositiveIntegerField(db_index=True, help_text='Идентификатор пакета загрузки', verbose_name='ID пакета загрузки')),
('purchase_number', models.CharField(db_index=True, help_text='Реестровый номер закупки', max_length=100, verbose_name='реестровый номер')),
('purchase_name', models.TextField(help_text='Наименование закупки', verbose_name='наименование закупки')),
('customer_inn', models.CharField(db_index=True, help_text='ИНН заказчика', max_length=20, verbose_name='ИНН заказчика')),
('customer_kpp', models.CharField(blank=True, help_text='КПП заказчика', max_length=20, verbose_name='КПП заказчика')),
('customer_ogrn', models.CharField(blank=True, db_index=True, help_text='ОГРН заказчика', max_length=20, verbose_name='ОГРН заказчика')),
('customer_name', models.TextField(help_text='Наименование заказчика', verbose_name='наименование заказчика')),
('max_price', models.CharField(blank=True, help_text='Начальная (максимальная) цена контракта', max_length=50, verbose_name='НМЦ')),
('currency_code', models.CharField(default='RUB', help_text='Код валюты', max_length=10, verbose_name='валюта')),
('placement_method', models.CharField(blank=True, help_text='Способ определения поставщика', max_length=255, verbose_name='способ определения')),
('publish_date', models.CharField(blank=True, help_text='Дата публикации извещения', max_length=30, verbose_name='дата публикации')),
('end_date', models.CharField(blank=True, help_text='Дата окончания подачи заявок', max_length=30, verbose_name='дата окончания')),
('status', models.CharField(blank=True, help_text='Статус закупки', max_length=100, verbose_name='статус')),
('law_type', models.CharField(blank=True, db_index=True, help_text='Тип закона (44-ФЗ, 223-ФЗ)', max_length=20, verbose_name='тип закона')),
('purchase_object_info', models.TextField(blank=True, help_text='Информация об объекте закупки', verbose_name='объект закупки')),
('href', models.URLField(blank=True, help_text='Ссылка на страницу закупки', max_length=500, verbose_name='ссылка')),
('region_code', models.CharField(blank=True, db_index=True, help_text='Код региона', max_length=10, verbose_name='код региона')),
('data_year', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Год, за который загружены данные', null=True, verbose_name='год данных')),
('data_month', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Месяц, за который загружены данные', null=True, verbose_name='месяц данных')),
],
options={
'verbose_name': 'закупка',
'verbose_name_plural': 'закупки',
'db_table': 'parsers_procurement',
'ordering': ['-created_at'],
},
),
migrations.AlterField(
model_name='parserloadlog',
name='source',
field=models.CharField(choices=[('industrial', 'Промышленное производство'), ('manufactures', 'Реестр производителей'), ('inspections', 'Единый реестр проверок'), ('procurements', 'Государственные закупки')], db_index=True, help_text='Источник данных', max_length=50, verbose_name='источник'),
),
migrations.AddIndex(
model_name='procurementrecord',
index=models.Index(fields=['customer_inn', 'purchase_number'], name='parsers_pro_custome_8e0271_idx'),
),
migrations.AddIndex(
model_name='procurementrecord',
index=models.Index(fields=['load_batch', 'customer_inn'], name='parsers_pro_load_ba_ca8e7f_idx'),
),
migrations.AddIndex(
model_name='procurementrecord',
index=models.Index(fields=['law_type', 'data_year', 'data_month'], name='parsers_pro_law_typ_5a53c9_idx'),
),
migrations.AddConstraint(
model_name='procurementrecord',
constraint=models.UniqueConstraint(fields=('purchase_number',), name='unique_procurement_purchase_number'),
),
]