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:
@@ -21,6 +21,7 @@ class ParserLoadLog(TimestampMixin, models.Model):
|
||||
MANUFACTURES = "manufactures", _("Реестр производителей")
|
||||
INSPECTIONS = "inspections", _("Единый реестр проверок")
|
||||
PROCUREMENTS = "procurements", _("Государственные закупки")
|
||||
FNS_REPORTS = "fns_reports", _("Бухгалтерская отчетность ФНС")
|
||||
|
||||
batch_id = models.PositiveIntegerField(
|
||||
_("ID пакета"),
|
||||
@@ -506,3 +507,153 @@ class ProcurementRecord(TimestampMixin, models.Model):
|
||||
def __str__(self) -> str:
|
||||
name = self.purchase_name[:50] if self.purchase_name else ""
|
||||
return f"{self.purchase_number} - {name}"
|
||||
|
||||
|
||||
class FinancialReport(TimestampMixin, models.Model):
|
||||
"""
|
||||
Бухгалтерская отчетность организации из ФНС.
|
||||
|
||||
Данные загружаются из Excel файлов, полученных от ФНС.
|
||||
Имя файла: fin_{external_id}_{ogrn}.xlsx
|
||||
"""
|
||||
|
||||
class Status(models.TextChoices):
|
||||
PENDING = "pending", _("Ожидает обработки")
|
||||
PROCESSING = "processing", _("Обрабатывается")
|
||||
SUCCESS = "success", _("Успешно")
|
||||
FAILED = "failed", _("Ошибка")
|
||||
|
||||
class SourceType(models.TextChoices):
|
||||
FILE_WATCH = "file_watch", _("Мониторинг папки")
|
||||
API = "api", _("API загрузка")
|
||||
|
||||
external_id = models.CharField(
|
||||
_("внешний ID"),
|
||||
max_length=50,
|
||||
unique=True,
|
||||
db_index=True,
|
||||
help_text=_("Уникальный идентификатор из имени файла"),
|
||||
)
|
||||
ogrn = models.CharField(
|
||||
_("ОГРН"),
|
||||
max_length=15,
|
||||
db_index=True,
|
||||
help_text=_("ОГРН организации"),
|
||||
)
|
||||
file_name = models.CharField(
|
||||
_("имя файла"),
|
||||
max_length=255,
|
||||
help_text=_("Оригинальное имя загруженного файла"),
|
||||
)
|
||||
file_hash = models.CharField(
|
||||
_("хеш файла"),
|
||||
max_length=64,
|
||||
unique=True,
|
||||
help_text=_("SHA256 хеш файла для дедупликации"),
|
||||
)
|
||||
load_batch = models.PositiveIntegerField(
|
||||
_("ID пакета загрузки"),
|
||||
db_index=True,
|
||||
help_text=_("Идентификатор пакета загрузки"),
|
||||
)
|
||||
status = models.CharField(
|
||||
_("статус"),
|
||||
max_length=20,
|
||||
choices=Status.choices,
|
||||
default=Status.PENDING,
|
||||
db_index=True,
|
||||
help_text=_("Статус обработки файла"),
|
||||
)
|
||||
error_message = models.TextField(
|
||||
_("сообщение об ошибке"),
|
||||
blank=True,
|
||||
help_text=_("Текст ошибки при неудачной обработке"),
|
||||
)
|
||||
source = models.CharField(
|
||||
_("источник"),
|
||||
max_length=20,
|
||||
choices=SourceType.choices,
|
||||
help_text=_("Источник загрузки файла"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "parsers_financial_report"
|
||||
verbose_name = _("финансовый отчет")
|
||||
verbose_name_plural = _("финансовые отчеты")
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["ogrn", "status"]),
|
||||
models.Index(fields=["load_batch", "status"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.external_id} (ОГРН: {self.ogrn}) - {self.status}"
|
||||
|
||||
|
||||
class FinancialReportLine(models.Model):
|
||||
"""
|
||||
Строка бухгалтерской отчетности.
|
||||
|
||||
Хранит значения по конкретной строке формы отчетности за конкретный год.
|
||||
Формы: №1 (Баланс), №2 (ОФР), №3 (Изменение капитала),
|
||||
№4 (Движение денежных средств), №6 (Целевое использование).
|
||||
"""
|
||||
|
||||
report = models.ForeignKey(
|
||||
FinancialReport,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="lines",
|
||||
verbose_name=_("отчет"),
|
||||
)
|
||||
form_code = models.CharField(
|
||||
_("код формы"),
|
||||
max_length=10,
|
||||
db_index=True,
|
||||
help_text=_("Номер формы отчетности (1, 2, 3, 4, 6)"),
|
||||
)
|
||||
line_code = models.CharField(
|
||||
_("код строки"),
|
||||
max_length=10,
|
||||
db_index=True,
|
||||
help_text=_("Код строки отчетности (например: 1100, 2110)"),
|
||||
)
|
||||
line_name = models.CharField(
|
||||
_("наименование строки"),
|
||||
max_length=255,
|
||||
help_text=_("Наименование строки отчетности"),
|
||||
)
|
||||
year = models.PositiveSmallIntegerField(
|
||||
_("год"),
|
||||
db_index=True,
|
||||
help_text=_("Отчетный год"),
|
||||
)
|
||||
period_start = models.BigIntegerField(
|
||||
_("на начало периода"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Значение на начало периода (тыс. руб.)"),
|
||||
)
|
||||
period_end = models.BigIntegerField(
|
||||
_("на конец периода"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Значение на конец периода (тыс. руб.)"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "parsers_financial_report_line"
|
||||
verbose_name = _("строка финансового отчета")
|
||||
verbose_name_plural = _("строки финансовых отчетов")
|
||||
indexes = [
|
||||
models.Index(fields=["report", "form_code", "line_code"]),
|
||||
models.Index(fields=["year", "line_code"]),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["report", "form_code", "line_code", "year"],
|
||||
name="unique_report_line_year",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.line_code} ({self.line_name[:30]}) - {self.year}"
|
||||
|
||||
Reference in New Issue
Block a user