feat(parsers): add proverki.gov.ru parser with sync_inspections task
- Add InspectionRecord model with is_federal_law_248, data_year, data_month fields - Add ProverkiClient with Playwright support for JS-rendered portal - Add streaming XML parser for large files (>50MB) - Add sync_inspections task with incremental loading logic - Starts from 01.01.2025 if DB is empty - Loads both FZ-294 and FZ-248 inspections - Stops after 2 consecutive empty months - Add InspectionService methods: get_last_loaded_period, has_data_for_period - Add Minpromtorg parsers (certificates, manufacturers) - Add Django Admin for parser models - Update README with parsers documentation and changelog
This commit is contained in:
90
src/apps/parsers/migrations/0001_initial_parsers.py
Normal file
90
src/apps/parsers/migrations/0001_initial_parsers.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# Generated by Django 3.2.25 on 2026-01-21 16:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IndustrialCertificateRecord',
|
||||
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 пакета загрузки')),
|
||||
('issue_date', models.CharField(blank=True, help_text='Дата выдачи сертификата', max_length=15, verbose_name='дата выдачи')),
|
||||
('certificate_number', models.CharField(db_index=True, help_text='Номер сертификата', max_length=100, verbose_name='номер сертификата')),
|
||||
('expiry_date', models.CharField(blank=True, help_text='Дата окончания действия', max_length=15, verbose_name='дата окончания')),
|
||||
('certificate_file_url', models.TextField(blank=True, help_text='Ссылка на файл сертификата', verbose_name='URL файла')),
|
||||
('organisation_name', models.TextField(help_text='Название организации', verbose_name='наименование организации')),
|
||||
('inn', models.CharField(db_index=True, help_text='ИНН организации', max_length=20, verbose_name='ИНН')),
|
||||
('ogrn', models.CharField(db_index=True, help_text='ОГРН организации', max_length=20, verbose_name='ОГРН')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'сертификат промпроизводства',
|
||||
'verbose_name_plural': 'сертификаты промпроизводства',
|
||||
'db_table': 'parsers_industrial_certificate',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManufacturerRecord',
|
||||
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 пакета загрузки')),
|
||||
('full_legal_name', models.TextField(help_text='Полное юридическое наименование организации', verbose_name='полное наименование')),
|
||||
('inn', models.CharField(db_index=True, help_text='ИНН организации', max_length=15, verbose_name='ИНН')),
|
||||
('ogrn', models.CharField(db_index=True, help_text='ОГРН организации', max_length=15, verbose_name='ОГРН')),
|
||||
('address', models.TextField(blank=True, help_text='Юридический адрес организации', verbose_name='адрес')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'производитель',
|
||||
'verbose_name_plural': 'производители',
|
||||
'db_table': 'parsers_manufacturer',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParserLoadLog',
|
||||
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='обновлено')),
|
||||
('batch_id', models.PositiveIntegerField(db_index=True, help_text='Уникальный идентификатор пакета загрузки', verbose_name='ID пакета')),
|
||||
('source', models.CharField(choices=[('industrial', 'Промышленное производство'), ('manufactures', 'Реестр производителей')], db_index=True, help_text='Источник данных', max_length=50, verbose_name='источник')),
|
||||
('records_count', models.PositiveIntegerField(default=0, help_text='Количество загруженных записей', verbose_name='количество записей')),
|
||||
('status', models.CharField(default='success', help_text='Статус загрузки', max_length=20, verbose_name='статус')),
|
||||
('error_message', models.TextField(blank=True, help_text='Текст ошибки при неудачной загрузке', verbose_name='сообщение об ошибке')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'лог загрузки',
|
||||
'verbose_name_plural': 'логи загрузок',
|
||||
'db_table': 'parsers_load_log',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='parserloadlog',
|
||||
index=models.Index(fields=['source', 'batch_id'], name='parsers_loa_source_f175af_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='manufacturerrecord',
|
||||
index=models.Index(fields=['load_batch', 'inn'], name='parsers_man_load_ba_5a660e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='industrialcertificaterecord',
|
||||
index=models.Index(fields=['inn', 'certificate_number'], name='parsers_ind_inn_6b7f8d_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='industrialcertificaterecord',
|
||||
index=models.Index(fields=['load_batch', 'inn'], name='parsers_ind_load_ba_6e497e_idx'),
|
||||
),
|
||||
]
|
||||
32
src/apps/parsers/migrations/0002_add_proxy_model.py
Normal file
32
src/apps/parsers/migrations/0002_add_proxy_model.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 3.2.25 on 2026-01-21 16:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('parsers', '0001_initial_parsers'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Proxy',
|
||||
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='обновлено')),
|
||||
('address', models.CharField(help_text='Адрес прокси (например: http://proxy:8080)', max_length=255, unique=True, verbose_name='адрес')),
|
||||
('is_active', models.BooleanField(db_index=True, default=True, help_text='Доступен ли прокси для использования', verbose_name='активен')),
|
||||
('last_used_at', models.DateTimeField(blank=True, help_text='Время последнего использования', null=True, verbose_name='последнее использование')),
|
||||
('fail_count', models.PositiveIntegerField(default=0, help_text='Количество неудачных попыток подключения', verbose_name='количество ошибок')),
|
||||
('description', models.CharField(blank=True, help_text='Описание прокси (провайдер, локация и т.д.)', max_length=255, verbose_name='описание')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'прокси',
|
||||
'verbose_name_plural': 'прокси',
|
||||
'db_table': 'parsers_proxy',
|
||||
'ordering': ['fail_count', '-last_used_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
53
src/apps/parsers/migrations/0003_add_inspection_model.py
Normal file
53
src/apps/parsers/migrations/0003_add_inspection_model.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 3.2.25 on 2026-01-21 17:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('parsers', '0002_add_proxy_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InspectionRecord',
|
||||
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 пакета загрузки')),
|
||||
('registration_number', models.CharField(db_index=True, help_text='Учётный номер проверки в реестре', max_length=100, verbose_name='учётный номер')),
|
||||
('inn', models.CharField(db_index=True, help_text='ИНН проверяемого лица', max_length=20, verbose_name='ИНН')),
|
||||
('ogrn', models.CharField(blank=True, db_index=True, help_text='ОГРН проверяемого лица', max_length=20, verbose_name='ОГРН')),
|
||||
('organisation_name', models.TextField(help_text='Наименование проверяемого лица', verbose_name='наименование организации')),
|
||||
('control_authority', models.TextField(blank=True, help_text='Наименование контрольного (надзорного) органа', verbose_name='контрольный орган')),
|
||||
('inspection_type', models.CharField(blank=True, help_text='Тип проверки (плановая/внеплановая)', max_length=100, verbose_name='тип проверки')),
|
||||
('inspection_form', models.CharField(blank=True, help_text='Форма проверки (документарная/выездная)', max_length=100, verbose_name='форма проверки')),
|
||||
('start_date', models.CharField(blank=True, help_text='Дата начала проверки', max_length=20, verbose_name='дата начала')),
|
||||
('end_date', models.CharField(blank=True, help_text='Дата окончания проверки', max_length=20, verbose_name='дата окончания')),
|
||||
('status', models.CharField(blank=True, help_text='Статус проверки', max_length=100, verbose_name='статус')),
|
||||
('legal_basis', models.CharField(blank=True, help_text='Правовое основание проверки (ФЗ-294, ФЗ-248)', max_length=255, verbose_name='правовое основание')),
|
||||
('result', models.TextField(blank=True, help_text='Результат проверки', verbose_name='результат')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'проверка',
|
||||
'verbose_name_plural': 'проверки',
|
||||
'db_table': 'parsers_inspection',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='parserloadlog',
|
||||
name='source',
|
||||
field=models.CharField(choices=[('industrial', 'Промышленное производство'), ('manufactures', 'Реестр производителей'), ('inspections', 'Единый реестр проверок')], db_index=True, help_text='Источник данных', max_length=50, verbose_name='источник'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='inspectionrecord',
|
||||
index=models.Index(fields=['inn', 'registration_number'], name='parsers_ins_inn_0d75e5_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='inspectionrecord',
|
||||
index=models.Index(fields=['load_batch', 'inn'], name='parsers_ins_load_ba_45a131_idx'),
|
||||
),
|
||||
]
|
||||
25
src/apps/parsers/migrations/0004_add_unique_constraints.py
Normal file
25
src/apps/parsers/migrations/0004_add_unique_constraints.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.25 on 2026-01-21 17:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('parsers', '0003_add_inspection_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='industrialcertificaterecord',
|
||||
constraint=models.UniqueConstraint(fields=('certificate_number',), name='unique_certificate_number'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='inspectionrecord',
|
||||
constraint=models.UniqueConstraint(fields=('registration_number',), name='unique_inspection_registration_number'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='manufacturerrecord',
|
||||
constraint=models.UniqueConstraint(fields=('inn',), name='unique_manufacturer_inn'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 3.2.25 on 2026-01-21 19:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('parsers', '0004_add_unique_constraints'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='inspectionrecord',
|
||||
name='data_month',
|
||||
field=models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Месяц, за который загружены данные', null=True, verbose_name='месяц данных'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inspectionrecord',
|
||||
name='data_year',
|
||||
field=models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Год, за который загружены данные', null=True, verbose_name='год данных'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inspectionrecord',
|
||||
name='is_federal_law_248',
|
||||
field=models.BooleanField(db_index=True, default=False, help_text='Проверка по ФЗ-248 (новые проверки с 2021 года)', verbose_name='по ФЗ-248'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='inspectionrecord',
|
||||
index=models.Index(fields=['is_federal_law_248', 'data_year', 'data_month'], name='parsers_ins_is_fede_e271e9_idx'),
|
||||
),
|
||||
]
|
||||
0
src/apps/parsers/migrations/__init__.py
Normal file
0
src/apps/parsers/migrations/__init__.py
Normal file
Reference in New Issue
Block a user