portal_publico/portal_api/dashboard_contabil/modelos.py

116 lines
3.7 KiB
Python

"""Dataclasses puras (sem ORM) do Dashboard Contábil: o formato intermediário
entre o PDF extraído (`parser.py`) e as regras de auditoria (`regras.py`).
Mesmo padrão de `portal_api.planos_saude.modelos`/`portal_api.indicadores.pipeline`
— quem persiste em model é a view, este módulo só representa dados em memória."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date
from decimal import Decimal
@dataclass
class CabecalhoExtraido:
codigo_empresa: str
nome_empresa: str
cnpj: str
periodo_inicio: date
periodo_fim: date
@dataclass
class LinhaBalanceteExtraida:
conta_numero: int
codigo: str
descricao: str
tipo: str # "S" (sintética) ou "A" (analítica)
saldo_anterior: Decimal
debito: Decimal
credito: Decimal
saldo_atual: Decimal
@dataclass
class LinhaDreExtraida:
ordem: int
descricao: str
nivel: int
valor: Decimal
totalizador: bool
@dataclass
class ValorMensalAnaliseVertical:
valor: Decimal
percentual: Decimal
@dataclass
class LinhaAnaliseVerticalExtraida:
"""Uma linha da seção "Demonstração Mensal (Análise Vertical)" — mesma
descrição/nível/negrito de `LinhaDreExtraida` (é a mesma árvore da DRE),
só que com um valor+percentual por mês em vez de um valor único (ver
`parser.py`)."""
ordem: int
descricao: str
nivel: int
totalizador: bool
valores: list[ValorMensalAnaliseVertical]
@dataclass
class ResultadoExtracao:
cabecalho: CabecalhoExtraido
contas: list[LinhaBalanceteExtraida]
linhas_dre: list[LinhaDreExtraida]
meses_analise_vertical: list[str] = field(default_factory=list)
linhas_analise_vertical: list[LinhaAnaliseVerticalExtraida] = field(default_factory=list)
# `True` quando o título de seção (DRE/Análise Vertical) só bateu depois
# de normalizar acento (ver `_normaliza_titulo`/`fonte_pdf_atipica` em
# `parser.py`) — sinal de que este PDF foi gerado com uma fonte
# diferente da do relatório de referência, o mesmo tipo de variação que
# também pode grudar palavras em descrições de conta (confirmado contra
# `1751 - Balancete 07.2026.pdf`). A view persiste isto em
# `ContabilApuracao.fonte_pdf_atipica`, só pra alertar o contador — não
# bloqueia nem tenta corrigir nada automaticamente.
fonte_pdf_atipica: bool = False
@dataclass
class AchadoDetectado:
regra: str
severidade: str # "alta" | "media" | "baixa"
titulo: str
mensagem: str
codigo_conta: str | None = None
valor_referencia: Decimal | None = None
# Preenchido só por regras que não têm uma conta do Balancete por trás
# (hoje, só `regra_variacao_atipica_dre`) — é o `ordem` (posição de
# leitura do PDF) da `LinhaAnaliseVerticalExtraida` referenciada, único
# dentro da apuração, usado pela view pra casar com a
# `ContabilLinhaAnaliseVertical` já persistida e alimentar o botão "Ver
# na tabela" (ver `AchadoDetectado`/`ContabilAchado.linha_analise_vertical`
# em views.py/models.py).
ordem_linha_analise_vertical: int | None = None
@dataclass
class SnapshotHistorico:
"""Retrato mínimo de uma apuração anterior já concluída/em revisão da
mesma empresa — usado pelas regras de variação mês a mês (ver
`regras.py`). `contas`/`linhas_dre` são dicionários já resolvidos
(codigo/descrição → valor) pra não precisar reconsultar o ORM dentro das
regras, que são funções puras."""
competencia: date
contas: dict[str, Decimal] = field(default_factory=dict)
linhas_dre: dict[str, Decimal] = field(default_factory=dict)
@dataclass
class ResultadoProcessamento:
extracao: ResultadoExtracao
achados: list[AchadoDetectado]