portal_publico/portal_api/dashboard_contabil/indicadores.py

173 lines
7.5 KiB
Python

"""Cálculo de referência dos antigos 11 indicadores financeiros "de
sistema" do relatório "Gerar Dashboard" — **não é mais chamado pela
aplicação em produção**: os 11 (ROA/ROE/Kanitz/EBIT/EBITDA/Liquidez
Corrente/Liquidez Seca/Liquidez Geral/Composição do Endividamento/Grau de
Endividamento/IPL) foram migrados pra `IndicadorContabilDefinicao` de
verdade (registros no banco, com componentes e fórmula reais avaliadas por
`dashboard_contabil.formula`), a pedido explícito do usuário. Este módulo
foi **mantido de propósito** (não apagado) como referência/auditoria — se
algum dia um valor exibido parecer suspeito, dá pra recalcular aqui e
comparar contra o que o banco de indicadores está devolvendo, já que os
dois deveriam bater sempre (a migração foi conferida, indicador por
indicador, contra uma apuração real antes de entrar em produção — ver
CLAUDE.md do pacote, "Migração dos 11 indicadores de sistema"). Funções
puras, mesmo espírito de `regras.py`: recebem os dados já extraídos (nunca
tocam no ORM).
Os grupos do Balancete (Ativo Circulante, Passivo Circulante, Patrimônio
Líquido...) são identificados por código de classificação **fixo**,
calibrado contra o balancete de referência do usuário
(`792 - balancete 072026.pdf`) — mesma decisão de risco já aceita em
`regras.regra_saldo_negativo_caixa` (código fixo "1.01.01.001" pro grupo
Caixa)."""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from .modelos import SnapshotHistorico
from .parser import LINHA_DRE_DESPESAS_RECEITAS_FINANCEIRAS
CODIGO_ATIVO_TOTAL = "1"
CODIGO_ATIVO_CIRCULANTE = "1.01"
CODIGO_ESTOQUES = "1.01.08"
CODIGO_IMOBILIZADO = "1.02.05"
CODIGO_DEPRECIACAO_ACUMULADA = "1.02.05.007"
CODIGO_PASSIVO_TOTAL = "2"
CODIGO_PASSIVO_CIRCULANTE = "2.01"
CODIGO_PATRIMONIO_LIQUIDO = "2.04"
@dataclass
class IndicadoresFinanceiros:
ativo_total: Decimal
ativo_circulante: Decimal
passivo_circulante: Decimal
passivo_nao_circulante: Decimal
patrimonio_liquido: Decimal
exigivel_total: Decimal
estoques: Decimal
imobilizado: Decimal
resultado_liquido: Decimal
ebit: Decimal
ebitda: Decimal | None
depreciacao_amortizacao: Decimal | None
roa: Decimal | None
roe: Decimal | None
liquidez_corrente: Decimal | None
liquidez_seca: Decimal | None
liquidez_geral: Decimal | None
composicao_endividamento: Decimal | None
grau_endividamento: Decimal | None
ipl: Decimal | None
kanitz: Decimal | None
def _saldo(contas: dict[str, Decimal], codigo: str) -> Decimal:
"""Contas do Passivo/Patrimônio Líquido vêm com saldo negativo no
relatório (documentado no CLAUDE.md do pacote) — todo indicador aqui
trabalha em valor absoluto."""
return abs(contas.get(codigo, Decimal(0)))
def _divide(numerador: Decimal, denominador: Decimal) -> Decimal | None:
if denominador == 0:
return None
return numerador / denominador
def _busca_linha_por_trecho(dre: dict[str, Decimal], trecho: str) -> Decimal:
return next((valor for descricao, valor in dre.items() if trecho in descricao.upper()), Decimal(0))
def calcula_indicadores(
contas_atuais: dict[str, Decimal],
dre_atual: dict[str, Decimal],
resultado_liquido: Decimal,
historico: list[SnapshotHistorico],
) -> IndicadoresFinanceiros:
"""`resultado_liquido` é passado à parte (não lido de `dre_atual` por
descrição) porque é sempre a última linha da DRE, na ordem em que o
relatório imprime — mais seguro do que casar por texto (ver
`ContabilApuracaoViewSet.dashboard`, que já tem a queryset ordenada por
`ordem` à mão). `historico` vem ordenado da apuração mais recente pra
mais antiga (mesma convenção de `regras.py`) — só `historico[0]`, quando
existir, é usado, pro cálculo de Depreciação/Amortização do mês."""
ativo_total = _saldo(contas_atuais, CODIGO_ATIVO_TOTAL)
ativo_circulante = _saldo(contas_atuais, CODIGO_ATIVO_CIRCULANTE)
passivo_total = _saldo(contas_atuais, CODIGO_PASSIVO_TOTAL)
passivo_circulante = _saldo(contas_atuais, CODIGO_PASSIVO_CIRCULANTE)
patrimonio_liquido = _saldo(contas_atuais, CODIGO_PATRIMONIO_LIQUIDO)
estoques = _saldo(contas_atuais, CODIGO_ESTOQUES)
imobilizado = _saldo(contas_atuais, CODIGO_IMOBILIZADO)
# Passivo Não Circulante/Exigível a Longo Prazo não tem código calibrado
# (não aparece no balancete de referência, que não tem dívida de longo
# prazo) — por eliminação é sempre exato pela identidade contábil, sem
# depender de mais um código a adivinhar.
passivo_nao_circulante = passivo_total - passivo_circulante - patrimonio_liquido
exigivel_total = passivo_circulante + passivo_nao_circulante
despesas_receitas_financeiras = _busca_linha_por_trecho(dre_atual, LINHA_DRE_DESPESAS_RECEITAS_FINANCEIRAS)
ebit = resultado_liquido - despesas_receitas_financeiras
depreciacao_amortizacao: Decimal | None = None
if historico:
deprec_anterior = historico[0].contas.get(CODIGO_DEPRECIACAO_ACUMULADA)
if deprec_anterior is not None:
deprec_atual = _saldo(contas_atuais, CODIGO_DEPRECIACAO_ACUMULADA)
depreciacao_amortizacao = deprec_atual - abs(deprec_anterior)
ebitda = ebit + depreciacao_amortizacao if depreciacao_amortizacao is not None else None
liquidez_corrente = _divide(ativo_circulante, passivo_circulante)
# Liquidez Geral: aproximação. O Ativo Não Circulante ("1.02") hoje
# mistura Investimentos/Imobilizado com um eventual Realizável a Longo
# Prazo, sem separar — então tratamos o Realizável a Longo Prazo como
# indisponível/0. Coincide com a Liquidez Corrente quando a empresa não
# tem Passivo Não Circulante (era o caso do balancete de referência).
liquidez_geral = _divide(ativo_circulante, exigivel_total)
liquidez_seca = _divide(ativo_circulante - estoques, passivo_circulante)
composicao_endividamento = _divide(passivo_circulante, exigivel_total)
grau_endividamento = _divide(exigivel_total, patrimonio_liquido)
ipl = _divide(imobilizado, patrimonio_liquido)
roa = _divide(resultado_liquido, ativo_total)
roe = _divide(resultado_liquido, patrimonio_liquido)
# Termômetro de Insolvência de Kanitz, fórmula-livro-texto padrão — não
# validada contra o BI antigo que este relatório substitui (ver CLAUDE.md
# do pacote, "Fórmulas").
kanitz = None
if None not in (roe, liquidez_geral, liquidez_seca, liquidez_corrente, grau_endividamento):
kanitz = (
Decimal("0.05") * roe
+ Decimal("1.65") * liquidez_geral
+ Decimal("3.55") * liquidez_seca
- Decimal("1.06") * liquidez_corrente
- Decimal("0.33") * grau_endividamento
)
return IndicadoresFinanceiros(
ativo_total=ativo_total,
ativo_circulante=ativo_circulante,
passivo_circulante=passivo_circulante,
passivo_nao_circulante=passivo_nao_circulante,
patrimonio_liquido=patrimonio_liquido,
exigivel_total=exigivel_total,
estoques=estoques,
imobilizado=imobilizado,
resultado_liquido=resultado_liquido,
ebit=ebit,
ebitda=ebitda,
depreciacao_amortizacao=depreciacao_amortizacao,
roa=roa,
roe=roe,
liquidez_corrente=liquidez_corrente,
liquidez_seca=liquidez_seca,
liquidez_geral=liquidez_geral,
composicao_endividamento=composicao_endividamento,
grau_endividamento=grau_endividamento,
ipl=ipl,
kanitz=kanitz,
)