92 lines
4.3 KiB
Python
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)]
|