"""Indicadores financeiros do relatório "Gerar Dashboard" (ver `ContabilApuracaoViewSet.dashboard()` em views.py) — funções puras, mesmo espírito de `regras.py`: recebem os dados já extraídos (nunca tocam no ORM) e devolvem um resultado calculado, fácil de revisar/testar isoladamente. 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). Se um cliente usar uma numeração de plano de contas diferente da vista até agora, os indicadores desse cliente saem errados silenciosamente — revisar contra mais balancetes reais antes de confiar cegamente no valor exibido ao cliente (ver "Fórmulas" no CLAUDE.md do pacote).""" 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, )