diff --git a/docs/RUKOVODSTVO_ADMINISTRATORA.md b/docs/RUKOVODSTVO_ADMINISTRATORA.md new file mode 100644 index 0000000..278ea5f --- /dev/null +++ b/docs/RUKOVODSTVO_ADMINISTRATORA.md @@ -0,0 +1,594 @@ +# РУКОВОДСТВО АДМИНИСТРАТОРА +## Система ETL MOSTOVIK + +Версия документа: 1.0 +Дата: 2026-01-21 + +--- + +## СОДЕРЖАНИЕ + +1. [Общие сведения](#1-общие-сведения) +2. [Архитектура системы](#2-архитектура-системы) +3. [Развёртывание системы](#3-развёртывание-системы) +4. [Конфигурирование](#4-конфигурирование) +5. [Управление службами](#5-управление-службами) +6. [Мониторинг и логирование](#6-мониторинг-и-логирование) +7. [Резервное копирование](#7-резервное-копирование) +8. [Безопасность](#8-безопасность) +9. [Устранение неисправностей](#9-устранение-неисправностей) + +--- + +## 1. ОБЩИЕ СВЕДЕНИЯ + +### 1.1. Назначение системы + +MOSTOVIK — система ETL (Extract, Transform, Load) для сбора и обработки данных из государственных источников: + +- **Минпромторг** (minpromtorg.gov.ru) — сертификаты промышленного производства, реестр производителей +- **Единый реестр проверок** (proverki.gov.ru) — данные о проверках по ФЗ-294 и ФЗ-248 +- **ФНС** — бухгалтерская отчётность + +### 1.2. Технические характеристики + +| Параметр | Значение | +|----------|----------| +| Язык программирования | Python 3.11 | +| Фреймворк | Django 3.2.25 | +| База данных | PostgreSQL 15.10 | +| Кеш/брокер | Redis 7.x | +| Очереди задач | Celery 5.3.6 | +| Веб-сервер | Apache 2.4.57 + Gunicorn | +| Автоматизация браузера | Playwright 1.52+ | + +--- + +## 2. АРХИТЕКТУРА СИСТЕМЫ + +### 2.1. Компоненты системы + +``` ++-------------------------------------------------------------+ +| Apache 2.4.57 | +| (HTTPS termination) | ++-----------------------------+-------------------------------+ + | + v ++-------------------------------------------------------------+ +| Gunicorn WSGI | +| (3 workers, gevent) | ++-----------------------------+-------------------------------+ + | + v ++-------------------------------------------------------------+ +| Django Application | +| +--------------+ +--------------+ +------------------+ | +| | Parsers | | Core | | User | | +| | (apps) | | (apps) | | (apps) | | +| +--------------+ +--------------+ +------------------+ | ++-----------------------------+-------------------------------+ + | + +-------------+-------------+ + v v v ++---------------+ +-----------+ +---------------+ +| PostgreSQL | | Redis | | File System | +| (port 5432) | | (port 6379)| | (logs/media) | ++---------------+ +-----------+ +---------------+ +``` + +### 2.2. Службы Celery + +| Служба | Назначение | Порт | +|--------|------------|------| +| celery-worker | Выполнение фоновых задач | - | +| celery-beat | Планирование периодических задач | - | +| flower | Мониторинг Celery (опционально) | 5555 | + +### 2.3. Структура каталогов + +``` +/var/www/project/ +├── src/ # Исходный код Django +│ ├── config/ # Конфигурация +│ │ ├── settings/ # Настройки (dev, prod, test) +│ │ ├── celery.py # Конфигурация Celery +│ │ └── wsgi.py # Точка входа WSGI +│ ├── apps/ # Приложения Django +│ │ ├── core/ # Базовые компоненты +│ │ ├── parsers/ # Парсеры данных +│ │ └── user/ # Пользователи и аутентификация +│ └── manage.py # Утилита управления +├── venv/ # Виртуальное окружение Python +├── logs/ # Логи приложения +├── media/ # Пользовательские файлы +├── staticfiles/ # Статические файлы +└── deploy/ # Файлы развёртывания + ├── systemd/ # Unit-файлы systemd + ├── apache/ # Конфигурация Apache + └── scripts/ # Скрипты развёртывания +``` + +--- + +## 3. РАЗВЁРТЫВАНИЕ СИСТЕМЫ + +### 3.1. Системные требования + +**Минимальные:** +- ЦПУ: 2 ядра +- ОЗУ: 4 ГБ +- Диск: 20 ГБ +- ОС: Astra Linux / Ubuntu 20.04+ + +**Рекомендуемые:** +- ЦПУ: 4 ядра +- ОЗУ: 8 ГБ +- Диск: 50 ГБ SSD +- ОС: Astra Linux Special Edition + +### 3.2. Предварительная настройка + +#### 3.2.1. Установка системных зависимостей + +```bash +apt-get update && apt-get upgrade -y + +apt-get install -y \ + python3.11 \ + python3.11-venv \ + python3.11-dev \ + postgresql-15 \ + postgresql-client-15 \ + redis-server \ + apache2 \ + libapache2-mod-wsgi-py3 \ + git \ + build-essential \ + libpq-dev \ + libffi-dev \ + libxml2-dev \ + libxslt1-dev \ + zlib1g-dev +``` + +#### 3.2.2. Установка менеджера пакетов uv + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +source $HOME/.cargo/env +``` + +### 3.3. Автоматическое развёртывание + +```bash +chmod +x deploy/scripts/deploy.sh +sudo ./deploy/scripts/deploy.sh +``` + +Скрипт выполняет: +- Обновление системы +- Установку зависимостей +- Клонирование репозитория +- Создание виртуального окружения +- Установку зависимостей Python +- Настройку базы данных +- Выполнение миграций +- Настройку systemd и Apache + +### 3.4. Ручное развёртывание + +#### 3.4.1. Настройка базы данных + +```bash +sudo -u postgres psql < . +git checkout main + +# Создание виртуального окружения +uv venv ${PROJECT_PATH}/venv +source ${PROJECT_PATH}/venv/bin/activate + +# Установка зависимостей +uv pip install --upgrade pip +uv pip install -r requirements.txt +``` + +#### 3.4.3. Настройка окружения + +```bash +cp .env.example .env +nano .env +``` + +**Обязательные параметры:** + +| Переменная | Описание | Пример | +|------------|----------|--------| +| SECRET_KEY | Ключ шифрования Django | django-insecure-... | +| POSTGRES_DB | Имя базы данных | mostovik_prod | +| POSTGRES_USER | Пользователь БД | mostovik_user | +| POSTGRES_PASSWORD | Пароль БД | secure_password | +| POSTGRES_HOST | Узел БД | 127.0.0.1 | +| POSTGRES_PORT | Порт БД | 5432 | +| REDIS_URL | URL Redis | redis://127.0.0.1:6379/0 | +| CELERY_BROKER_URL | URL брокера | redis://127.0.0.1:6379/0 | + +#### 3.4.4. Выполнение миграций + +```bash +cd ${PROJECT_PATH}/src +python manage.py makemigrations +python manage.py migrate +python manage.py collectstatic --noinput +``` + +#### 3.4.5. Создание суперпользователя + +```bash +python manage.py createsuperuser +``` + +Или автоматически: + +```bash +echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@example.com', 'adminpass') if not User.objects.filter(username='admin').exists() else None" | python manage.py shell +``` + +--- + +## 4. КОНФИГУРИРОВАНИЕ + +### 4.1. Настройка Apache + +Конфигурационный файл: /etc/apache2/sites-available/project.conf + +**Основные параметры для изменения:** + +```apache +ServerName your-domain.com +SSLCertificateFile /etc/ssl/certs/your-cert.pem +SSLCertificateKeyFile /etc/ssl/private/your-key.pem +SSLCertificateChainFile /etc/ssl/certs/your-chain.pem +``` + +**Активация сайта:** + +```bash +a2ensite project.conf +a2enmod ssl rewrite headers expires +a2dissite 000-default +systemctl restart apache2 +``` + +### 4.2. Настройка systemd + +Файлы служб расположены в /etc/systemd/system/: + +| Файл | Служба | +|------|--------| +| gunicorn.service | WSGI-сервер Gunicorn | +| celery-worker.service | Worker Celery | +| celery-beat.service | Планировщик Celery | + +**Перезапуск после изменений:** + +```bash +systemctl daemon-reload +systemctl restart gunicorn +systemctl restart celery-worker +systemctl restart celery-beat +``` + +### 4.3. Настройка брандмауэра + +```bash +ufw allow 80/tcp +ufw allow 443/tcp +ufw deny 5432/tcp +ufw deny 6379/tcp +ufw enable +``` + +--- + +## 5. УПРАВЛЕНИЕ СЛУЖБАМИ + +### 5.1. Проверка состояния служб + +```bash +systemctl status gunicorn +systemctl status celery-worker +systemctl status celery-beat +systemctl status apache2 +systemctl status postgresql +systemctl status redis-server +``` + +### 5.2. Запуск/остановка/перезапуск + +```bash +systemctl start gunicorn +systemctl stop gunicorn +systemctl restart gunicorn + +systemctl start celery-worker +systemctl stop celery-worker +systemctl restart celery-worker + +systemctl start celery-beat +systemctl stop celery-beat +systemctl restart celery-beat + +systemctl restart apache2 +``` + +### 5.3. Включение автозагрузки + +```bash +systemctl enable gunicorn celery-worker celery-beat apache2 postgresql redis-server +``` + +### 5.4. Управление через Makefile + +```bash +cd /var/www/project + +make migrate +make createsuperuser +make shell +make logs +``` + +--- + +## 6. МОНИТОРИНГ И ЛОГИРОВАНИЕ + +### 6.1. Логи приложения + +| Лог | Путь | +|-----|------| +| Django | /var/www/project/logs/django.log | +| Celery | /var/www/project/logs/celery.log | +| Gunicorn | /var/log/gunicorn/error.log | +| Apache error | /var/log/apache2/project_error.log | +| Apache access | /var/log/apache2/project_access.log | +| PostgreSQL | /var/log/postgresql/postgresql-15-main.log | +| Redis | /var/log/redis/redis-server.log | + +### 6.2. Просмотр логов в реальном времени + +```bash +tail -f /var/www/project/logs/django.log +tail -f /var/www/project/logs/celery.log + +journalctl -u gunicorn -f +journalctl -u celery-worker -f +journalctl -u celery-beat -f +journalctl -u apache2 -f +``` + +### 6.3. Мониторинг Celery Flower + +```bash +source /var/www/project/venv/bin/activate +celery -A config flower --port=5555 +``` + +**Доступ:** http://localhost:5555 + +### 6.4. Мониторинг базы данных + +```bash +sudo -u postgres psql mostovik_prod + +# Проверка размера таблиц +SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size +FROM pg_tables +WHERE schemaname = 'public' +ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC; + +# Проверка активных подключений +SELECT * FROM pg_stat_activity WHERE datname = 'mostovik_prod'; +``` + +### 6.5. Мониторинг Redis + +```bash +redis-cli +INFO +DBSIZE +``` + +--- + +## 7. РЕЗЕРВНОЕ КОПИРОВАНИЕ + +### 7.1. Резервное копирование базы данных + +```bash +#!/bin/bash +BACKUP_DIR="/var/backups/mostovik" +DATE=$(date +%Y%m%d_%H%M%S) +DB_NAME="mostovik_prod" +DB_USER="mostovik_user" + +mkdir -p ${BACKUP_DIR} +pg_dump -U ${DB_USER} -h 127.0.0.1 ${DB_NAME} | gzip > ${BACKUP_DIR}/db_backup_${DATE}.sql.gz +find ${BACKUP_DIR} -name "db_backup_*.sql.gz" -mtime +7 -delete +``` + +**Настройка cron:** + +```bash +0 2 * * * /var/www/project/scripts/backup_db.sh +``` + +### 7.2. Резервное копирование медиафайлов + +```bash +#!/bin/bash +BACKUP_DIR="/var/backups/mostovik" +DATE=$(date +%Y%m%d_%H%M%S) +MEDIA_DIR="/var/www/project/media" + +mkdir -p ${BACKUP_DIR} +tar -czf ${BACKUP_DIR}/media_backup_${DATE}.tar.gz ${MEDIA_DIR} +find ${BACKUP_DIR} -name "media_backup_*.tar.gz" -mtime +7 -delete +``` + +### 7.3. Восстановление + +```bash +# Восстановление БД +gunzip -c db_backup_20260121_020000.sql.gz | psql -U mostovik_user -h 127.0.0.1 mostovik_prod + +# Восстановление медиа +tar -xzf media_backup_20260121_020000.tar.gz -C /var/www/project/ +``` + +--- + +## 8. БЕЗОПАСНОСТЬ + +### 8.1. Минимальные требования + +1. Смена SECRET_KEY в .env +2. Смена паролей БД и суперпользователя +3. Настройка HTTPS с действительным сертификатом +4. Ограничение доступа к портам БД и Redis +5. Регулярное обновление системы + +### 8.2. Настройка SSL/TLS + +```bash +apt-get install certbot python3-certbot-apache +certbot --apache -d your-domain.com -d www.your-domain.com +certbot renew --dry-run +``` + +### 8.3. Обновление зависимостей + +```bash +source /var/www/project/venv/bin/activate +pip-audit +uv pip install --upgrade -r requirements.txt +systemctl restart gunicorn celery-worker celery-beat +``` + +### 8.4. Аудит безопасности + +```bash +bandit -r /var/www/project/src/ +pip-audit +python manage.py check --deploy +``` + +--- + +## 9. УСТРАНЕНИЕ НЕИСПРАВНОСТЕЙ + +### 9.1. Служба не запускается + +```bash +journalctl -u -n 50 --no-pager +apache2ctl configtest +systemctl cat +``` + +### 9.2. Ошибки подключения к БД + +```bash +systemctl status postgresql +sudo -u postgres psql -c "SELECT * FROM pg_stat_activity WHERE datname = 'mostovik_prod';" +cat /var/www/project/.env | grep POSTGRES +``` + +### 9.3. Ошибки Celery + +```bash +journalctl -u celery-worker -n 100 + +source /var/www/project/venv/bin/activate +celery -A config inspect active +celery -A config inspect registered +celery -A config status +celery -A config purge # осторожно! +``` + +### 9.4. Проблемы с производительностью + +```bash +free -h +top -p $(pgrep -d',' -f gunicorn|celery) +df -h +du -sh /var/www/project/logs +``` + +**Оптимизация PostgreSQL:** + +```sql +SELECT query, calls, total_time, mean_time +FROM pg_stat_statements +ORDER BY mean_time DESC +LIMIT 10; + +VACUUM ANALYZE; +``` + +--- + +## ПРИЛОЖЕНИЕ А. КОМАНДЫ БЫСТРОГО ДОСТУПА + +```bash +# Перезапуск всех служб +systemctl restart apache2 gunicorn celery-worker celery-beat + +# Проверка состояния +systemctl status apache2 gunicorn celery-worker celery-beat postgresql redis-server + +# Просмотр логов +journalctl -u gunicorn -n 100 --no-pager + +# Django shell +cd /var/www/project/src && python manage.py shell + +# Миграции +cd /var/www/project/src && python manage.py makemigrations && python manage.py migrate + +# Статические файлы +cd /var/www/project/src && python manage.py collectstatic --noinput +``` + +--- + +## ПРИЛОЖЕНИЕ Б. ПЕРИОДИЧЕСКИЕ ЗАДАЧИ CELERY + +| Задача | Расписание | Описание | +|--------|------------|----------| +| parse_industrial_production | Ежедневно в 3:00 | Парсинг сертификатов Минпромторга | +| parse_manufactures | Ежедневно в 4:00 | Парсинг реестра производителей | +| scan_fns_directory | Каждые 5 минут | Сканирование папки ФНС | + +**Изменение расписания:** + +Через Django Admin: /admin/django_celery_beat/periodictask/ + +--- + +*Документ составлен на основе версии кода от 2026-01-21* diff --git a/docs/RUKOVODSTVO_PROGRAMMISTA.md b/docs/RUKOVODSTVO_PROGRAMMISTA.md new file mode 100644 index 0000000..72176c9 --- /dev/null +++ b/docs/RUKOVODSTVO_PROGRAMMISTA.md @@ -0,0 +1,1278 @@ +# РУКОВОДСТВО СИСТЕМНОГО ПРОГРАММИСТА +## Система ETL MOSTOVIK + +Версия документа: 1.0 +Дата: 2026-01-21 + +--- + +## СОДЕРЖАНИЕ + +1. [Общие сведения](#1-общие-сведения) +2. [Структура проекта](#2-структура-проекта) +3. [Технологический стек](#3-технологический-стек) +4. [Конфигурация и зависимости](#4-конфигурация-и-зависимости) +5. [Модели данных](#5-модели-данных) +6. [Сервисы и бизнес-логика](#6-сервисы-и-бизнес-логика) +7. [Задачи Celery](#7-задачи-celery) +8. [Клиенты и парсеры](#8-клиенты-и-парсеры) +9. [API и представители](#9-api-и-представители) +10. [Тестирование](#10-тестирование) +11. [Разработка и отладка](#11-разработка-и-отладка) +12. [Расширение функциональности](#12-расширение-функциональности) + +--- + +## 1. ОБЩИЕ СВЕДЕНИЯ + +### 1.1. Назначение документа + +Руководство содержит техническую информацию для разработчиков и системных программистов, работающих с системой MOSTOVIK. + +### 1.2. Описание системы + +MOSTOVIK — ETL-система для сбора, обработки и хранения данных из государственных источников: + +- **Минпромторг** (minpromtorg.gov.ru) — сертификаты промышленного производства, реестр производителей +- **Проверки.гов.ру** (proverki.gov.ru) — данные о проверках (ФЗ-294, ФЗ-248) +- **ФНС** — бухгалтерская отчётность + +### 1.3. Основные возможности + +- Автоматический парсинг данных через Celery +- Отслеживание прогресса задач (BackgroundJob) +- Логирование всех операций (ParserLoadLog) +- Повторные попытки при ошибках +- Потоковая обработка больших файлов +- Дедупликация данных + +--- + +## 2. СТРУКТУРА ПРОЕКТА + +``` +mostovik-backend/ +├── src/ # Исходный код Django +│ ├── config/ # Конфигурация Django +│ │ ├── settings/ # Настройки (base, dev, prod, test) +│ │ │ ├── base.py # Базовая конфигурация +│ │ │ ├── dev.py # Разработка +│ │ │ ├── production.py # Production +│ │ │ └── test.py # Тесты +│ │ ├── celery.py # Конфигурация Celery +│ │ ├── urls.py # Корневые URL +│ │ ├── api_v1_urls.py # Маршруты API +│ │ ├── wsgi.py # Точка входа WSGI +│ │ └── asgi.py # Точка входа ASGI +│ ├── apps/ # Приложения Django +│ │ ├── core/ # Базовые компоненты +│ │ │ ├── models.py # Базовые модели (TimestampMixin) +│ │ │ ├── services.py # BackgroundJobService +│ │ │ ├── tasks.py # Общие задачи +│ │ │ ├── views.py # Базовые классы представлений +│ │ │ ├── serializers.py +│ │ │ ├── filters.py +│ │ │ ├── pagination.py +│ │ │ ├── permissions.py +│ │ │ └── middleware.py +│ │ ├── parsers/ # Парсеры данных +│ │ │ ├── models.py # Модели данных +│ │ │ ├── services.py # Бизнес-логика +│ │ │ ├── tasks.py # Задачи Celery +│ │ │ ├── clients/ # API-клиенты +│ │ │ │ ├── minpromtorg/ +│ │ │ │ ├── proverki/ +│ │ │ │ ├── zakupki/ +│ │ │ │ └── fns/ +│ │ │ ├── admin.py # Django admin +│ │ │ ├── urls.py # Маршруты +│ │ │ ├── views.py # Классы представлений +│ │ │ └── serializers.py +│ │ └── user/ # Пользователи +│ │ ├── models.py +│ │ ├── admin.py +│ │ └── ... +│ └── manage.py # Утилита управления Django +├── tests/ # Тесты +│ ├── apps/ +│ │ ├── user/ +│ │ └── parsers/ +│ ├── conftest.py # Конфигурация pytest +│ └── factories.py # Фабрики Factory Boy +├── deploy/ # Развёртывание +│ ├── scripts/ # Скрипты +│ ├── systemd/ # Файлы systemd +│ └── apache/ # Конфигурация Apache +├── docker/ # Конфигурация Docker +├── scripts/ # Вспомогательные скрипты +├── logs/ # Логи +├── .env.example # Пример окружения +├── pyproject.toml # Зависимости и конфигурация +├── requirements.txt # Производственные зависимости +├── requirements-dev.txt # Зависимости для разработки +└── Makefile # Команды разработки +``` + +--- + +## 3. ТЕХНОЛОГИЧЕСКИЙ СТЕК + +### 3.1. Основные технологии + +| Компонент | Технология | Версия | +|-----------|-------------|---------| +| Язык | Python | 3.11 | +| Фреймворк | Django | 3.2.25 | +| API | Django REST Framework | 3.14.0 | +| БД | PostgreSQL | 15.10 | +| Кеш | Redis | 7.x | +| Очереди | Celery | 5.3.6 | +| Веб-сервер | Gunicorn + Apache | 21.2.0 / 2.4.57 | + +### 3.2. Библиотеки для парсинга + +| Назначение | Библиотека | Версия | +|-------------|-----------|---------| +| Автоматизация браузера | Playwright | 1.57.0+ | +| Web scraping | Scrapy | 2.11.2 | +| Browser automation | Selenium | 4.17.2 | +| HTML parsing | BeautifulSoup4 | 4.12.3 | +| HTTP-запросы | requests | 2.31.0 | + +### 3.3. Обработка данных + +| Назначение | Библиотека | Версия | +|-------------|-----------|---------| +| Таблицы | pandas | 2.0.3 | +| Excel | openpyxl | 3.1.5+ | +| Word | python-docx | 1.2.0+ | +| CSV | built-in | - | + +### 3.4. Инструменты разработки + +| Назначение | Инструмент | Версия | +|-------------|-----------|---------| +| Тестирование | pytest | 7.4.4 | +| Покрытие | coverage | 7.4.0 | +| Linting | ruff | 0.1.14 | +| Форматирование | black | 23.12.1 | +| Сортировка импортов | isort | 5.13.2 | +| Проверка типов | mypy | 1.8.0 | +| Безопасность | bandit | 1.7.5 | + +--- + +## 4. КОНФИГУРАЦИЯ И ЗАВИСИМОСТИ + +### 4.1. Установка зависимостей + +```bash +# Через uv (рекомендуется) +uv pip install -e ".[dev]" + +# Или через requirements +uv pip install -r requirements.txt +uv pip install -r requirements-dev.txt +``` + +### 4.2. Конфигурация окружения + +```bash +cp .env.example .env +``` + +**Ключевые переменные для разработки:** + +```ini +# Django +DJANGO_SETTINGS_MODULE=config.settings.dev +DEBUG=True +SECRET_KEY=django-insecure-dev-key + +# База данных +POSTGRES_DB=mostovik +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_HOST=127.0.0.1 +POSTGRES_PORT=5432 + +# Redis +REDIS_URL=redis://127.0.0.1:6379/0 +CELERY_BROKER_URL=redis://127.0.0.1:6379/0 + +# API парсеров +ZAKUPKI_TOKEN= +CHECKO_API_KEY= +``` + +### 4.3. Конфигурация инструментов (pyproject.toml) + +```toml +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "config.settings.test" +testpaths = ["tests"] +addopts = ["--verbose", "--tb=short", "--reuse-db"] + +[tool.ruff] +line-length = 88 +target-version = "py311" + +[tool.black] +line-length = 88 +target-version = ['py311'] + +[tool.mypy] +python_version = "3.11" +check_untyped_defs = true +plugins = ["mypy_django_plugin.main"] +``` + +--- + +## 5. МОДЕЛИ ДАННЫХ + +### 5.1. Базовые модели (apps.core) + +**TimestampMixin** — миксин для автоматического добавления полей времени: + +```python +class TimestampMixin(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True +``` + +### 5.2. Модели парсеров (apps.parsers) + +#### ParserLoadLog + +Лог загрузок парсеров: + +```python +class ParserLoadLog(TimestampMixin, models.Model): + class Source(models.TextChoices): + INDUSTRIAL = "industrial", "Промышленное производство" + MANUFACTURES = "manufactures", "Реестр производителей" + INSPECTIONS = "inspections", "Единый реестр проверок" + PROCUREMENTS = "procurements", "Госзакупки" + FNS_REPORTS = "fns_reports", "Бухгалтерская отчётность ФНС" + + batch_id = models.PositiveIntegerField(db_index=True) + source = models.CharField(max_length=50, choices=Source.choices, db_index=True) + records_count = models.PositiveIntegerField(default=0) + status = models.CharField(max_length=20, default="success") + error_message = models.TextField(blank=True) + + class Meta: + db_table = "parsers_load_log" + constraints = [ + models.UniqueConstraint( + fields=["source", "batch_id"], + name="unique_load_batch_per_source", + ), + ] +``` + +#### IndustrialCertificateRecord + +Сертификат промышленного производства: + +```python +class IndustrialCertificateRecord(TimestampMixin, models.Model): + load_batch = models.PositiveIntegerField(db_index=True) + issue_date = models.CharField(max_length=15, blank=True) + certificate_number = models.CharField(max_length=100, db_index=True) + expiry_date = models.CharField(max_length=15, blank=True) + certificate_file_url = models.TextField(blank=True) + organisation_name = models.TextField() + inn = models.CharField(max_length=20, db_index=True) + ogrn = models.CharField(max_length=20, db_index=True) + + class Meta: + db_table = "parsers_industrial_certificate" + constraints = [ + models.UniqueConstraint( + fields=["certificate_number"], + name="unique_certificate_number", + ), + ] +``` + +#### ManufacturerRecord + +Производитель из реестра Минпромторга: + +```python +class ManufacturerRecord(TimestampMixin, models.Model): + load_batch = models.PositiveIntegerField(db_index=True) + inn = models.CharField(max_length=20, db_index=True) + ogrn = models.CharField(max_length=20, db_index=True) + manufacturer_name = models.TextField() + # ... другие поля +``` + +#### InspectionRecord + +Запись о проверке (proverki.gov.ru): + +```python +class InspectionRecord(TimestampMixin, models.Model): + class InspectionType(models.TextChoices): + FZ294 = "294", "ФЗ-294 (традиционные)" + FZ248 = "248", "ФЗ-248 (новые)" + + load_batch = models.PositiveIntegerField(db_index=True) + inspection_id = models.CharField(max_length=100, unique=True) + inspection_type = models.CharField(max_length=3, choices=InspectionType.choices) + data_year = models.PositiveIntegerField() + data_month = models.PositiveIntegerField() + # ... другие поля +``` + +#### FinancialReport + +Бухгалтерский отчёт (ФНС): + +```python +class FinancialReport(TimestampMixin, models.Model): + class SourceType(models.TextChoices): + FILE_WATCH = "file_watch", "Мониторинг папок" + MANUAL = "manual", "Ручная загрузка" + + external_id = models.CharField(max_length=100, unique=True) + ogrn = models.CharField(max_length=20, db_index=True) + file_name = models.CharField(max_length=255) + file_hash = models.CharField(max_length=64, db_index=True) + source = models.CharField(max_length=20, choices=SourceType.choices) + batch_id = models.PositiveIntegerField(db_index=True) +``` + +### 5.3. Модели ядра (apps.core) + +#### BackgroundJob + +Отслеживание прогресса задач: + +```python +class BackgroundJob(TimestampMixin, models.Model): + class Status(models.TextChoices): + PENDING = "pending", "Ожидание" + RUNNING = "running", "Выполняется" + COMPLETED = "completed", "Завершено" + FAILED = "failed", "Ошибка" + + task_id = models.CharField(max_length=100, unique=True) + task_name = models.CharField(max_length=255) + status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING) + progress = models.PositiveIntegerField(default=0) # 0-100 + message = models.TextField(blank=True) + meta = models.JSONField(default=dict, blank=True) + result = models.JSONField(default=dict, blank=True) + error = models.TextField(blank=True) + started_at = models.DateTimeField(null=True, blank=True) + completed_at = models.DateTimeField(null=True, blank=True) +``` + +--- + +## 6. СЕРВИСЫ И БИЗНЕС-ЛОГИКА + +### 6.1. BackgroundJobService (apps.core) + +Управление фоновыми задачами: + +```python +class BackgroundJobService: + @classmethod + def create_job( + cls, + task_id: str, + task_name: str, + meta: dict | None = None, + ) -> BackgroundJob: + """Создать новую задачу.""" + return BackgroundJob.objects.create( + task_id=task_id, + task_name=task_name, + meta=meta or {}, + ) + + @classmethod + def get_by_task_id(cls, task_id: str) -> BackgroundJob | None: + """Получить задачу по ID.""" + return BackgroundJob.objects.filter(task_id=task_id).first() +``` + +**Методы:** + +| Метод | Описание | +|-------|----------| +| `create_job()` | Создание новой задачи | +| `get_by_task_id()` | Получение задачи по ID | +| `mark_started()` | Установка статуса "running" | +| `update_progress()` | Обновление прогресса (%) | +| `complete()` | Завершение задачи | +| `fail()` | Пометка как ошибочной | + +### 6.2. ParserLoadLogService (apps.parsers) + +Логирование загрузок: + +```python +class ParserLoadLogService: + @classmethod + def create_load_log_with_next_batch_id( + cls, + source: ParserLoadLog.Source, + status: str = "in_progress", + ) -> tuple[ParserLoadLog, int]: + """Создать лог загрузки со следующим batch_id.""" + last_log = ParserLoadLog.objects.filter(source=source).order_by('-batch_id').first() + next_batch_id = (last_log.batch_id + 1) if last_log else 1 + + log = ParserLoadLog.objects.create( + batch_id=next_batch_id, + source=source, + status=status, + ) + return log, next_batch_id + + @classmethod + def update( + cls, + log: ParserLoadLog, + status: str | None = None, + records_count: int | None = None, + error_message: str | None = None, + ) -> None: + """Обновить лог загрузки.""" + # ... + + @classmethod + def mark_failed(cls, log: ParserLoadLog, error: str) -> None: + """Пометить как неудачную.""" + # ... +``` + +### 6.3. Сервисы парсеров + +#### IndustrialCertificateService + +```python +class IndustrialCertificateService: + @classmethod + def save_certificates( + cls, + certificates: list[dict], + batch_id: int, + ) -> int: + """Сохранить сертификаты с дедупликацией.""" + saved = 0 + for cert_data in certificates: + obj, created = IndustrialCertificateRecord.objects.update_or_create( + certificate_number=cert_data['certificate_number'], + defaults={**cert_data, 'load_batch': batch_id}, + ) + if created: + saved += 1 + return saved +``` + +#### InspectionService + +```python +class InspectionService: + @classmethod + def get_last_loaded_period( + cls, + is_federal_law_248: bool, + ) -> tuple[int | None, int | None]: + """Получить последний загруженный период.""" + last_log = InspectionRecord.objects.filter( + is_federal_law_248=is_federal_law_248 + ).order_by('-data_year', '-data_month').first() + + if last_log: + return last_log.data_year, last_log.data_month + return None, None + + @classmethod + def save_inspections( + cls, + inspections: list[dict], + batch_id: int, + is_federal_law_248: bool = False, + data_year: int | None = None, + data_month: int | None = None, + ) -> int: + """Сохранить данные о проверках.""" + # ... +``` + +#### FNSReportService + +```python +class FNSReportService: + @classmethod + def exists_by_hash(cls, file_hash: str) -> bool: + """Проверить существование файла по хешу.""" + return FinancialReport.objects.filter(file_hash=file_hash).exists() + + @classmethod + def save_report( + cls, + external_id: str, + ogrn: str, + file_name: str, + file_hash: str, + source: FinancialReport.SourceType, + batch_id: int, + lines_data: list[dict], + ) -> FinancialReport: + """Сохранить отчёт со строками.""" + # ... +``` + +--- + +## 7. ЗАДАЧИ CELERY + +### 7.1. Конфигурация (config/celery.py) + +```python +app = Celery("project") +app.config_from_object("django.conf:settings", namespace="CELERY") +app.autodiscover_tasks() + +app.conf.beat_schedule = { + "parse-industrial-production-daily": { + "task": "apps.parsers.tasks.parse_industrial_production", + "schedule": 86400.0, # Каждые 24 часа + }, + "parse-manufactures-daily": { + "task": "apps.parsers.tasks.parse_manufactures", + "schedule": 86400.0, + }, + "scan-fns-directory": { + "task": "apps.parsers.tasks.scan_fns_directory", + "schedule": 300.0, # Каждые 5 минут + }, +} +``` + +### 7.2. Основные задачи + +#### parse_industrial_production + +```python +@shared_task(bind=True) +def parse_industrial_production( + self, + proxies: list[str] | None = None, + client_adapter: BaseAdapter | None = None, +) -> dict: + """Парсинг сертификатов Минпромторга.""" + source = ParserLoadLog.Source.INDUSTRIAL + load_log, batch_id = ParserLoadLogService.create_load_log_with_next_batch_id( + source=source, status="in_progress" + ) + task_id = self.request.id or str(uuid.uuid4()) + + # Создание BackgroundJob + job = BackgroundJobService.create_job( + task_id=task_id, + task_name="apps.parsers.tasks.parse_industrial_production", + meta={"source": source, "batch_id": batch_id}, + ) + job.mark_started() + + try: + # Парсинг + if proxies is None: + proxies = ProxyService.get_active_proxies_or_none() + + with IndustrialProductionClient(proxies=proxies) as client: + certificates = client.fetch_certificates() + + # Сохранение + saved_count = IndustrialCertificateService.save_certificates( + certificates, batch_id=batch_id + ) + + ParserLoadLogService.update(load_log, status="success", records_count=saved_count) + job.complete(result={"batch_id": batch_id, "saved": saved_count}) + + return {"batch_id": batch_id, "saved": saved_count, "status": "success"} + + except Exception as e: + ParserLoadLogService.mark_failed(load_log, str(e)) + job.fail(error=str(e)) + return {"status": "failed", "error": str(e)} +``` + +#### sync_inspections + +Автоматическая синхронизация проверок: + +```python +@shared_task(bind=True) +def sync_inspections( + self, + proxies: list[str] | None = None, + use_playwright: bool | None = None, + current_year: int | None = None, + current_month: int | None = None, +) -> dict: + """Синхронизация данных о проверках. + + Логика: + 1. Проверяет последнюю загруженную дату в БД + 2. Если данных нет — начинает с 01.01.2025 + 3. Загружает месяц за месяцем до текущего + 4. Загружает оба типа проверок (ФЗ-294 и ФЗ-248) + 5. При отсутствии данных (2 пустых месяца) — прекращает + """ + # ... +``` + +#### process_fns_file + +```python +@shared_task(bind=True) +def process_fns_file(self, file_path: str | Path) -> dict: + """Обработка файла ФНС.""" + task_id = self.request.id + file_path = Path(file_path) + + # Создание BackgroundJob + job = BackgroundJobService.create_job( + task_id=task_id, + task_name="apps.parsers.tasks.process_fns_file", + meta={"file": file_path.name}, + ) + job.mark_started() + + try: + # Проверка дубликата + file_hash = hashlib.sha256(file_path.read_bytes()).hexdigest() + if FNSReportService.exists_by_hash(file_hash): + job.complete(result={"status": "skipped", "reason": "duplicate"}) + return {"status": "skipped"} + + # Парсинг Excel + parsed = FNSExcelParser.parse_file(file_path) + + # Сохранение + report = FNSReportService.save_report( + external_id=parsed.external_id, + ogrn=parsed.ogrn, + file_name=file_path.name, + file_hash=file_hash, + source=FinancialReport.SourceType.FILE_WATCH, + lines_data=[asdict(line) for line in parsed.lines], + ) + + job.complete(result={"report_id": report.id}) + return {"status": "success"} + + except Exception as e: + job.fail(error=str(e)) + return {"status": "failed", "error": str(e)} +``` + +### 7.3. Вызов задач + +```python +# Асинхронно +from apps.parsers.tasks import parse_industrial_production + +result = parse_industrial_production.delay() +print(result.id) # ID задачи + +# С параметрами +result = parse_inspections.delay(year=2025, month=10, is_federal_law_248=False) + +# Синхронно (для тестов) +from apps.parsers.tasks import parse_manufactures +result = parse_manufactures.apply(kwargs={"proxies": [...]}) +``` + +--- + +## 8. КЛИЕНТЫ И ПАРСЕРЫ + +### 8.1. Структура клиентов + +``` +apps/parsers/clients/ +├── minpromtorg/ +│ ├── __init__.py +│ ├── base.py # Базовый класс +│ ├── industrial_production.py +│ └── manufactures.py +├── proverki/ +│ ├── __init__.py +│ └── proverki_client.py +├── zakupki/ +│ └── zakupki_client.py +└── fns/ + ├── __init__.py + ├── parser.py # FNSExcelParser + └── directory_scanner.py +``` + +### 8.2. Клиент Минпромторга + +```python +class IndustrialProductionClient: + """Клиент для API Минпромторга (сертификаты).""" + + BASE_URL = "https://minpromtorg.gov.ru/api" + + def __init__( + self, + proxies: list[str] | None = None, + http_adapter: BaseAdapter | None = None, + ): + self.session = requests.Session() + if proxies: + self.session.proxies = {"http": proxies[0], "https": proxies[0]} + if http_adapter: + self.session.mount("http://", http_adapter) + + def fetch_certificates(self) -> list[dict]: + """Получить список сертификатов.""" + response = self.session.get(f"{self.BASE_URL}/certificates") + response.raise_for_status() + return response.json() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.session.close() +``` + +### 8.3. Клиент proverki.gov.ru + +```python +class ProverkiClient: + """Клиент для proverki.gov.ru с поддержкой Playwright.""" + + def __init__( + self, + proxies: list[str] | None = None, + use_playwright: bool = True, + ): + self.proxies = proxies + self.use_playwright = use_playwright + self.browser = None + self.page = None + + def _init_browser(self): + """Инициализация Playwright.""" + from playwright.sync_api import sync_playwright + + playwright = sync_playwright().start() + self.browser = playwright.chromium.launch(headless=True) + self.page = self.browser.new_page() + + def fetch_inspections( + self, + year: int | None = None, + month: int | None = None, + file_url: str | None = None, + progress_callback: Callable | None = None, + ) -> list[dict]: + """Получить данные о проверках.""" + if self.use_playwright: + self._init_browser() + return self._fetch_with_playwright(year, month, file_url, progress_callback) + else: + return self._fetch_with_requests(year, month, file_url) + + def _fetch_with_playwright(...) -> list[dict]: + """Парсинг через Playwright (JS-rendering).""" + self.page.goto("https://proverki.gov.ru") + # ... навигация и парсинг + return inspections + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.browser: + self.browser.close() +``` + +### 8.4. Парсер ФНС + +```python +class FNSExcelParser: + """Парсер Excel-файлов ФНС.""" + + @staticmethod + def parse_file(file_path: Path) -> ParsedData: + """Разобрать Excel-файл.""" + import pandas as pd + + df = pd.read_excel(file_path, sheet_name=0) + + # Извлечение данных + external_id = df.iloc[0, 0] # Пример + ogrn = df.iloc[1, 0] + + lines = [] + for _, row in df.iterrows(): + lines.append(FNSLine( + period=row['period'], + revenue=row['revenue'], + # ... + )) + + return ParsedData( + external_id=external_id, + ogrn=ogrn, + lines=lines, + ) +``` + +--- + +## 9. API И ПРЕДСТАВИТЕЛИ + +### 9.1. Сериализаторы + +```python +# apps/parsers/serializers.py + +class IndustrialCertificateSerializer(serializers.ModelSerializer): + class Meta: + model = IndustrialCertificateRecord + fields = [ + 'id', 'certificate_number', 'issue_date', 'expiry_date', + 'organisation_name', 'inn', 'ogrn', 'created_at', + ] + read_only_fields = ['id', 'created_at'] + + +class ParserLoadLogSerializer(serializers.ModelSerializer): + class Meta: + model = ParserLoadLog + fields = [ + 'id', 'batch_id', 'source', 'records_count', + 'status', 'error_message', 'created_at', + ] +``` + +### 9.2. Классы представлений + +```python +# apps/parsers/views.py + +from rest_framework import viewsets, permissions +from apps.parsers.models import IndustrialCertificateRecord +from apps.parsers.serializers import IndustrialCertificateSerializer + +class IndustrialCertificateViewSet(viewsets.ReadOnlyModelViewSet): + """Только чтение для сертификатов.""" + queryset = IndustrialCertificateRecord.objects.all() + serializer_class = IndustrialCertificateSerializer + permission_classes = [permissions.IsAuthenticated] + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['organisation_name', 'certificate_number'] + ordering_fields = ['created_at', 'issue_date'] +``` + +### 9.3. Маршруты (URLs) + +```python +# apps/parsers/urls.py + +from rest_framework.routers import DefaultRouter +from apps.parsers.views import IndustrialCertificateViewSet + +router = DefaultRouter() +router.register('certificates', IndustrialCertificateViewSet, basename='certificate') + +urlpatterns = router.urls +``` + +```python +# config/api_v1_urls.py + +from django.urls import path, include + +urlpatterns = [ + path('parsers/', include('apps.parsers.urls')), + # ... +] +``` + +--- + +## 10. ТЕСТИРОВАНИЕ + +### 10.1. Конфигурация (pytest) + +```python +# tests/conftest.py + +import pytest +from django.conf import settings + +@pytest.fixture(autouse=True) +def enable_db_access_for_all_tests(db): + pass + +@pytest.fixture +def api_client(): + from rest_framework.test import APIClient + return APIClient() + +@pytest.fixture +def user(): + from apps.user.models import User + return User.objects.create_user(email='test@test.com', password='test') +``` + +### 10.2. Пример теста + +```python +# tests/apps/parsers/test_services.py + +import pytest +from model_bakery import baker +from apps.parsers.services import IndustrialCertificateService +from apps.parsers.models import IndustrialCertificateRecord + +@pytest.mark.django_db +class TestIndustrialCertificateService: + + def test_save_certificates(self): + certificates = [ + { + 'certificate_number': 'CERT-001', + 'issue_date': '2025-01-01', + 'organisation_name': 'ООО Ромашка', + 'inn': '1234567890', + 'ogrn': '1234567890123', + } + ] + + saved = IndustrialCertificateService.save_certificates(certificates, batch_id=1) + + assert saved == 1 + assert IndustrialCertificateRecord.objects.count() == 1 + + def test_save_certificates_duplicate(self): + # Создать дубликат + baker.make( + IndustrialCertificateRecord, + certificate_number='CERT-001' + ) + + certificates = [ + { + 'certificate_number': 'CERT-001', + 'issue_date': '2025-01-01', + 'organisation_name': 'ООО Ромашка', + 'inn': '1234567890', + 'ogrn': '1234567890123', + } + ] + + saved = IndustrialCertificateService.save_certificates(certificates, batch_id=2) + + assert saved == 0 # Не сохранён (дубликат) + assert IndustrialCertificateRecord.objects.count() == 1 +``` + +### 10.3. Тесты задач Celery + +```python +# tests/apps/parsers/test_tasks.py + +import pytest +from apps.parsers.tasks import parse_industrial_production + +@pytest.mark.django_db +class TestParseTasks: + + def test_parse_industrial_production(self, mocker): + # Мок клиента + mock_client = mocker.patch( + 'apps.parsers.tasks.IndustrialProductionClient' + ) + mock_client.return_value.__enter__.return_value.fetch_certificates.return_value = [] + + result = parse_industrial_production.delay() + + assert result.get()['status'] == 'success' +``` + +### 10.4. Команды тестирования + +```bash +# Все тесты +make test + +# С покрытием +make test-cov + +# Только быстрые (без медленных) +make test-fast TARGET="--fast" + +# Параллельно +make test-parallel + +# Конкретный модуль +python -m pytest tests/apps/parsers/test_services.py -v +``` + +--- + +## 11. РАЗРАБОТКА И ОТЛАДКА + +### 11.1. Локальный запуск + +```bash +# Миграции +cd src +python manage.py makemigrations +python manage.py migrate + +# Создание суперпользователя +python manage.py createsuperuser + +# Запуск сервера разработки +python manage.py runserver + +# Запуск Celery worker (в отдельном терминале) +celery -A config worker --loglevel=info + +# Запуск Celery beat +celery -A config beat --loglevel=info +``` + +### 11.2. Отладка Celery + +```bash +# Worker с отладкой +celery -A config worker --loglevel=debug --pool=solo + +# Проверка очереди +celery -A config inspect active +celery -A config inspect registered + +# Очистка очереди +celery -A config purge +``` + +### 11.3. Django Debug Toolbar + +```bash +# Установка (уже в requirements-dev.txt) +uv pip install django-debug-toolbar + +# Добавить в settings/dev.py: +INSTALLED_APPS += ['debug_toolbar'] +MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] +``` + +### 11.4. Логирование + +```python +# config/settings/dev.py + +LOGGING = { + 'version': 1, + 'handlers': { + 'console': {'class': 'logging.StreamHandler'}, + 'file': { + 'class': 'logging.FileHandler', + 'filename': 'logs/debug.log', + }, + }, + 'loggers': { + 'apps.parsers': { + 'handlers': ['console', 'file'], + 'level': 'DEBUG', + }, + }, +} +``` + +--- + +## 12. РАСШИРЕНИЕ ФУНКЦИОНАЛЬНОСТИ + +### 12.1. Добавление нового парсера + +1. **Создать модель:** + +```python +# apps/parsers/models.py + +class NewSourceRecord(TimestampMixin, models.Model): + load_batch = models.PositiveIntegerField(db_index=True) + # ... поля + + class Meta: + db_table = "parsers_new_source" + constraints = [ + models.UniqueConstraint( + fields=["unique_field"], + name="unique_new_source", + ), + ] +``` + +2. **Создать сервис:** + +```python +# apps/parsers/services.py + +class NewSourceService: + @classmethod + def save_records(cls, records: list[dict], batch_id: int) -> int: + saved = 0 + for data in records: + obj, created = NewSourceRecord.objects.update_or_create( + unique_field=data['unique_field'], + defaults={**data, 'load_batch': batch_id}, + ) + if created: + saved += 1 + return saved +``` + +3. **Создать клиента:** + +```python +# apps/parsers/clients/new_source.py + +class NewSourceClient: + def __init__(self, proxies=None): + self.session = requests.Session() + + def fetch_data(self) -> list[dict]: + response = self.session.get("https://api.example.com/data") + return response.json() +``` + +4. **Создать задачу Celery:** + +```python +# apps/parsers/tasks.py + +@shared_task(bind=True) +def parse_new_source(self, proxies=None): + source = ParserLoadLog.Source.NEW_SOURCE + load_log, batch_id = ParserLoadLogService.create_load_log_with_next_batch_id(source) + + job = BackgroundJobService.create_job( + task_id=self.request.id, + task_name="parse_new_source", + meta={"source": source}, + ) + job.mark_started() + + try: + with NewSourceClient(proxies=proxies) as client: + records = client.fetch_data() + + saved = NewSourceService.save_records(records, batch_id) + ParserLoadLogService.update(load_log, status="success", records_count=saved) + job.complete(result={"saved": saved}) + return {"status": "success"} + except Exception as e: + ParserLoadLogService.mark_failed(load_log, str(e)) + job.fail(error=str(e)) + return {"status": "failed"} +``` + +5. **Добавить в admin:** + +```python +# apps/parsers/admin.py + +@admin.register(NewSourceRecord) +class NewSourceRecordAdmin(admin.ModelAdmin): + list_display = ['id', 'unique_field', 'load_batch', 'created_at'] + list_filter = ['created_at'] + search_fields = ['unique_field'] +``` + +### 12.2. Добавление периодической задачи + +```python +# config/celery.py + +app.conf.beat_schedule = { + # ... существующие + "parse-new-source-hourly": { + "task": "apps.parsers.tasks.parse_new_source", + "schedule": 3600.0, # Каждый час + }, +} +``` + +### 12.3. Миграции + +```bash +cd src +python manage.py makemigrations parsers +python manage.py migrate +``` + +--- + +## ПРИЛОЖЕНИЕ А. ПОЛЕЗНЫЕ КОМАНДЫ + +```bash +# Создание миграций +python manage.py makemigrations +python manage.py migrate + +# Django shell +python manage.py shell + +# Проверка кода +ruff check src/ +black --check src/ +mypy src/ + +# Форматирование +black src/ +isort src/ + +# Безопасность +bandit -r src/ + +# Тесты +pytest tests/ -v +pytest tests/ --cov=src/ +``` + +--- + +## ПРИЛОЖЕНИЕ Б. СТРУКТУРА БД + +```sql +-- Основные таблицы + +parsers_load_log -- Логи загрузок +parsers_industrial_certificate -- Сертификаты +parsers_manufacturer_record -- Производители +parsers_inspection_record -- Проверки +parsers_financial_report -- Отчёты ФНС +core_backgroundjob -- Задачи +auth_user -- Пользователи +``` + +--- + +*Документ составлен на основе версии кода от 2026-01-21* diff --git a/docs/TECHNICAL_SPECIFICATION.md b/docs/TECHNICAL_SPECIFICATION.md new file mode 100644 index 0000000..ba55687 --- /dev/null +++ b/docs/TECHNICAL_SPECIFICATION.md @@ -0,0 +1,493 @@ +# ТЕХНИЧЕСКАЯ СПЕЦИФИКАЦИЯ +## Система ETL MOSTOVIK + +Версия документа: 1.0 +Дата: 2026-01-21 + +--- + +## 1. НАЗНАЧЕНИЕ ПРОГРАММЫ + +### 1.1. Общее назначение + +Система MOSTOVIK представляет собой ETL-платформу (Extract, Transform, Load) для автоматизированного сбора, обработки и хранения данных из государственных информационных систем и реестров. + +### 1.2. Функциональное назначение + +Программа предназначена для решения следующих задач: + +- **Автоматический сбор данных** из государственных источников: + - Минпромторг России (minpromtorg.gov.ru) — сертификаты промышленного производства, реестр производителей + - Единый реестр проверок (proverki.gov.ru) — данные о проверках юридических лиц и ИП + - Федеральная налоговая служба (ФНС) — бухгалтерская отчётность организаций + +- **Обработка и трансформация данных**: + - Парсинг структурированных и неструктурированных данных + - Нормализация форматов данных + - Дедупликация записей + - Валидация целостности данных + +- **Хранение и предоставление доступа**: + - Централизованное хранение собранных данных + - Предоставление данных через REST API + - Ведение журнала загрузок и аудита + +- **Автоматизация процессов**: + - Планирование периодических задач сбора данных + - Мониторинг выполнения задач + - Обработка ошибок и повторные попытки + +### 1.3. Ограничения, накладываемые на область применения программы + +#### 1.3.1. Правовые ограничения + +Программа должна применяться в соответствии с требованиями: + +- Федерального закона от 27.07.2006 № 149-ФЗ «Об информации, информационных технологиях и о защите информации» +- Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных» +- Положений об использовании данных государственных информационных систем +- Лицензионных соглашений источников данных + +#### 1.3.2. Технические ограничения + +- **Частота обращения к источникам**: не чаще одного раза в сутки для каждого источника (согласно настройкам Celery beat) +- **Объём обрабатываемых данных**: ограничен доступными системными ресурсами (ОЗУ, дисковое пространство) +- **Время отклика источников**: зависит от доступности внешних API и веб-ресурсов +- **Поддерживаемые форматы файлов**: + - Excel (.xlsx) — для отчётов ФНС + - JSON — для API-ответов + - HTML — для веб-парсинга + +#### 1.3.3. Эксплуатационные ограничения + +- Требуется стабильное подключение к сети Интернет для доступа к внешним источникам +- Работа с некоторыми источниками возможна только через браузерную автоматизацию (Playwright), что увеличивает требования к ресурсам +- Не допускается одновременная работа нескольких экземпляров системы с одной базой данных без дополнительной настройки + +#### 1.3.4. Область неприменимости + +Программа не предназначена для: + +- Обработки данных в реальном времени +- Работы с классифицированной информацией +- Использования в качестве единственного источника истины для критически важных систем +- Замены официальных запросов в государственные органы + +--- + +## 2. УСЛОВИЯ ПРИМЕНЕНИЯ + +### 2.1. Требования к необходимым для данной программы другим программам + +#### 2.1.1. Операционная система + +| Параметр | Минимальные требования | Рекомендуемые требования | +|----------|----------------------|-------------------------| +| ОС | Ubuntu 20.04 / Astra Linux Common Edition | Astra Linux Special Edition 1.7 | +| Ядро Linux | 5.4+ | 5.15+ | + +#### 2.1.2. Системное программное обеспечение + +| Компонент | Версия | Назначение | +|-----------|--------|------------| +| Python | 3.11 | Язык программирования | +| PostgreSQL | 15.10 | Система управления БД | +| Redis | 7.x | Кеш и брокер сообщений | +| Apache | 2.4.57 | Веб-сервер (HTTPS termination) | +| Gunicorn | 21.2.0 | WSGI-сервер для Django | +| Playwright | 1.57.0+ | Автоматизация браузера | + +#### 2.1.3. Библиотеки Python (основные) + +| Библиотека | Версия | Назначение | +|------------|--------|------------| +| Django | 3.2.25 | Веб-фреймворк | +| Django REST Framework | 3.14.0 | REST API | +| Celery | 5.3.6 | Очередь задач | +| requests | 2.31.0 | HTTP-запросы | +| pandas | 2.0.3 | Обработка табличных данных | +| openpyxl | 3.1.5+ | Чтение Excel | +| python-docx | 1.2.0+ | Чтение Word | +| BeautifulSoup4 | 4.12.3 | Парсинг HTML | +| Scrapy | 2.11.2 | Веб-скрапинг | +| Selenium | 4.17.2 | Автоматизация браузера | + +#### 2.1.4. Дополнительные компоненты + +- **libpq-dev** — клиентская библиотека PostgreSQL +- **libffi-dev** — Foreign Function Interface +- **libxml2-dev, libxslt1-dev** — библиотеки для работы с XML +- **zlib1g-dev** — библиотека сжатия +- **build-essential** — компилятор и инструменты сборки + +### 2.2. Требования к необходимым для данной программы техническим средствам + +#### 2.2.1. Минимальные аппаратные требования + +| Компонент | Требование | Примечание | +|-----------|------------|------------| +| Процессор | 2 ядра | x86_64 совместимый | +| Оперативная память | 4 ГБ | При минимальной нагрузке | +| Дисковое пространство | 20 ГБ | SSD рекомендуется | +| Сетевой интерфейс | 100 Мбит/с | Для доступа к источникам | + +#### 2.2.2. Рекомендуемые аппаратные требования + +| Компонент | Требование | Примечание | +|-----------|------------|------------| +| Процессор | 4 ядра | Для параллельного выполнения задач | +| Оперативная память | 8 ГБ | Для работы Playwright и обработки больших файлов | +| Дисковое пространство | 50 ГБ SSD | Для БД, логов и медиафайлов | +| Сетевой интерфейс | 1 Гбит/с | Для высокой пропускной способности | + +#### 2.2.3. Требования к инфраструктуре + +- **Доступ в Интернет**: обязательный для всех внешних источников +- **Статический IP-адрес**: рекомендуется для production-развёртывания +- **SSL/TLS сертификат**: обязателен для HTTPS-соединения +- **Резервное питание**: рекомендуется для предотвращения потери данных + +#### 2.2.4. Требования к системе управления базами данных + +| Параметр | Значение | +|----------|----------| +| СУБД | PostgreSQL 15.10 | +| Минимальный размер БД | 1 ГБ | +| Рекомендуемый размер БД | 10 ГБ+ | +| Максимальное количество подключений | 100 | +| Режим изоляции | Read Committed (по умолчанию) | + +#### 2.2.5. Требования к системе кеширования + +| Параметр | Значение | +|----------|----------| +| Система | Redis 7.x | +| Режим работы | in-memory с persistence (RDB/AOF) | +| Выделенная память | 512 МБ — 2 ГБ | +| Порты | 6379 (по умолчанию) | + +--- + +## 3. ОПИСАНИЕ ЗАДАЧИ + +### 3.1. Определение задачи + +#### 3.1.1. Общая характеристика + +Система MOSTOVIK решает задачу автоматизированного сбора и консолидации данных из распределённых государственных источников с целью создания единого централизованного хранилища для последующего анализа и использования. + +#### 3.1.2. Основные подзадачи + +**Извлечение данных (Extract):** + +- Подключение к удалённым источникам данных через HTTP/HTTPS +- Авторизация и аутентификация в системах-источниках (при необходимости) +- Получение данных в различных форматах (JSON, HTML, Excel) +- Обход ограничений и защита от блокировок (proxy, rate limiting) + +**Трансформация данных (Transform):** + +- Парсинг сырых данных и извлечение структурированной информации +- Нормализация форматов (даты, числовые значения, строки) +- Валидация данных на соответствие ожидаемым схемам +- Дедупликация записей по уникальным ключам +- Обогащение данных метаданными (источник, время загрузки, batch ID) + +**Загрузка данных (Load):** + +- Сохранение обработанных данных в PostgreSQL +- Ведение журнала загрузок (ParserLoadLog) +- Отслеживание прогресса задач (BackgroundJob) +- Индексация для ускорения поиска + +#### 3.1.3. Функциональные требования + +| ID | Требование | Описание | +|----|------------|----------| +| ФТ-001 | Автоматический сбор данных | Система должна автоматически собирать данные по расписанию | +| ФТ-002 | Поддержка множественных источников | Система должна поддерживать подключение к различным источникам | +| ФТ-003 | Обработка ошибок | Система должна обрабатывать ошибки сети и источников | +| ФТ-004 | Повторные попытки | Система должна выполнять повторные попытки при сбоях | +| ФТ-005 | Логирование | Система должна вести подробный журнал всех операций | +| ФТ-006 | Мониторинг | Система должна предоставлять информацию о статусе задач | +| ФТ-007 | REST API | Система должна предоставлять доступ к данным через API | +| ФТ-008 | Дедупликация | Система должна предотвращать дублирование записей | + +### 3.2. Методы решения задачи + +#### 3.2.1. Архитектурный подход + +Система построена по **ETL-архитектуре** с использованием следующих принципов: + +- **Модульность**: каждый источник данных реализуется в виде отдельного модуля +- **Расширяемость**: новые источники добавляются без изменения ядра системы +- **Асинхронность**: задачи выполняются фоново через очередь Celery +- **Отказоустойчивость**: сохранение прогресса и повторные попытки при ошибках + +#### 3.2.2. Методы извлечения данных + +**HTTP-запросы (requests library):** + +Прямые запросы к API источников данных. + +**Веб-скрапинг (Scrapy, BeautifulSoup):** + +Парсинг HTML-страниц государственных сайтов. + +**Браузерная автоматизация (Playwright, Selenium):** + +Для JavaScript-рендеринга и динамических страниц. + +**Парсинг файлов (pandas, openpyxl):** + +Обработка Excel-файлов и других табличных форматов. + +#### 3.2.3. Методы трансформации данных + +**Нормализация:** + +- Приведение дат к единому формату (YYYY-MM-DD) +- Очистка строк от лишних пробелов и символов +- Стандартизация числовых форматов + +**Дедупликация:** + +Механизм update_or_create по уникальным ключам (ИНН, ОГРН, номер сертификата). + +**Валидация:** + +- Проверка обязательных полей +- Валидация форматов (ИНН, ОГРН) +- Проверка ссылочной целостности + +#### 3.2.4. Методы загрузки данных + +**Пакетная загрузка:** + +- Группировка записей по batch ID +- Атомарное сохранение пакетов +- Откат при ошибках + +**Потоковая обработка:** + +- Обработка больших файлов по частям +- Прогресс-отслеживание +- Кеширование промежуточных результатов + +#### 3.2.5. Методы планирования задач + +**Celery Beat (периодические задачи):** + +| Задача | Расписание | +|--------|------------| +| Парсинг Минпромторга | Ежедневно в 3:00 | +| Парсинг реестра производителей | Ежедневно в 4:00 | +| Синхронизация проверок | По мере необходимости | +| Сканирование папки ФНС | Каждые 5 минут | + +**Управление очередями:** + +- Приоритизация задач +- Распределение по workers +- Мониторинг через Flower + +#### 3.2.6. Методы обеспечения надёжности + +**Логирование:** + +- Запись всех операций в ParserLoadLog +- Детализация ошибок +- Аудит действий пользователей + +**Мониторинг:** + +- Отслеживание статуса задач (BackgroundJob) +- Прогресс выполнения (0–100%) +- Уведомления об ошибках + +**Восстановление:** + +- Точки сохранения (checkpoints) +- Повторные попытки с экспоненциальной задержкой +- Резервное копирование БД + +--- + +## 4. ВХОДНЫЕ И ВЫХОДНЫЕ ДАННЫЕ + +### 4.1. Сведения о входных данных + +#### 4.1.1. Классификация входных данных + +| Тип | Источник | Формат | Периодичность | +|-----|----------|--------|---------------| +| Сертификаты промышленного производства | Минпромторг (API) | JSON | Ежедневно | +| Реестр производителей | Минпромторг (веб) | HTML/JSON | Ежедневно | +| Данные о проверках | proverki.gov.ru (веб) | HTML/JSON | По запросу | +| Бухгалтерская отчётность | ФНС (файлы) | Excel (.xlsx) | По мере поступления | + +#### 4.1.2. Структура входных данных + +**Сертификаты Минпромторга:** + +- certificate_number: номер сертификата (строка) +- issue_date: дата выдачи (YYYY-MM-DD) +- expiry_date: дата окончания (YYYY-MM-DD) +- organisation_name: название организации (текст) +- inn: ИНН организации (строка) +- ogrn: ОГРН организации (строка) +- certificate_file_url: URL файла сертификата + +**Реестр производителей:** + +- inn: ИНН производителя +- ogrn: ОГРН производителя +- manufacturer_name: наименование производителя +- address: юридический адрес +- products: список продукции + +**Данные о проверках:** + +- inspection_id: идентификатор проверки +- inspection_type: тип (294/248 ФЗ) +- data_year: год данных +- data_month: месяц данных +- entity_name: название юридического лица +- entity_inn: ИНН проверяемого лица +- inspection_date: дата проверки +- inspection_body: орган проведения проверки + +**Бухгалтерская отчётность (Excel):** + +- Период: отчётный период +- Выручка: сумма выручки +- Прибыль: сумма прибыли +- Активы: сумма активов +- Обязательства: сумма обязательств + +#### 4.1.3. Требования к качеству входных данных + +| Параметр | Требование | +|----------|------------| +| Полнота | Все обязательные поля должны быть заполнены | +| Актуальность | Данные должны соответствовать текущему состоянию источника | +| Консистентность | Данные должны соответствовать ожидаемой схеме | +| Уникальность | Дубликаты должны быть идентифицируемы по ключевым полям | + +#### 4.1.4. Ограничения входных данных + +- **Максимальный размер файла**: 50 МБ (для Excel-файлов ФНС) +- **Максимальное количество записей в пакете**: 10 000 +- **Таймаут запроса к источнику**: 30 секунд +- **Максимальное количество повторных попыток**: 3 + +### 4.2. Сведения о выходных данных + +#### 4.2.1. Классификация выходных данных + +| Тип | Назначение | Формат | Способ доступа | +|-----|------------|--------|----------------| +| Нормализованные данные | Хранение в БД | PostgreSQL таблицы | Внутреннее API | +| Данные для клиентов | REST API | JSON | HTTP/HTTPS | +| Отчёты о загрузках | Аудит и мониторинг | PostgreSQL + логи | Django Admin, API | +| Файлы выгрузки | Экспорт данных | CSV, Excel | По запросу | + +#### 4.2.2. Структура выходных данных + +**Таблицы базы данных:** + +| Таблица | Описание | Ключевые поля | +|---------|----------|---------------| +| parsers_industrial_certificate | Сертификаты Минпромторга | certificate_number, inn, ogrn | +| parsers_manufacturer_record | Реестр производителей | inn, ogrn | +| parsers_inspection_record | Проверки | inspection_id, entity_inn | +| parsers_financial_report | Отчёты ФНС | external_id, ogrn | +| parsers_load_log | Журнал загрузок | batch_id, source, status | +| core_backgroundjob | Статус задач | task_id, status, progress | + +**REST API ответы:** + +Формат JSON с пагинацией: +- count: общее количество записей +- next: URL следующей страницы +- previous: URL предыдущей страницы +- results: массив объектов данных + +**Статус задачи (BackgroundJob):** + +- task_id: уникальный идентификатор задачи +- task_name: имя задачи +- status: статус (pending/running/completed/failed) +- progress: прогресс выполнения (0-100) +- message: текстовое сообщение +- result: результат выполнения (JSON) +- started_at: время начала +- completed_at: время завершения + +**Лог загрузки (ParserLoadLog):** + +- batch_id: идентификатор пакета +- source: источник данных +- records_count: количество записей +- status: статус загрузки +- error_message: сообщение об ошибке +- created_at: время создания + +#### 4.2.3. Требования к качеству выходных данных + +| Параметр | Требование | +|----------|------------| +| Целостность | Все внешние ключи должны быть валидны | +| Консистентность | Данные должны соответствовать схеме БД | +| Индексирование | Ключевые поля должны быть индексированы | +| Аудит | Все изменения должны логироваться | + +#### 4.2.4. Форматы представления данных + +**JSON (REST API):** + +- Кодировка: UTF-8 +- Формат дат: ISO 8601 +- Пагинация: cursor-based или page-based + +**CSV (экспорт):** + +- Разделитель: точка с запятой (;) +- Кодировка: UTF-8 +- Заголовки: имена полей + +**Excel (экспорт):** + +- Формат: .xlsx +- Кодировка: UTF-8 +- Листы: по одному на таблицу + +#### 4.2.5. Объёмы выходных данных + +| Источник | Ожидаемый объём (мес.) | Рост (год) | +|----------|----------------------|------------| +| Сертификаты | 1 000 — 5 000 записей | ~50 000 записей | +| Производители | 500 — 2 000 записей | ~20 000 записей | +| Проверки | 5 000 — 20 000 записей | ~200 000 записей | +| Отчёты ФНС | 100 — 500 файлов | ~5 000 файлов | + +--- + +## ПРИЛОЖЕНИЕ А. ГЛОССАРИЙ + +| Термин | Определение | +|--------|-------------| +| ETL | Extract, Transform, Load — процесс извлечения, трансформации и загрузки данных | +| Batch ID | Уникальный идентификатор пакета загруженных данных | +| BackgroundJob | Фоновая задача с отслеживанием прогресса | +| ParserLoadLog | Журнал загрузок парсеров | +| Playwright | Библиотека для автоматизации браузера | +| Celery | Распределённая очередь задач | +| Django | Веб-фреймворк для разработки на Python | +| REST API | Программный интерфейс на основе HTTP | +| PostgreSQL | Реляционная система управления базами данных | +| Redis | Система кеширования и брокер сообщений | + +--- + +*Документ составлен на основе версии кода от 2026-01-21* diff --git a/docs/Источники для парсинга микросервис 1.docx b/docs/Источники для парсинга микросервис 1.docx new file mode 100644 index 0000000..388bf97 Binary files /dev/null and b/docs/Источники для парсинга микросервис 1.docx differ diff --git a/docs/Реализация_ГИСП_книга_1_с приложениями.odt b/docs/Реализация_ГИСП_книга_1_с приложениями.odt new file mode 100755 index 0000000..55c2a38 Binary files /dev/null and b/docs/Реализация_ГИСП_книга_1_с приложениями.odt differ diff --git a/docs/Реализация_ГИСП_книга_4.docx b/docs/Реализация_ГИСП_книга_4.docx new file mode 100755 index 0000000..cb833a1 Binary files /dev/null and b/docs/Реализация_ГИСП_книга_4.docx differ diff --git a/docs/Техническая справка ЕИС Закупки.md b/docs/Техническая справка ЕИС Закупки.md new file mode 100644 index 0000000..1c2779d --- /dev/null +++ b/docs/Техническая справка ЕИС Закупки.md @@ -0,0 +1,1104 @@ +# Техническая справка: Парсер ЕИС Закупок (zakupki.gov.ru) + +**Версия:** 1.0 +**Дата:** 26 марта 2026 +**Статус:** Production-ready + +--- + +## Содержание + +1. [Обзор системы](#1-обзор-системы) +2. [Парсируемый ресурс](#2-парсируемый-ресурс) +3. [Архитектура](#3-архитектура) +4. [Процесс загрузки данных](#4-процесс-загрузки-данных) +5. [Структура данных](#5-структура-данных) +6. [Хранение в БД](#6-хранение-в-бд) +7. [API](#7-api) +8. [Фоновые задачи](#8-фоновые-задачи) +9. [Конфигурация](#9-конфигурация) +10. [Примеры](#10-примеры) + +--- + +## 1. Обзор системы + +### Назначение + +Сервис парсит данные о государственных закупках из **Единой информационной системы в сфере закупок (ЕИС)** — zakupki.gov.ru. + +### Поддерживаемые законы + +- **44-ФЗ** — Федеральный закон "О контрактной системе в сфере закупок" +- **223-ФЗ** — Федеральный закон "О закупках товаров, работ, услуг отдельными видами юридических лиц" + +### Возможности + +- SOAP API (int44.zakupki.gov.ru) +- Парсинг XML-архивов +- Поддержка 80+ регионов РФ +- Инкрементальная синхронизация +- Точечный запрос по номеру закупки +- Связывание с организациями +- Отслеживание прогресса (BackgroundJob) +- Логирование (ParserLoadLog) + +--- + +## 2. Парсируемый ресурс + +### Источник данных + +| Параметр | Значение | +|----------|----------| +| **Название** | Единая информационная система в сфере закупок (ЕИС) | +| **Домен** | zakupki.gov.ru | +| **SOAP API** | https://int44.zakupki.gov.ru/eis-integration/services/getDocsIP | +| **Протокол** | SOAP 1.2 over HTTPS | +| **Формат** | XML в ZIP-архивах | +| **Авторизация** | Токен через Госуслуги | + +### Получение токена + +``` +URL: https://zakupki.gov.ru/pmd/auth/welcome +Требуется: Учётная запись Госуслуги (ЕСИА) +Токен: individualPerson_token (в SOAP-заголовке) +``` + +### Методы SOAP API + +#### getDocsByOrgRegionRequest + +Запрос по региону и периоду. + +**Параметры:** +- `orgRegion` — код региона ("77" = Москва) +- `subsystemType` — "PRIZ" (44-ФЗ), "OOS223" (223-ФЗ) +- `documentType44` — тип документа: + - `epNotificationEF2020` — электронный аукцион + - `epNotificationOK2020` — открытый конкурс + - `epNotificationZK2020` — запрос котировок +- `periodInfo/exactDate` — дата (YYYY-MM-DD) + +**Ответ:** +```xml + + https://zakupki.gov.ru/opendata/download/... + +``` + +#### getDocsByReestrNumberRequest + +Точечный запрос по номеру закупки. + +**Параметры:** +- `reestrNumber` — номер (например, "0888200000224000038") + +--- + +## 3. Архитектура + +### Компоненты + +``` +┌─────────────────────────────────────────────────────┐ +│ Celery Task Layer │ +│ parse_procurements │ sync_procurements │ +│ └──────┬────────────┴────────────┬────────────────┤ +│ ▼ ▼ │ +│ BackgroundJob (progress) │ │ +│ ParserLoadLog (audit) │ │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Service Layer │ +│ ProcurementService │ +│ - save_procurements() (bulk upsert) │ +│ - get_last_loaded_period() │ +│ - find_by_inn(), find_by_purchase_number() │ +│ │ +│ RegistryOrganizationResolver │ +│ - build_lookup() (INN/OGRN → Org ID) │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Client Layer │ +│ ZakupkiClient │ +│ - fetch_procurements() │ +│ - _fetch_via_soap() │ +│ - _build_soap_request_*() │ +│ - _parse_soap_response() │ +│ - _parse_archive_content() │ +│ - _parse_xml_record() │ +│ │ +│ BaseHTTPClient │ +│ - get(), post(), download_file() │ +│ - Proxy support │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Data Layer │ +│ ProcurementRecord (Django Model) │ +│ - 30+ fields │ +│ - Indexes & constraints │ +│ │ +│ Procurement (dataclass DTO) │ +└─────────────────────────────────────────────────────┘ +``` + +### Файловая структура + +``` +src/apps/parsers/ +├── clients/ +│ ├── base.py # Базовый HTTP-клиент +│ └── zakupki/ +│ ├── __init__.py # ZakupkiClient (888 строк) +│ └── schemas.py # Dataclass схемы +├── models.py # Django модели +├── services.py # Business logic +├── tasks.py # Celery задачи +├── views.py # DRF ViewSet +├── serializers.py # DRF Serializers +├── admin.py # Django Admin +└── migrations/ + ├── 0006_add_procurement_model.py + ├── 0010_link_registry_organizations.py + └── 0011_add_normalized_date_and_amount_fields.py +``` + +--- + +## 4. Процесс загрузки данных + +### 4.1. Полный цикл (parse_procurements) + +```python +# 1. Создание лога +load_log, batch_id = ParserLoadLogService.create_load_log_with_next_batch_id( + source=ParserLoadLog.Source.PROCUREMENTS, + status="in_progress" +) + +# 2. BackgroundJob для отслеживания +job = BackgroundJob.objects.create( + task_id=task_id, + task_name="apps.parsers.tasks.parse_procurements", + status="in_progress" +) + +# 3. Клиент +client = ZakupkiClient( + token=settings.ZAKUPKI_TOKEN, + proxies=proxies +) + +# 4. SOAP запрос +soap_request = client._build_soap_request_by_region( + region_code="77", + law_type="44", + year=2025, + month=3 +) + +response = requests.post( + "https://int44.zakupki.gov.ru/eis-integration/services/getDocsIP", + data=soap_request, + headers={ + "Content-Type": "text/xml; charset=utf-8", + "individualPerson_token": settings.ZAKUPKI_TOKEN + } +) + +# 5. Парсинг ответа → archive_url +archive_url = client._parse_soap_response(response) + +# 6. Скачивание ZIP +archive_content = client.http_client.download_file( + archive_url, + headers={"individualPerson_token": settings.ZAKUPKI_TOKEN} +) + +# 7. Распаковка и парсинг XML +procurements = client._parse_archive_content(archive_content, archive_url) + +# 8. Сохранение (bulk upsert) +saved_count = ProcurementService.save_procurements( + procurements, + batch_id=batch_id, + region_code="77", + data_year=2025, + data_month=3, + chunk_size=500 +) + +# 9. Обновление статуса +ParserLoadLogService.update(load_log, status="success", records_count=saved_count) +job.complete(result={"batch_id": batch_id, "saved": saved_count}) +``` + +### 4.2. Инкрементальная синхронизация (sync_procurements) + +```python +# 1. Последняя загруженная дата +last_year, last_month = ProcurementService.get_last_loaded_period( + region_code="77", + law_type="44-FZ" +) + +# 2. Начальная точка +if last_year and last_month: + start_year, start_month = last_year, last_month + 1 +else: + start_year, start_month = 2025, 1 # по умолчанию + +# 3. Загрузка месяц за месяцем +empty_months_count = 0 +year, month = start_year, start_month + +while year < current_year or (year == current_year and month <= current_month): + procurements = client.fetch_procurements( + region_code=region_code, + year=year, + month=month + ) + + if procurements: + ProcurementService.save_procurements(...) + empty_months_count = 0 + else: + empty_months_count += 1 + + if empty_months_count >= 2: # остановка + break + + # следующий месяц + month += 1 + if month > 12: + year += 1 + month = 1 +``` + +### 4.3. Парсинг XML + +```python +def _parse_xml_record(element: ET.Element) -> Procurement: + # Поиск с учётом namespace + def find_child(tag): ... + + # Извлечение текста + def get_text(tags): + for tag in tags: + if tag in element.attrib: + return element.attrib[tag] + child = find_child(tag) + if child is not None and child.text: + return child.text.strip() + return "" + + # Вложенные структуры + def get_nested_text(parent_tags, child_tags): ... + + # Маппинг полей + purchase_number = get_text(["purchaseNumber", "regNum"]) + purchase_name = get_text(["purchaseObjectInfo", "name"]) + + customer_inn = get_nested_text( + ["customer", "organizationInfo"], + ["INN", "inn"] + ) + + max_price = get_nested_text( + ["lot", "lotData"], + ["maxPrice", "initialSum"] + ) + + publish_date = get_text(["publishDate", "createDate"]) + end_date = get_text(["endDate", "submissionCloseDate"]) + status = get_text(["state", "status"]) + + # Определение закона + law_type = "" + if "44" in element.tag or "fcs" in element.tag.lower(): + law_type = "44-FZ" + elif "223" in element.tag: + law_type = "223-FZ" + + return Procurement(...) +``` + +### 4.4. Нормализация + +```python +def normalize_to_date(value: str | None) -> date | None: + """Строка → date (YYYY-MM-DD, DD.MM.YYYY, ISO 8601)""" + if not value: + return None + + candidate = str(value).strip().replace("T", " ").replace("Z", "") + + for fmt in ["%Y-%m-%d", "%d.%m.%Y", "%Y-%m-%d %H:%M:%S"]: + try: + return datetime.strptime(candidate, fmt).date() + except ValueError: + continue + + # Fallback: regex + match = re.search(r"\b\d{4}-\d{2}-\d{2}\b", candidate) + if match: + return datetime.strptime(match.group(0), "%Y-%m-%d").date() + + return None + + +def normalize_to_decimal(value: str | None) -> Decimal | None: + """Строка → Decimal (удаление ₽, пробелов, замена запятой)""" + if not value: + return None + + normalized = ( + str(value) + .replace("\u00a0", "") + .replace(" ", "") + .replace("₽", "") + .replace("руб.", "") + .replace("руб", "") + ) + + normalized = re.sub(r"[^0-9,.\-]", "", normalized) + + # Обработка разделителя + if "," in normalized and "." in normalized: + if normalized.rfind(",") > normalized.rfind("."): + normalized = normalized.replace(".", "").replace(",", ".") + else: + normalized = normalized.replace(",", "") + elif "," in normalized: + normalized = normalized.replace(",", ".") + + try: + return Decimal(normalized) + except (InvalidOperation, ValueError): + return None +``` + +--- + +## 5. Структура данных + +### DTO (Procurement dataclass) + +**Файл:** `src/apps/parsers/clients/zakupki/schemas.py` + +```python +@dataclass(frozen=True) +class Procurement: + purchase_number: str # Реестровый номер + purchase_name: str # Наименование + customer_inn: str # ИНН заказчика + customer_kpp: str # КПП заказчика + customer_ogrn: str # ОГРН заказчика + customer_name: str # Наименование заказчика + max_price: str # НМЦ (строка) + currency_code: str # Код валюты (RUB) + placement_method: str # Способ определения + publish_date: str # Дата публикации + end_date: str # Дата окончания + status: str # Статус + law_type: str # 44-ФЗ / 223-ФЗ + purchase_object_info: str = "" # Объект закупки + href: str = "" # Ссылка +``` + +### Пример данных + +```json +{ + "purchase_number": "0888200000224000038", + "purchase_name": "Поставка офисной бумаги", + "customer_inn": "7707083893", + "customer_kpp": "770701001", + "customer_ogrn": "1027700034460", + "customer_name": "ПАО СБЕРБАНК", + "max_price": "1500000.00", + "currency_code": "RUB", + "placement_method": "Электронный аукцион", + "publish_date": "2025-03-15", + "end_date": "2025-03-25T18:00:00", + "status": "Подача заявок", + "law_type": "44-FZ" +} +``` + +--- + +## 6. Хранение в БД + +### 6.1. Таблица: parsers_procurement + +**Модель:** `ProcurementRecord` +**Файл:** `src/apps/parsers/models.py` + +#### Поля + +| Поле | Тип БД | Django | Null | Index | Описание | +|------|--------|--------|------|-------|----------| +| id | BIGSERIAL | BigAutoField | NO | PK | Первичный ключ | +| created_at | TIMESTAMP | DateTimeField | NO | ✓ | Дата создания | +| updated_at | TIMESTAMP | DateTimeField | NO | - | Дата обновления | +| load_batch | INTEGER | PositiveIntegerField | NO | ✓ | ID пакета | +| purchase_number | VARCHAR(100) | CharField | NO | ✓ | Реестровый номер | +| purchase_name | TEXT | TextField | NO | - | Наименование | +| customer_inn | VARCHAR(20) | CharField | NO | ✓ | ИНН | +| customer_kpp | VARCHAR(20) | CharField | YES | - | КПП | +| customer_ogrn | VARCHAR(20) | CharField | YES | ✓ | ОГРН | +| customer_name | TEXT | TextField | NO | - | Наименование заказчика | +| max_price | VARCHAR(50) | CharField | YES | - | НМЦ (строка) | +| max_price_amount | DECIMAL(20,2) | DecimalField | YES | ✓ | НМЦ (число) | +| currency_code | VARCHAR(10) | CharField | NO | - | Код валюты | +| placement_method | VARCHAR(255) | CharField | YES | - | Способ определения | +| publish_date | VARCHAR(30) | CharField | YES | - | Дата (строка) | +| publish_date_normalized | DATE | DateField | YES | ✓ | Дата (date) | +| end_date | VARCHAR(30) | CharField | YES | - | Дата окончания (строка) | +| end_date_normalized | DATE | DateField | YES | ✓ | Дата окончания (date) | +| status | VARCHAR(100) | CharField | YES | - | Статус | +| law_type | VARCHAR(20) | CharField | YES | ✓ | Тип закона | +| purchase_object_info | TEXT | TextField | YES | - | Объект | +| href | VARCHAR(500) | URLField | YES | - | Ссылка | +| region_code | VARCHAR(10) | CharField | YES | ✓ | Код региона | +| data_year | SMALLINT | PositiveSmallIntegerField | YES | ✓ | Год данных | +| data_month | SMALLINT | PositiveSmallIntegerField | YES | ✓ | Месяц данных | +| registry_organization_id | BIGINT | ForeignKey | YES | - | FK к организациям | + +#### Индексы + +```sql +-- Одиночные (db_index=True) +CREATE INDEX ON parsers_procurement(created_at); +CREATE INDEX ON parsers_procurement(load_batch); +CREATE INDEX ON parsers_procurement(purchase_number); +CREATE INDEX ON parsers_procurement(customer_inn); +CREATE INDEX ON parsers_procurement(customer_ogrn); +CREATE INDEX ON parsers_procurement(max_price_amount); +CREATE INDEX ON parsers_procurement(publish_date_normalized); +CREATE INDEX ON parsers_procurement(end_date_normalized); +CREATE INDEX ON parsers_procurement(law_type); +CREATE INDEX ON parsers_procurement(region_code); +CREATE INDEX ON parsers_procurement(data_year); +CREATE INDEX ON parsers_procurement(data_month); + +-- Составные (Meta.indexes) +CREATE INDEX ON parsers_procurement(customer_inn, purchase_number); +CREATE INDEX ON parsers_procurement(load_batch, customer_inn); +CREATE INDEX ON parsers_procurement(law_type, data_year, data_month); +``` + +#### Ограничения + +```sql +-- Уникальность номера +ALTER TABLE parsers_procurement + ADD CONSTRAINT unique_procurement_purchase_number + UNIQUE (purchase_number); + +-- FK на организации +ALTER TABLE parsers_procurement + ADD CONSTRAINT fk_registry_organization + FOREIGN KEY (registry_organization_id) + REFERENCES registers_organization(id) + ON DELETE SET NULL; +``` + +#### DDL (CREATE TABLE) + +```sql +CREATE TABLE "parsers_procurement" ( + "id" bigserial NOT NULL PRIMARY KEY, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "load_batch" integer NOT NULL, + "purchase_number" varchar(100) NOT NULL, + "purchase_name" text NOT NULL, + "customer_inn" varchar(20) NOT NULL, + "customer_kpp" varchar(20), + "customer_ogrn" varchar(20), + "customer_name" text NOT NULL, + "max_price" varchar(50), + "max_price_amount" decimal(20, 2), + "currency_code" varchar(10) NOT NULL DEFAULT 'RUB', + "placement_method" varchar(255), + "publish_date" varchar(30), + "publish_date_normalized" date, + "end_date" varchar(30), + "end_date_normalized" date, + "status" varchar(100), + "law_type" varchar(20), + "purchase_object_info" text, + "href" varchar(500), + "region_code" varchar(10), + "data_year" smallint, + "data_month" smallint, + "registry_organization_id" bigint, + + CONSTRAINT "unique_procurement_purchase_number" UNIQUE ("purchase_number"), + CONSTRAINT "fk_registry_organization" + FOREIGN KEY ("registry_organization_id") + REFERENCES "registers_organization" ("id") + ON DELETE SET NULL +); +``` + +### 6.2. Таблица: parsers_load_log + +**Модель:** `ParserLoadLog` + +| Поле | Тип | Null | Index | Описание | +|------|-----|------|-------|----------| +| id | BIGSERIAL | NO | PK | Первичный ключ | +| created_at | TIMESTAMP | NO | - | Дата создания | +| updated_at | TIMESTAMP | NO | - | Дата обновления | +| batch_id | INTEGER | NO | ✓ | ID пакета | +| source | VARCHAR(50) | NO | ✓ | Источник | +| records_count | INTEGER | NO | - | Количество | +| status | VARCHAR(20) | NO | - | Статус | +| error_message | TEXT | YES | - | Ошибка | + +**Ограничение:** UNIQUE (source, batch_id) + +```sql +CREATE TABLE "parsers_load_log" ( + "id" bigserial NOT NULL PRIMARY KEY, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "batch_id" integer NOT NULL, + "source" varchar(50) NOT NULL, + "records_count" integer NOT NULL DEFAULT 0, + "status" varchar(20) NOT NULL DEFAULT 'success', + "error_message" text, + + CONSTRAINT "unique_load_batch_per_source" UNIQUE ("source", "batch_id") +); +``` + +**Значения source:** +- `procurements` — Госзакупки (ЕИС) +- `industrial` — Промышленное производство +- `manufactures` — Реестр производителей +- `inspections` — Единый реестр проверок +- `fns_reports` — Бухгалтерская отчётность ФНС + +### 6.3. Связь с организациями + +```python +registry_organization = models.ForeignKey( + "registers.Organization", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="procurement_records" +) +``` + +**Алгоритм связывания:** + +```python +class RegistryOrganizationResolver: + @classmethod + def build_lookup(cls, identifiers): + """ + identifiers: [(inn, ogrn), ...] + + Returns: + by_pair: {(inn, ogrn): org_id} + by_inn: {inn: org_id} + by_ogrn: {ogrn: org_id} + """ + # 1. Уникальные значения + inn_values = {int(inn) for inn, _ in identifiers if inn} + ogrn_values = {int(ogrn) for _, ogrn in identifiers if ogrn} + + # 2. Запрос организаций + organizations = Organization.objects.filter( + Q(inn__in=inn_values) | Q(ogrn__in=ogrn_values) + ).values("id", "inn", "ogrn") + + # 3. Построение индексов + by_pair, by_inn, by_ogrn = {}, {}, {} + for org in organizations: + inn = normalize(org["inn"]) + ogrn = normalize(org["ogrn"]) + org_id = org["id"] + + if inn and ogrn: + by_pair[(inn, ogrn)] = org_id + if inn: + by_inn[inn] = org_id + if ogrn: + by_ogrn[ogrn] = org_id + + return Lookup(by_pair, by_inn, by_ogrn) + + @classmethod + def resolve_organization_id(cls, lookup, inn, ogrn): + """Приоритет: пара → INN → OGRN""" + inn_norm = normalize(inn) + ogrn_norm = normalize(ogrn) + + if inn_norm and ogrn_norm: + if (inn_norm, ogrn_norm) in lookup.by_pair: + return lookup.by_pair[(inn_norm, ogrn_norm)] + + if inn_norm and inn_norm in lookup.by_inn: + return lookup.by_inn[inn_norm] + + if ogrn_norm and ogrn_norm in lookup.by_ogrn: + return lookup.by_ogrn[ogrn_norm] + + return None +``` + +--- + +## 7. API + +### Endpoints + +**Base:** `/api/v1/zakupki/` + +#### GET /api/v1/zakupki/ + +Список закупок. + +**Параметры:** + +| Параметр | Тип | Описание | +|----------|-----|----------| +| customer_inn | string | Фильтр по ИНН | +| customer_ogrn | string | Фильтр по ОГРН | +| purchase_number | string | Фильтр по номеру | +| law_type | string | 44-FZ / 223-FZ | +| status | string | Статус | +| region_code | string | Код региона | +| data_year | integer | Год | +| data_month | integer | Месяц | +| load_batch | integer | Пакет | +| search | string | Поиск по названию/номеру/заказчику | +| ordering | string | Сортировка | +| page, page_size | integer | Пагинация | + +**Пример:** +```http +GET /api/v1/zakupki/?customer_inn=7707083893&law_type=44-FZ&data_year=2025 +Authorization: Bearer +``` + +**Ответ:** +```json +{ + "count": 156, + "next": ".../api/v1/zakupki/?page=2", + "results": [ + { + "id": 12345, + "purchase_number": "0888200000224000038", + "purchase_name": "Поставка офисной бумаги", + "customer_inn": "7707083893", + "max_price_amount": "1500000.00", + "publish_date_normalized": "2025-03-15", + "law_type": "44-FZ", + "status": "Подача заявок" + } + ] +} +``` + +#### GET /api/v1/zakupki/{id}/ + +Детали закупки. + +### Serializer + +**Файл:** `src/apps/parsers/serializers.py` + +```python +class ProcurementSerializer(serializers.ModelSerializer): + class Meta: + model = ProcurementRecord + fields = [ + "id", "load_batch", "purchase_number", "purchase_name", + "customer_inn", "customer_kpp", "customer_ogrn", "customer_name", + "max_price", "max_price_amount", "currency_code", + "placement_method", "publish_date", "publish_date_normalized", + "end_date", "end_date_normalized", "status", "law_type", + "purchase_object_info", "href", "region_code", + "data_year", "data_month", "registry_organization", + "created_at", "updated_at" + ] + read_only_fields = fields +``` + +--- + +## 8. Фоновые задачи + +### Celery Tasks + +**Файл:** `src/apps/parsers/tasks.py` + +#### parse_procurements + +Одноразовая загрузка. + +```python +@shared_task(bind=True) +def parse_procurements( + self, + region_code: str | None = None, + year: int | None = None, + month: int | None = None, + law_type: str = "44", + proxies: list[str] | None = None, + requested_by_id: int | None = None, +) -> dict: + """ + Returns: + {"batch_id": int, "saved": int, "status": "success"} + """ +``` + +**Вызов:** +```python +parse_procurements.delay( + region_code="77", + year=2025, + month=3, + law_type="44" +) +``` + +#### sync_procurements + +Инкрементальная синхронизация. + +```python +@shared_task(bind=True) +def sync_procurements( + self, + region_code: str, + law_type: str = "44", + proxies: list[str] | None = None, +) -> dict: + """ + Логика: + 1. Проверить последнюю дату в БД + 2. Если нет данных — начать с 01.01.2025 + 3. Загружать месяц за месяцем + 4. Остановиться после 2 месяцев без данных + + Returns: + { + "batch_id": int, + "total_saved": int, + "results": [{"year": 2025, "month": 3, "fetched": 150, "saved": 145}], + "status": "success" + } + """ +``` + +**Вызов:** +```python +sync_procurements.delay( + region_code="77", + law_type="44" +) +``` + +### Periodic Tasks (Celery Beat) + +**Файл:** `src/core/celery.py` + +```python +CELERY_BEAT_SCHEDULE = { + "sync-procurements-daily": { + "task": "apps.parsers.tasks.sync_procurements", + "schedule": crontab(hour=2, minute=0), # Ежедневно в 02:00 + "kwargs": {"region_code": "77", "law_type": "44"}, + }, +} +``` + +### Progress Tracking + +```python +# Создание +job = BackgroundJob.objects.create( + task_id=task_id, + task_name="apps.parsers.tasks.parse_procurements", + status="in_progress" +) + +# Прогресс +job.update_progress(50, "Загрузка за 03/2025...") + +# Завершение +job.complete(result={"batch_id": 123, "saved": 150}) + +# Ошибка +job.fail(error="SOAP API timeout") +``` + +--- + +## 9. Конфигурация + +### Переменные окружения + +**Файл:** `.env.prod.example` / `.env.dev` + +```bash +# Токен ЕИС (Госуслуги) +ZAKUPKI_TOKEN= + +# Прокси (опционально) +PARSER_PROXIES=http://user:pass@proxy1:8080,http://user:pass@proxy2:8080 + +# PostgreSQL +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=mostovik +POSTGRES_USER=postgres +POSTGRES_PASSWORD= + +# Redis (Celery) +CELERY_BROKER_URL=redis://localhost:6379/0 +CELERY_RESULT_BACKEND=redis://localhost:6379/0 + +# Django +DJANGO_SETTINGS_MODULE=config.settings.production +SECRET_KEY= +ALLOWED_HOSTS=example.com +``` + +### Django Settings + +**Файл:** `src/settings/base.py` + +```python +INSTALLED_APPS = [ + # ... + "apps.parsers", + "apps.registers", +] + +ZAKUPKI_TOKEN = os.getenv("ZAKUPKI_TOKEN", "") + +PARSER_PROXIES = [] +if parser_proxies := os.getenv("PARSER_PROXIES"): + PARSER_PROXIES = [p.strip() for p in parser_proxies.split(",")] +``` + +### Docker Compose + +**Файл:** `docker-compose.prod.yml` + +```yaml +services: + celery: + image: registry.example.com/mostovik/celery:latest + command: celery -A config worker --loglevel=info --concurrency=4 + env_file: .env.prod + depends_on: [postgres, redis] + + celery-beat: + image: registry.example.com/mostovik/celery:latest + command: celery -A config beat --loglevel=info + env_file: .env.prod + depends_on: [postgres, redis] +``` + +--- + +## 10. Примеры + +### 10.1. Прямой вызов клиента + +```python +from apps.parsers.clients.zakupki import ZakupkiClient +from django.conf import settings + +client = ZakupkiClient( + token=settings.ZAKUPKI_TOKEN, + proxies=["http://proxy.example.com:8080"] +) + +# По региону +procurements = client.fetch_procurements( + region_code="77", + year=2025, + month=3, + law_type="44" +) + +# По номеру +procurements = client.fetch_by_reestr_number( + reestr_number="0888200000224000038", + law_type="44" +) + +# Context manager +with ZakupkiClient(token=settings.ZAKUPKI_TOKEN) as client: + procurements = client.fetch_procurements(region_code="77", year=2025) +``` + +### 10.2. Сервис + +```python +from apps.parsers.services import ProcurementService + +# Поиск по ИНН +procurements = ProcurementService.find_by_inn("7707083893") + +# Поиск по номеру +procurement = ProcurementService.find_by_purchase_number( + "0888200000224000038" +).first() + +# Последний период +last_year, last_month = ProcurementService.get_last_loaded_period( + region_code="77", + law_type="44-FZ" +) +print(f"Last loaded: {last_year}/{last_month}") +``` + +### 10.3. API + +```bash +# Все закупки заказчика +curl -X GET "http://localhost:8000/api/v1/zakupki/?customer_inn=7707083893" \ + -H "Authorization: Bearer " + +# Фильтрация +curl -X GET "http://localhost:8000/api/v1/zakupki/?data_year=2025&law_type=44-FZ" \ + -H "Authorization: Bearer " + +# Поиск +curl -X GET "http://localhost:8000/api/v1/zakupki/?search=бумага" \ + -H "Authorization: Bearer " + +# Детали +curl -X GET "http://localhost:8000/api/v1/zakupki/12345/" \ + -H "Authorization: Bearer " +``` + +### 10.4. SQL + +```sql +-- Закупки заказчика за 2025 +SELECT + purchase_number, + purchase_name, + max_price_amount, + publish_date_normalized, + status +FROM parsers_procurement +WHERE customer_inn = '7707083893' + AND data_year = 2025 +ORDER BY publish_date_normalized DESC; + +-- Сумма по регионам +SELECT + region_code, + COUNT(*) as count, + SUM(max_price_amount) as total +FROM parsers_procurement +WHERE data_year = 2025 +GROUP BY region_code +ORDER BY total DESC; + +-- Последние загрузки +SELECT + batch_id, + created_at, + records_count, + status, + error_message +FROM parsers_load_log +WHERE source = 'procurements' +ORDER BY created_at DESC +LIMIT 10; + +-- Статистика по законам +SELECT + law_type, + COUNT(*) as count, + SUM(max_price_amount) as total, + AVG(max_price_amount) as avg +FROM parsers_procurement +GROUP BY law_type; +``` + +### 10.5. Мониторинг + +```python +from apps.parsers.models import ParserLoadLog, ProcurementRecord +from django.db.models import Count, Sum + +# Логи +logs = ParserLoadLog.objects.filter( + source=ParserLoadLog.Source.PROCUREMENTS +).order_by("-created_at")[:10] + +# Статистика +stats = ProcurementRecord.objects.values("region_code").annotate( + count=Count("*"), + total=Sum("max_price_amount") +).order_by("-count") + +# По законам +by_law = ProcurementRecord.objects.values("law_type").annotate( + count=Count("*") +) +``` + +--- + +## Приложения + +### A. Коды регионов + +| Код | Регион | +|-----|--------| +| 01 | Адыгея | +| 77 | Москва | +| 78 | Санкт-Петербург | +| 99 | Все регионы | + +### B. Типы документов 44-ФЗ + +| document_type | Значение | +|---------------|----------| +| notification | Электронный аукцион | +| notification_ok | Открытый конкурс | +| notification_zk | Запрос котировок | + +### C. Статусы + +- Планирование +- Публикация извещения +- Подача заявок +- Рассмотрение заявок +- Заключение контракта +- Исполнение +- Завершено +- Отменено + +--- + +**Файл:** `docs/Техническая справка ЕИС Закупки.md` +**Код:** `src/apps/parsers/` +**Тесты:** `tests/apps/parsers/` diff --git a/docs/описание источников.docx b/docs/описание источников.docx new file mode 100644 index 0000000..5e40977 Binary files /dev/null and b/docs/описание источников.docx differ