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

@@ -3,6 +3,8 @@ Admin configuration for parsers app.
"""
from apps.parsers.models import (
FinancialReport,
FinancialReportLine,
IndustrialCertificateRecord,
InspectionRecord,
ManufacturerRecord,
@@ -520,3 +522,107 @@ class ProcurementRecordAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
"""Запретить редактирование записей."""
return False
class FinancialReportLineInline(admin.TabularInline):
"""Inline для строк финансового отчета."""
model = FinancialReportLine
extra = 0
readonly_fields = [
"form_code",
"line_code",
"line_name",
"year",
"period_start",
"period_end",
]
can_delete = False
def has_add_permission(self, request, obj=None):
return False
@admin.register(FinancialReport)
class FinancialReportAdmin(admin.ModelAdmin):
"""Admin для финансовых отчетов ФНС."""
list_display = [
"external_id",
"ogrn",
"file_name",
"status_badge",
"source",
"lines_count",
"load_batch",
"created_at",
]
list_filter = ["status", "source", "load_batch", "created_at"]
search_fields = ["external_id", "ogrn", "file_name"]
readonly_fields = [
"external_id",
"ogrn",
"file_name",
"file_hash",
"load_batch",
"status",
"source",
"error_message",
"created_at",
"updated_at",
]
ordering = ["-created_at"]
list_per_page = 50
date_hierarchy = "created_at"
inlines = [FinancialReportLineInline]
fieldsets = (
(
"Основное",
{"fields": ("external_id", "ogrn", "file_name", "file_hash")},
),
(
"Статус",
{"fields": ("status", "source", "error_message")},
),
(
"Системное",
{
"fields": ("load_batch", "created_at", "updated_at"),
"classes": ("collapse",),
},
),
)
def lines_count(self, obj):
"""Количество строк в отчете."""
return obj.lines.count()
lines_count.short_description = "Строк"
def status_badge(self, obj):
"""Цветной бейдж статуса."""
colors = {
"pending": "#6c757d",
"processing": "#ffc107",
"success": "#28a745",
"failed": "#dc3545",
}
color = colors.get(obj.status, "#6c757d")
return format_html(
'<span style="color: white; background: {}; padding: 3px 10px; '
'border-radius: 3px;">{}</span>',
color,
obj.get_status_display(),
)
status_badge.short_description = "Статус"
status_badge.admin_order_field = "status"
def has_add_permission(self, request):
"""Запретить создание записей вручную."""
return False
def has_change_permission(self, request, obj=None):
"""Запретить редактирование записей."""
return False