Correção das contas com sinal invertidos
This commit is contained in:
parent
86c738cc53
commit
2a12eabf62
@ -88,7 +88,9 @@
|
||||
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/sim_destaque.py\")",
|
||||
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/sim_expandir_tudo.py\")",
|
||||
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_reprocessamento_log.py\")",
|
||||
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_recria_achados.py\")"
|
||||
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_recria_achados.py\")",
|
||||
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_regra_sinal_invertido.py\")",
|
||||
"Bash(PYTHONPATH='C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal' .venv/Scripts/python.exe -c ' *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,3 +357,11 @@ Pedido explícito do usuário: "sempre que o usuário reprocessar um documento,
|
||||
`_contabil_sincroniza_achados()` (que casava por `(regra, código da conta)` e preservava `status`/`observacao_contador`/`tratado_por`/`tratado_em`/`oculto_no_relatorio`) virou `_contabil_recria_achados()` — apaga **todos** os achados da apuração (`apuracao.achados.all().delete()`) e recria do zero a partir do motor de regras rodado sobre o PDF novo (mesmo `bulk_create()` de `create()`). Todo achado nasce `pendente`, mesmo que a mesma combinação regra+conta já tivesse sido tratada com justificativa antes do reprocessamento — os valores mudaram, então a tratativa antiga deixa de fazer sentido. A preocupação original da rodada 124 (delete+recria quebraria a FK `conta` de um achado preservado) não se aplica mais: não existe mais achado "preservado" através do delete, a FK é sempre resolvida fresca contra as contas já sincronizadas na mesma transação.
|
||||
|
||||
**`ContabilObservacao` (comentário/observação do contador numa conta, sistema separado desde a rodada 126) não é tocada** — nunca teve relação com achados, é casada por chave natural (empresa+conta) e sobrevive a qualquer reprocessamento, exatamente como pedido. Validado com um script Python isolado (`transaction.atomic()` com rollback forçado, contra uma apuração real já em produção): um achado marcado manualmente como "tratado" com justificativa desapareceu depois de simular `_contabil_recria_achados()` com a mesma lista de achados detectados, dando lugar a um achado novo `pendente` com o mesmo conteúdo (regra/conta/mensagem) mas `id` diferente; uma `ContabilObservacao` criada na mesma apuração permaneceu intacta. Nada persistido em produção. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 144. Bug real: "Conta do Passivo/Ativo com saldo invertido" disparava pra conta descendente de uma redutora, não só a filha direta
|
||||
|
||||
Usuário reportou, com print de achados reais ("Conta do Passivo 'DANITHI LTDA'"/"'MPASARABIAHOLDINGPARTICIPAÇÕES E'" com saldo devedor): quando a conta "mãe" tem o sinal `"(-)"` na frente (ex. `"(-) CAPITALA INTEGRALIZAR"`), o saldo "invertido" dos filhos dela é o comportamento esperado da própria natureza daquela conta, não uma inconsistência — mas a regra `saldo_sinal_invertido` só excluía a conta que **em si** começava com `"(-)"`, não suas descendentes.
|
||||
|
||||
Corrigido com `_indices_descendentes_de_conta_redutora()` (novo, `regras.py`) — identifica toda conta com algum ancestral (não só o pai direto) redutora, usando o mesmo algoritmo de pilha de níveis já usado em toda a aplicação pra árvore de contas (nível = `codigo.count(".")`, ordem de leitura do PDF), não comparação de prefixo de código (o Questor reaproveita código de classificação entre contas analíticas irmãs, então string matching não seria confiável pra achar o pai). Validado com uma árvore sintética reproduzindo o exemplo do usuário (confirmando que o achado deixa de disparar com a correção e **dispararia** sem ela) e rodando contra as 3 apurações reais já em produção — a apuração `1751`/TAROBA (a mesma do print) teve exatamente os 2 achados dos sócios suprimidos.
|
||||
|
||||
Não afeta achados já persistidos: os 2 achados reais da apuração `1751` só somem da tela depois de reprocessar essa apuração (o motor de regras roda de novo do zero, ver rodada 143) — nenhuma edição direta no banco foi feita. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
@ -32,7 +32,7 @@ Cada `regra_*` é uma função pura: `(ResultadoExtracao da apuração atual, li
|
||||
1. **`balanceamento_ativo_passivo`** (alta) — soma do grupo Ativo (`codigo="1"`) deve fechar **exatamente** com a do Passivo (`codigo="2"`, já vem negativo no relatório) — diferença precisa ser zero, sem tolerância de centavos (removida numa rodada seguinte, pedido explícito do usuário).
|
||||
2. **`debito_credito_divergente`** (alta) — soma de Débito das contas-raiz (`codigo` sem ponto, ou seja só "1" e "2") deve bater **exatamente** com a soma de Crédito (mesma remoção de tolerância). **Não é uma checagem trivial de "todo balancete sempre bate"**: como a DRE (Resultado) não tem colunas de débito/crédito próprias neste relatório (só um valor líquido por linha), a identidade só fecha porque a movimentação de Resultado também transita pelas contas de Patrimônio Líquido do Passivo (ex.: "LUCROS/PREJUÍZOS DO EXERCÍCIO") — confirmado empiricamente contra `792 - balancete 072026.pdf` (débito total = crédito total = R$ 416.271.243,32 nas contas-raiz).
|
||||
3. **`saldo_negativo_caixa`** (alta) — conta com `codigo` começando em `1.01.01.001` (grupo Caixa) e `saldo_atual < 0`.
|
||||
4. **`saldo_sinal_invertido`** (média) — conta analítica (`tipo="A"`) do Ativo (`1.`) com saldo credor, ou do Passivo (`2.`) com saldo devedor, exceto contas redutoras (descrição começando com `"(-)"`, que são esperadas ter o sinal oposto ao grupo).
|
||||
4. **`saldo_sinal_invertido`** (média) — conta analítica (`tipo="A"`) do Ativo (`1.`) com saldo credor, ou do Passivo (`2.`) com saldo devedor, exceto contas redutoras (descrição começando com `"(-)"`, que são esperadas ter o sinal oposto ao grupo) **e exceto toda conta descendente de uma conta redutora** (`_indices_descendentes_de_conta_redutora()`, rodada 144 — bug real, ver abaixo): se a conta "mãe" (sintética, em qualquer nível acima, não só o pai direto) começa com `"(-)"`, o sinal "invertido" dos analíticos dentro dela é o comportamento esperado da própria natureza daquela conta, não uma inconsistência.
|
||||
5. **`lucro_balancete_diverge_dre`** (alta, rodada seguinte) — o resultado do exercício (lucro **ou** prejuízo) precisa ser o mesmo valor no Balancete e na DRE, sem tolerância. Lê `CODIGO_LUCRO_PREJUIZO_EXERCICIO` (`"2.04.13.002"`, código de classificação fixo pra linha sintética "LUCROS/PREJUÍZOS DO EXERCÍCIO" dentro do Patrimônio Líquido — calibrado contra os 2 balancetes reais já em produção, mesmo padrão de risco de `CODIGO_CAIXA`/`indicadores.CODIGO_*`; agrega "LUCROS DO EXERCÍCIO" ou "(-) PREJUÍZOS DO EXERCÍCIO" conforme o resultado do mês), negado (mesma convenção Passivo/PL com sinal invertido de `balanceamento_ativo_passivo`) contra `linhas_dre[-1].valor` (última linha da DRE — mesma fonte que `_ContabilDadosIndicadores.resultado_liquido` em `views.py` já usa pros indicadores). Validado batendo exato contra os 2 balancetes reais antes de entrar em produção.
|
||||
6. **`conta_transitoria_com_saldo`** (média) — descrição contém "TRANSIT" com `saldo_atual != 0`, **exceto** a palavra isolada "TRANSITO" (`\bTRANSITO\b`, "dinheiro em trânsito" — conceito diferente de conta transitória/de compensação, excluído numa rodada seguinte). A exclusão é por palavra isolada, não um trecho maior como "TRANSITOR": testando contra `1751 - Balancete 07.2026.pdf` antes de decidir, a fonte embutida corrompe o acento de "TRANSITÓRIA" num caractere ilegível (não recuperável) na extração — "TRANSITOR" nunca bateria com essa conta (que tem saldo real), então a mudança pra um trecho positivo mais longo foi descartada a favor de manter "TRANSIT" e só excluir o falso positivo conhecido.
|
||||
7. **`conta_deveria_zerar`** (média) — `TRECHOS_CONTA_DEVERIA_ZERAR` (lista curta e deliberadamente restrita — só "ADIANTAMENTOS DE SALÁRIOS", que o ITD confirma dever ficar zerada todo mês; **não** inclui "Adiantamento de Férias"/"13º Salário", que legitimamente carregam saldo entre meses).
|
||||
@ -41,6 +41,12 @@ Cada `regra_*` é uma função pura: `(ResultadoExtracao da apuração atual, li
|
||||
|
||||
**Duas regras removidas na mesma rodada** (pedido explícito do usuário, "utilizar a análise vertical" no lugar delas): `variacao_atipica_saldo` (variação de saldo de conta do Balancete contra a apuração anterior) — a Análise Vertical do PDF só cobre linhas da DRE, não contas do Balancete, então não tinha como reaproveitar a mesma fonte pra ela, e a alternativa de mantê-la como estava (usando `historico`) foi descartada a favor de simplificar o motor; `percentual_custo_receita_atipico` (razão Custos/Receita Líquida do mês contra o mês anterior, via `historico`) — a granularidade maior de `variacao_atipica_dre` sobre a Análise Vertical já cobre esse caso (e qualquer outra linha da DRE) sem precisar de uma regra dedicada.
|
||||
|
||||
**Bug real, rodada 144 — `saldo_sinal_invertido` disparava pra conta descendente de uma redutora, não só a filha direta**: usuário reportou, com print de achados reais ("Conta do Passivo 'DANITHI LTDA'"/"'MPASARABIAHOLDINGPARTICIPAÇÕES E'" com saldo devedor, ambas `2.04.01.003.001`), que o filtro de conta redutora (`_eh_conta_redutora()`, checa se a própria descrição começa com `"(-)"`) só excluía a conta que **em si** tem o prefixo — não bastava a "mãe" (sintética, em qualquer nível acima, não só o pai direto) ter o sinal `"(-)"`: as duas contas do print são analíticas dentro de `2.04.01.003` `"(-) CAPITALA INTEGRALIZAR"` (sic — espaço grudado, mesma classe de artefato de extração já documentada em "PDF de fonte atípica" acima; confirmado contra a apuração real `1751`/TAROBA em produção), então herdam o sinal devedor esperado da conta-mãe, mas não tinham `"(-)"` na própria descrição — geravam achado indevido.
|
||||
|
||||
Corrigido com `_indices_descendentes_de_conta_redutora(contas)` (novo, `regras.py`) — calcula, pra toda a árvore de uma vez, quais índices têm **algum** ancestral redutora, usando o mesmo algoritmo de pilha de níveis já usado em todo o resto da aplicação pra árvore de contas (`codigo.count(".")` + ordem de leitura do PDF, mesmo espírito de `dcUltimaLevaVisivel()`/`_contabil_arvore_contexto()`) — **não** comparação de prefixo de código: o Questor reaproveita o mesmo código de classificação entre contas analíticas irmãs (mesmo problema já documentado em `ContabilObservacao.chave_conta()`), então string matching por código não seria confiável pra achar o pai; a pilha de níveis segue a ordem/profundidade real da árvore impressa no PDF, funciona mesmo com códigos repetidos entre irmãos. `regra_saldo_sinal_invertido()` passou a pular toda conta cujo índice está nesse conjunto, além da checagem já existente na própria conta.
|
||||
|
||||
Validado de duas formas: (1) árvore sintética reproduzindo exatamente a estrutura de um exemplo do usuário (conta "(-) LUCROS DISTRIBUÍDOS" com 2 sócios dentro) — confirmado que o achado deixa de ser gerado com a correção, e que **seria** gerado sem ela (não um teste vazio por acidente); (2) rodado contra as **3 apurações reais já em produção** — a apuração `1751`/TAROBA (a mesma do print do usuário) tem exatamente os 2 achados dos sócios suprimidos, e as 3 apurações somadas têm 32 contas identificadas como "descendente de redutora" (a maioria contas de depreciação acumulada, `1.02.05.007.*`, mesmo padrão "(-) DEPREC. ..."), sem nenhum falso positivo óbvio nos nomes. **Não afeta achados já persistidos** — os 2 achados reais da apuração `1751` (`id=20`/`21`, ainda `pendente` no banco) só somem da tela depois que essa apuração for reprocessada (`_contabil_recria_achados()`, rodada 143, roda o motor de regras de novo do zero); não foi feita nenhuma edição direta no banco pra removê-los manualmente.
|
||||
|
||||
## Models (`portal_api/models.py`)
|
||||
|
||||
Padrão cabeçalho → linhas de detalhe → achados (mesma filosofia de `IndicadorApuracao`/`IndicadorApuracaoColaborador`):
|
||||
|
||||
@ -172,10 +172,43 @@ def _eh_conta_redutora(conta: LinhaBalanceteExtraida) -> bool:
|
||||
return conta.descricao.strip().startswith("(-)")
|
||||
|
||||
|
||||
def _indices_descendentes_de_conta_redutora(contas: list[LinhaBalanceteExtraida]) -> set[int]:
|
||||
"""Índices (posição em `contas`, mesma ordem de leitura do PDF) de toda
|
||||
conta que tem algum ANCESTRAL sintético (não só o pai direto) com
|
||||
descrição começando em "(-)" — pedido explícito do usuário: se a conta
|
||||
"mãe" tem o sinal de redutora, o saldo "invertido" dos filhos é o
|
||||
comportamento esperado, não uma inconsistência (ex.: "(-) LUCROS
|
||||
DISTRIBUÍDOS" é uma conta do Passivo, mas devedora por natureza — as
|
||||
contas analíticas dentro dela, um sócio por linha, herdam esse mesmo
|
||||
sinal e não deveriam gerar achado de `regra_saldo_sinal_invertido`).
|
||||
|
||||
Usa o mesmo algoritmo de nível/hierarquia já usado em toda a aplicação
|
||||
pra árvore de contas (`codigo.count(".")` + ordem de leitura do PDF, ver
|
||||
`_contabil_arvore_contexto()`/`dcContaNivel()`), **não** comparação de
|
||||
prefixo de código — o Questor reaproveita o mesmo código de classificação
|
||||
entre contas analíticas irmãs (ver `ContabilObservacao.chave_conta()`),
|
||||
então string matching por código não seria confiável pra identificar o
|
||||
pai; a pilha de níveis, sim, já que segue estritamente a ordem/profundidade
|
||||
real da árvore impressa no PDF."""
|
||||
niveis = [conta.codigo.count(".") for conta in contas]
|
||||
pilha: list[tuple[int, bool]] = [] # (nível, é redutora OU descende de uma)
|
||||
descendentes: set[int] = set()
|
||||
for i, conta in enumerate(contas):
|
||||
nivel = niveis[i]
|
||||
while pilha and pilha[-1][0] >= nivel:
|
||||
pilha.pop()
|
||||
heranca = pilha[-1][1] if pilha else False
|
||||
if heranca:
|
||||
descendentes.add(i)
|
||||
pilha.append((nivel, heranca or _eh_conta_redutora(conta)))
|
||||
return descendentes
|
||||
|
||||
|
||||
def regra_saldo_sinal_invertido(atual: ResultadoExtracao, historico: list[SnapshotHistorico]) -> list[AchadoDetectado]:
|
||||
achados = []
|
||||
for conta in atual.contas:
|
||||
if conta.tipo != "A" or conta.saldo_atual == 0 or _eh_conta_redutora(conta):
|
||||
descendentes_de_redutora = _indices_descendentes_de_conta_redutora(atual.contas)
|
||||
for i, conta in enumerate(atual.contas):
|
||||
if conta.tipo != "A" or conta.saldo_atual == 0 or _eh_conta_redutora(conta) or i in descendentes_de_redutora:
|
||||
continue
|
||||
if conta.codigo.startswith("1.") and conta.saldo_atual < 0:
|
||||
achados.append(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user