feat(fns): парсер ФНС бухгалтерской отчетности

- Модели FinancialReport и FinancialReportLine
- FNSExcelParser для файлов fin_{id}_{ogrn}.xlsx
- FNSReportService с дедупликацией по хешу файла
- Celery задачи для мониторинга папки (каждые 5 мин)
- API: POST /fns/upload/, GET /fns/reports/
- Django admin интеграция
- 25 unit-тестов
This commit is contained in:
2026-02-01 14:44:19 +01:00
parent eb0d6f2600
commit cd0e21350b
17 changed files with 1537 additions and 10 deletions

View File

@@ -1,7 +1,110 @@
"""
Views для приложения парсеров.
TODO: Добавить views по мере необходимости.
"""
# Views будут добавлены по мере разработки конкретных парсеров
import hashlib
from pathlib import Path
from apps.parsers.models import FinancialReport
from apps.parsers.serializers import (
FinancialReportDetailSerializer,
FinancialReportSerializer,
FNSFileUploadSerializer,
)
from apps.parsers.tasks import process_fns_file
from django.conf import settings
from rest_framework import status
from rest_framework.parsers import MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ReadOnlyModelViewSet
class FinancialReportViewSet(ReadOnlyModelViewSet):
"""
API для просмотра финансовых отчетов ФНС.
list:
Получить список всех отчетов.
Поддерживает фильтрацию по: ogrn, external_id, status.
retrieve:
Получить детальную информацию об отчете, включая все строки.
"""
queryset = FinancialReport.objects.all().order_by("-created_at")
filterset_fields = ["ogrn", "external_id", "status", "source"]
def get_serializer_class(self):
if self.action == "retrieve":
return FinancialReportDetailSerializer
return FinancialReportSerializer
class FNSReportUploadView(APIView):
"""
API для загрузки файлов бухгалтерской отчетности ФНС.
POST:
Пакетная загрузка файлов.
Файлы сохраняются во временную директорию и ставятся в очередь
на обработку через Celery.
Request:
multipart/form-data с полем 'files' (можно несколько файлов)
Response:
{
"queued": 3,
"skipped": 1,
"task_ids": ["uuid1", "uuid2", "uuid3"]
}
"""
parser_classes = [MultiPartParser]
def post(self, request):
serializer = FNSFileUploadSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
files = serializer.validated_data["files"]
task_ids = []
queued = 0
skipped = 0
# Создаём директорию для загрузки
upload_dir = Path(settings.FNS_WATCH_DIRECTORY)
upload_dir.mkdir(parents=True, exist_ok=True)
from apps.parsers.services import FNSReportService
for file in files:
# Вычисляем хеш файла
file_content = file.read()
file_hash = hashlib.sha256(file_content).hexdigest()
file.seek(0)
# Проверяем дубликат
if FNSReportService.exists_by_hash(file_hash):
skipped += 1
continue
# Сохраняем файл
file_path = upload_dir / file.name
with open(file_path, "wb") as f:
for chunk in file.chunks():
f.write(chunk)
# Ставим в очередь
task = process_fns_file.delay(str(file_path))
task_ids.append(task.id)
queued += 1
return Response(
{
"queued": queued,
"skipped": skipped,
"task_ids": task_ids,
},
status=status.HTTP_202_ACCEPTED,
)