portal_publico/portal_api/dashboard_contabil/chaves.py

92 lines
4.3 KiB
Python

"""Chaves naturais de conta/linha do Relatório Contábil.
Python puro (sem ORM), como todo o resto deste pacote, porque a mesma regra
precisa servir a três consumidores que não podem divergir entre si: a
sincronização do reprocessamento (`_contabil_sincroniza_*()` em `views.py`),
o histórico de observações (`ContabilObservacao.alvo_chave`) e os mapas de
âncora do relatório HTML. Quando essas três definições de "é a mesma linha"
divergem, o sintoma é silencioso: linha duplicada que sobrevive ao
reprocessamento, ou observação que aparece numa conta que não é a dela.
**A chave da DRE/Análise Vertical inclui o caminho na árvore**, não só a
descrição e o nível: o mesmo rótulo aparece em ramos diferentes no mesmo
nível (caso real: "DESPESAS COM PESSOAL" sob "DESPESAS DE VENDAS" e sob
"DESPESAS ADMINISTRATIVAS"), e o par `(descricao, nivel)` sozinho não
distinguia os dois. Mesmo espírito da descrição ter entrado na chave do
Balancete quando se descobriu que o Questor reaproveita o mesmo código de
classificação entre contas analíticas irmãs.
"""
from typing import Protocol, Sequence
# Separador entre os rótulos dos grupos ancestrais dentro do caminho. Só
# precisa ser algo que não apareça numa descrição de conta do Questor; o
# `|` já é o separador entre caminho e nível.
SEPARADOR_CAMINHO = " > "
class LinhaComNivel(Protocol):
"""Qualquer linha de DRE/Análise Vertical — serve tanto para os models
(`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical`) quanto para as
dataclasses de extração (`modelos.LinhaDreExtraida`/
`LinhaAnaliseVerticalExtraida`), que não compartilham base nenhuma."""
descricao: str
nivel: int
def nivel_normalizado(nivel: int) -> int:
"""Nível nunca negativo — o parser deriva o nível da posição horizontal
do primeiro caractere (ver `parser.py`), então uma linha ligeiramente à
esquerda da raiz poderia sair em -1. `views.py` e `dashboard-contabil.js`
já normalizavam com `max(0, ...)` ao montar a árvore; a chave precisa da
mesma normalização para não depender do arredondamento."""
return max(0, nivel)
def caminhos_linhas(linhas: Sequence[LinhaComNivel]) -> list[str]:
"""Caminho de cada linha na árvore, alinhado por posição com `linhas`
(que precisa vir na ordem de leitura do PDF, `ordem` crescente).
O caminho é a descrição dos grupos ancestrais mais a da própria linha,
unidas por `SEPARADOR_CAMINHO` — a mesma pilha de níveis usada por
`regras._indices_descendentes_de_conta_redutora()`, e não comparação de
prefixo de texto. Um nível pulado (o PDF vai do 0 direto para o 2) deixa
um buraco na pilha, descartado do caminho: o que importa é a sequência de
ancestrais reais, não a profundidade numérica.
"""
caminhos: list[str] = []
pilha: list[str] = []
for linha in linhas:
nivel = nivel_normalizado(linha.nivel)
del pilha[nivel:]
while len(pilha) < nivel:
pilha.append("")
pilha.append(linha.descricao)
caminhos.append(SEPARADOR_CAMINHO.join(parte for parte in pilha if parte))
return caminhos
def chave_conta(codigo: str, descricao: str) -> str:
"""Chave natural de uma conta do Balancete. `codigo` sozinho **não** é
único (o Questor reaproveita a mesma classificação entre contas
analíticas de mesma natureza, ex. seis bancos diferentes sob o código de
"Depósitos Bancários à Vista"), por isso a descrição entra junto."""
return f"{codigo}|{descricao}"
def chave_linha(caminho: str, nivel: int) -> str:
"""Chave natural de uma linha da DRE/Análise Vertical, a partir do
caminho devolvido por `caminhos_linhas()`. O nível continua na chave
mesmo sendo quase sempre derivável do caminho: um mesmo rótulo pode
aparecer duas vezes no mesmo ramo em níveis diferentes."""
return f"{caminho}|{nivel_normalizado(nivel)}"
def chaves_linhas(linhas: Sequence[LinhaComNivel]) -> list[str]:
"""`chave_linha()` de cada linha, alinhada por posição com `linhas` — o
atalho que todos os chamadores usam, já que a chave de uma linha nunca é
calculável isoladamente, só no contexto da árvore inteira."""
caminhos = caminhos_linhas(linhas)
return [chave_linha(caminho, linha.nivel) for caminho, linha in zip(caminhos, linhas)]