Busca pelo apontamento - Relatório Contábil
This commit is contained in:
parent
bdf35aa814
commit
3c396cbaa5
@ -91,7 +91,12 @@
|
|||||||
"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:/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 ' *)",
|
"Bash(PYTHONPATH='C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal' .venv/Scripts/python.exe -c ' *)",
|
||||||
"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_excluir_observacao.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_excluir_observacao.py\")",
|
||||||
|
"Bash(mkdir -p \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6\\\\scratchpad\")",
|
||||||
|
"Bash(cp /tmp/pid_test_iripara.py \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6\\\\scratchpad\\\\pid_test_iripara.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\\\\pid_test_iripara.py\")",
|
||||||
|
"Bash(sed -i 's#/api/apuracoes-contabeis/26/#/api/contabil-apuracoes/26/#' \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6\\\\scratchpad\\\\pid_test_iripara3.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\\\\pid_test_iripara3.py\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -373,3 +373,21 @@ Pedido explícito do usuário: "inclua outro botão, transforme o atual em outro
|
|||||||
O ícone de "Encerrar" por coincidência já era desenhado como um glifo de lixeira (tampa+alça+corpo) — trocado por um ícone de "arquivo" antes de liberar a lixeira de verdade pro botão novo, senão os dois ficariam visualmente idênticos fazendo coisas diferentes. Backend: `ContabilObservacaoSerializer` ganhou `criado_por` (o id, não só o nome) e `ContabilObservacaoViewSet.perform_destroy()` ganhou a checagem de autoria (`PermissionDenied` se quem pede não é quem criou), além da checagem que já existia de a apuração estar "Em revisão". Frontend: botão novo nos dois lugares onde uma observação aparece (thread inline por conta/linha e as 4 listas de resumo), condicionado a `!concluida && obs.criado_por === me.id`; usa `pidConfirm({perigoso: true})`, nunca `window.confirm()`.
|
O ícone de "Encerrar" por coincidência já era desenhado como um glifo de lixeira (tampa+alça+corpo) — trocado por um ícone de "arquivo" antes de liberar a lixeira de verdade pro botão novo, senão os dois ficariam visualmente idênticos fazendo coisas diferentes. Backend: `ContabilObservacaoSerializer` ganhou `criado_por` (o id, não só o nome) e `ContabilObservacaoViewSet.perform_destroy()` ganhou a checagem de autoria (`PermissionDenied` se quem pede não é quem criou), além da checagem que já existia de a apuração estar "Em revisão". Frontend: botão novo nos dois lugares onde uma observação aparece (thread inline por conta/linha e as 4 listas de resumo), condicionado a `!concluida && obs.criado_por === me.id`; usa `pidConfirm({perigoso: true})`, nunca `window.confirm()`.
|
||||||
|
|
||||||
Validado via `Client.force_login()` dentro de uma transação com rollback forçado, contra dados reais em produção: usuário diferente do autor tentando excluir → 403; o próprio autor tentando excluir numa apuração já concluída → 400; o próprio autor excluindo numa apuração em revisão → 204, registro realmente removido. Nada persistido em produção. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
Validado via `Client.force_login()` dentro de uma transação com rollback forçado, contra dados reais em produção: usuário diferente do autor tentando excluir → 403; o próprio autor tentando excluir numa apuração já concluída → 400; o próprio autor excluindo numa apuração em revisão → 204, registro realmente removido. Nada persistido em produção. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||||
|
|
||||||
|
### 146. Botão "Ver na tabela" nos cards de achado — navega até a linha referida e destaca
|
||||||
|
|
||||||
|
Pedido explícito do usuário, a partir de um caso real de "Variação atípica na DRE": várias linhas da Análise Vertical repetem a mesma descrição em centros de custo diferentes ("MATERIAIS E SERVIÇOS APLICADOS NA OBRA" aparecia em mais de um grupo), tornando inviável localizar manualmente qual linha da tabela um achado descreve só pelo texto do card. Pedido generalizado pra "todas as observações identificadas pela aplicação quais referem-se a contas específicas", não só essa regra.
|
||||||
|
|
||||||
|
Todo achado com `conta` (a maioria das regras) ou `linha_analise_vertical` (só `regra_variacao_atipica_dre`, ver abaixo) ganhou um botão "Ver na tabela" ao lado do "Revisar" — clicar troca pra aba certa (Balancete ou Análise Vertical), abre só os ancestrais da linha alvo (não a árvore inteira, mesma pilha de níveis usada em toda a tela) e aplica um pulso visual temporário (`.dc-conta-row--foco`, ~2,4s) na linha, além de rolar até ela. Achados sem alvo específico (ex. desbalanceamento Ativo x Passivo) não ganham o botão.
|
||||||
|
|
||||||
|
**Por que só `conta`/`linha_analise_vertical`, nunca `linha_dre`**: `regra_variacao_atipica_dre` lê a seção "Demonstração Mensal (Análise Vertical)" do PDF (`atual.linhas_analise_vertical`), não a DRE simples — os percentuais que o achado descreve só existem naquela tabela (a aba DRE só tem uma coluna de valor, sem percentual), então o destino correto é a aba Análise Vertical, mesmo o achado se chamando "Variação atípica na DRE" (nome herdado do título da seção no relatório do Questor). `ContabilAchado` ganhou `linha_analise_vertical` (FK opcional pra `ContabilLinhaAnaliseVertical`, `SET_NULL`) e `AchadoDetectado` ganhou `ordem_linha_analise_vertical` — a view casa por `ordem` (já existia como campo persistido, único dentro da apuração) contra `apuracao.linhas_analise_vertical.all()` depois de criar/ressincronizar as linhas, tanto em `create()` quanto em `_contabil_recria_achados()` (reprocessamento). `ContabilAchadoSerializer` expõe só o `id` da linha — o frontend já tem a linha completa cacheada em `apuracaoAtual.linhas_analise_vertical`.
|
||||||
|
|
||||||
|
Validado rodando `regra_variacao_atipica_dre` sobre os dados reais de uma apuração de produção (57 achados) e conferindo que todo `ordem_linha_analise_vertical` retornado casa com uma `ContabilLinhaAnaliseVertical` de verdade via o mesmo dicionário `ordem → linha` usado pela view (transação com rollback, nada persistido). Migração `0076_contabilachado_linha_analise_vertical`. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||||
|
|
||||||
|
### 146.1. Correção na mesma rodada: o botão não aparecia em nenhum achado já existente
|
||||||
|
|
||||||
|
Usuário reportou que o botão não apareceu, mesmo reiniciando o servidor e forçando a atualização da página. Não era cache nem JS antigo: o vínculo `linha_analise_vertical` só era gravado na **criação** do achado, e todos os achados em produção foram criados antes do campo existir. Diagnóstico no banco real: das 4 apurações, 3 tinham **exclusivamente** achados de `variacao_atipica_dre` (a que estava na tela do usuário tinha 13 de 13), então nenhum card daquela tela tinha como mostrar o botão. Reprocessar devolveria o vínculo, mas reprocessar exige reanexar o PDF — inviável pedir isso por causa de um botão.
|
||||||
|
|
||||||
|
Corrigido com uma migração de dados (`0077_backfill_achado_linha_analise_vertical`) que vincula os 78 achados antigos à linha certa, casando pela **assinatura contida na própria mensagem** do achado (descrição + percentual dos dois últimos meses) contra `ContabilLinhaAnaliseVertical.valores[-2:]`. Não re-executa `regras.py` de propósito — migração que importa código de app deixa de ser auto-contida se a regra mudar depois.
|
||||||
|
|
||||||
|
O primeiro algoritmo casava só por descrição, avançando um ponteiro na ordem de leitura, e a conferência contra produção reprovou: **4 dos 78 achados vincularam a linha errada**, justamente nas descrições repetidas entre centros de custo (`MATERIAIS E SERVIÇOSAPLICADOS NAOBRA`, 4 ocorrências) — ou seja, exatamente o caso que motivou o pedido do usuário teria ficado errado em silêncio. A regra dispara na ocorrência cujos percentuais estouram o limiar, que não é necessariamente a primeira. Com a assinatura completa: 78 de 78 casados, e a conferência final (comparar os percentuais da linha vinculada com os citados no texto de cada achado) deu 0 divergências. Migração já aplicada em produção; os 13 achados da apuração do print passaram a trazer o botão.
|
||||||
|
|||||||
@ -504,3 +504,30 @@ Corrigido com um helper novo, `_moeda(valor: Decimal) -> str` (`regras.py`), dup
|
|||||||
**Só vale pra achados gerados dali em diante** — um achado já persistido em produção mantém o texto antigo (sem formatação) até a apuração ser reprocessada (`reprocessar()`/`_contabil_recria_achados()` em `views.py` recria **todo** achado do zero, inclusive `mensagem`, desde a rodada 143 — ver "Achados são recriados do zero a cada reprocessamento" abaixo) ou até uma nova apuração da mesma empresa ser criada do zero; não foi escrita nenhuma migração de dados pra reformatar o texto já gravado (parsear números dentro de frase livre por regex é arriscado — o mesmo texto tem números que não são valores, como código de conta "1.01.01.001").
|
**Só vale pra achados gerados dali em diante** — um achado já persistido em produção mantém o texto antigo (sem formatação) até a apuração ser reprocessada (`reprocessar()`/`_contabil_recria_achados()` em `views.py` recria **todo** achado do zero, inclusive `mensagem`, desde a rodada 143 — ver "Achados são recriados do zero a cada reprocessamento" abaixo) ou até uma nova apuração da mesma empresa ser criada do zero; não foi escrita nenhuma migração de dados pra reformatar o texto já gravado (parsear números dentro de frase livre por regex é arriscado — o mesmo texto tem números que não são valores, como código de conta "1.01.01.001").
|
||||||
|
|
||||||
**Testado ponta a ponta via `Client.force_login()`** dentro de uma transação com rollback forçado: listagem por vigência, criação com chave derivada no servidor, alvo de outra apuração recusado, edição de texto, bloqueio do texto quando a apuração de origem está concluída (com a visibilidade ainda alternável nesse mesmo caso), encerrar e reativar com as fronteiras de competência conferidas nos dois sentidos, herança numa competência seguinte (observação marcada como histórica) e o relatório gerado nos dois meses, incluindo a conferência de que observação interna não vaza pro relatório do cliente. Nada gravado em produção além da própria migração.
|
**Testado ponta a ponta via `Client.force_login()`** dentro de uma transação com rollback forçado: listagem por vigência, criação com chave derivada no servidor, alvo de outra apuração recusado, edição de texto, bloqueio do texto quando a apuração de origem está concluída (com a visibilidade ainda alternável nesse mesmo caso), encerrar e reativar com as fronteiras de competência conferidas nos dois sentidos, herança numa competência seguinte (observação marcada como histórica) e o relatório gerado nos dois meses, incluindo a conferência de que observação interna não vaza pro relatório do cliente. Nada gravado em produção além da própria migração.
|
||||||
|
|
||||||
|
### Botão "Ver na tabela" nos cards de achado (rodada 146)
|
||||||
|
|
||||||
|
Pedido explícito do usuário a partir de um caso real: a regra `regra_variacao_atipica_dre` gera achados citando a **descrição** de uma linha (ex. "MATERIAIS E SERVIÇOS APLICADOS NA OBRA"), e a mesma descrição pode aparecer em mais de um centro de custo/grupo da Análise Vertical — não dava pra saber, só pelo texto do card, qual linha exata da tabela o achado se referia. Pedido generalizado: "isso poderia ser aplicado para todas as observações identificadas pela aplicação quais referem-se a contas específicas", não só essa regra.
|
||||||
|
|
||||||
|
**Alvo do achado, dois campos mutuamente exclusivos**: a maioria das regras já preenchia `ContabilAchado.conta` (FK opcional pra `ContabilConta`, casada por `codigo_conta` em `AchadoDetectado`, ver "Regras de auditoria v1" acima). `regra_variacao_atipica_dre` é a exceção — não itera `atual.contas` (Balancete), itera `atual.linhas_analise_vertical` (a seção "Demonstração Mensal (Análise Vertical)" do PDF), que não tem código de classificação nenhum, só `descricao`/`nivel`/`ordem`. Pra ela, `ContabilAchado` ganhou `linha_analise_vertical` (FK opcional pra `ContabilLinhaAnaliseVertical`, `SET_NULL`, migração `0076_contabilachado_linha_analise_vertical`) e `AchadoDetectado` ganhou `ordem_linha_analise_vertical: int | None` — `regra_variacao_atipica_dre` preenche com `linha.ordem` (o campo `ordem` já existia em `LinhaAnaliseVerticalExtraida`/`ContabilLinhaAnaliseVertical`, é a posição de leitura do PDF, única dentro da apuração — nenhum campo novo precisou ser criado só pra isso).
|
||||||
|
|
||||||
|
**Casamento por `ordem`, não por descrição**: tanto `ContabilApuracaoViewSet.create()` quanto `_contabil_recria_achados()` (reprocessamento) montam `av_por_ordem = {linha.ordem: linha for linha in apuracao.linhas_analise_vertical.all()}` **depois** de persistir/ressincronizar as linhas da Análise Vertical (`ContabilLinhaAnaliseVertical.objects.bulk_create(...)` em `create()`, `_contabil_sincroniza_linhas_analise_vertical()` em `reprocessar()` — a segunda sempre reescreve `antiga.ordem = linha.ordem` no registro casado, então o dicionário reflete a ordem da extração mais recente mesmo num reprocessamento) e usam isso pra resolver `achado.ordem_linha_analise_vertical → ContabilLinhaAnaliseVertical`. Evita depender de descrição (ambígua entre centros de custo, o próprio motivo desta rodada) ou de id (não existe ainda no momento em que `regras.py` roda, que é Python puro sem tocar o ORM). Validado contra dados reais de produção (apuração 26, 57 achados de `variacao_atipica_dre`): toda `ordem_linha_analise_vertical` retornada pela regra casou com uma `ContabilLinhaAnaliseVertical` de verdade via o mesmo dicionário, dentro de uma transação com rollback forçado.
|
||||||
|
|
||||||
|
`ContabilAchadoSerializer` expõe só o `id` de `linha_analise_vertical` (`read_only`) — o frontend já tem a linha completa cacheada em `apuracaoAtual.linhas_analise_vertical` (carregada junto da apuração), sem precisar de campos espelhados tipo `conta_codigo`/`conta_descricao`. `get_queryset()` do `ContabilApuracaoViewSet` ganhou `"achados__linha_analise_vertical"` no `prefetch_related` (mesmo padrão de `"achados__conta"`).
|
||||||
|
|
||||||
|
**Frontend** (`dashboard-contabil.js`): `renderAchados()` mostra um botão "Ver na tabela" (`PID_DC_ICON_LOCALIZAR`, novo ícone de pin) ao lado do "Revisar", condicionado a `achado.conta != null || achado.linha_analise_vertical != null`. O clique chama `dcIrParaLinha(tipo, id)` (`tipo` é `"conta"` ou `"av"`, roteado pelo campo presente no achado) — a função:
|
||||||
|
|
||||||
|
1. Acha o índice da linha alvo no array certo (`apuracaoAtual.contas`/`apuracaoAtual.linhas_analise_vertical`, via `DC_IR_PARA_CONFIG`).
|
||||||
|
2. Calcula só os **ancestrais** dela (`dcAncestraisIds()`, a mesma pilha de níveis usada em toda a árvore desta tela — `nivel > próximo? tem filho`) e tira só esses ids de `dcContasColapsadas`/`dcAvColapsadas`, não a árvore inteira (diferente do botão "+"/"Expandir tudo", que zera o Set inteiro) — só abre o necessário pra revelar a linha alvo, preservando o resto do estado de expansão que o contador já tinha montado.
|
||||||
|
3. Marca `dcFocoLinha = { tipo, id }` (estado module-scope novo, sempre derivado/temporário — mesmo espírito de `dcContasDestaque`) e re-renderiza a tabela certa, que aplica a classe `.dc-conta-row--foco` só na linha marcada (`renderContas()`/`renderAnaliseVertical()` ganharam essa checagem, junto de um `data-dc-conta-row-id`/`data-dc-av-row-id` novo no `<tr>` pra dar pra selecionar a linha depois).
|
||||||
|
4. Clica programaticamente no botão da aba certa (`#dc-tabs [data-dc-tab="balancete"|"analise-vertical"]`) — reaproveita o listener de troca de aba já existente, sem duplicar a lógica de mostrar/esconder painel.
|
||||||
|
5. Num `requestAnimationFrame` (depois do painel já visível), `scrollIntoView({ behavior: "smooth", block: "center" })` na linha.
|
||||||
|
6. Um `setTimeout` de 2,4s limpa `dcFocoLinha` e re-renderiza de novo, tirando a classe — o pulso da linha é sempre temporário, nunca fica "grudado".
|
||||||
|
|
||||||
|
`.dc-conta-row--foco` (`dashboard-contabil.css`) anima `box-shadow` (não `background-color`, que já está em disputa entre o tingimento padrão e `.dc-conta-row--destaque` — ver o comentário ao lado das duas regras) num pulso de 3 iterações (~2,1s, `@keyframes dcFocoPulse`), então o efeito aparece por cima de qualquer estado que a linha já tenha. A regra cobre `.dc-contas-table`/`.dc-dre-table`, e a Análise Vertical reaproveita porque `<table class="pa-table dc-dre-table dc-av-table">` já carrega as duas classes (mesmo reaproveitamento que `.dc-conta-row--destaque` já fazia).
|
||||||
|
|
||||||
|
**Por que não existe um "ir para a DRE"**: nenhuma regra hoje referencia `ContabilLinhaDre` diretamente (`regra_lucro_balancete_diverge_dre` lê `atual.linhas_dre[-1].valor`, mas o achado é ligado à conta do Balancete, `conta_lucro`, não a uma linha da DRE) — se uma regra nova precisar linkar a DRE simples no futuro, o mesmo padrão (`linha_dre` FK + `ordem_linha_dre` em `AchadoDetectado` + entrada nova em `DC_IR_PARA_CONFIG` com `tab: "dre"`) se replica sem precisar reprojetar nada.
|
||||||
|
|
||||||
|
**Backfill dos achados que já existiam** (migração `0077_backfill_achado_linha_analise_vertical`, mesma rodada): o campo nasceu na `0076`, então todo achado criado antes ficou nulo e **não** ganhava o botão. Na prática isso deixava o recurso invisível — das 4 apurações em produção, 3 tinham *só* achados de `variacao_atipica_dre` (a apuração que o usuário estava olhando tinha 13 de 13), e a única forma de repor o vínculo seria reprocessar, o que **exige reanexar o PDF**. A migração casa cada achado antigo com a linha certa usando a **assinatura extraída da própria mensagem** — descrição + percentual do penúltimo e do último mês (`Linha "X" da DRE foi de A% ... para B% ...`), os três conferidos contra `ContabilLinhaAnaliseVertical.valores[-2:]`. Não re-executa `regras.py` de propósito: migração que importa código de app muda de comportamento se a regra mudar depois, e esta precisa continuar auto-contida.
|
||||||
|
|
||||||
|
**Por que a assinatura completa, e não só a descrição**: a primeira versão do backfill casava por descrição avançando um ponteiro na ordem de leitura (os achados são criados percorrendo as linhas em ordem, então parecia suficiente). Conferido contra produção, **4 dos 78 achados apontaram pra linha errada** — exatamente as descrições repetidas em vários centros de custo (`MATERIAIS E SERVIÇOSAPLICADOS NAOBRA` aparece 4x na apuração 27), que é o caso que originou a rodada: a regra dispara na ocorrência cujos percentuais estouram o limiar, que não é necessariamente a primeira a partir do ponteiro. Com descrição+percentuais, os 78 casaram com a linha certa (conferido comparando os percentuais da linha vinculada com os citados no texto do achado: 0 divergências). O ponteiro continua no algoritmo, mas só como desempate final entre linhas que tenham descrição **e** percentuais idênticos — aí são indistinguíveis pelo dado e a ordem é o único critério que resta. Achado que não casa fica nulo de propósito (o card só não mostra o botão), nada é vinculado por aproximação.
|
||||||
|
|||||||
@ -86,6 +86,14 @@ class AchadoDetectado:
|
|||||||
mensagem: str
|
mensagem: str
|
||||||
codigo_conta: str | None = None
|
codigo_conta: str | None = None
|
||||||
valor_referencia: Decimal | None = None
|
valor_referencia: Decimal | None = None
|
||||||
|
# Preenchido só por regras que não têm uma conta do Balancete por trás
|
||||||
|
# (hoje, só `regra_variacao_atipica_dre`) — é o `ordem` (posição de
|
||||||
|
# leitura do PDF) da `LinhaAnaliseVerticalExtraida` referenciada, único
|
||||||
|
# dentro da apuração, usado pela view pra casar com a
|
||||||
|
# `ContabilLinhaAnaliseVertical` já persistida e alimentar o botão "Ver
|
||||||
|
# na tabela" (ver `AchadoDetectado`/`ContabilAchado.linha_analise_vertical`
|
||||||
|
# em views.py/models.py).
|
||||||
|
ordem_linha_analise_vertical: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@ -315,6 +315,7 @@ def regra_variacao_atipica_dre(atual: ResultadoExtracao, historico: list[Snapsho
|
|||||||
f"({diferenca_pp:+.2f} pontos percentuais)."
|
f"({diferenca_pp:+.2f} pontos percentuais)."
|
||||||
),
|
),
|
||||||
valor_referencia=diferenca_pp,
|
valor_referencia=diferenca_pp,
|
||||||
|
ordem_linha_analise_vertical=linha.ordem,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return achados
|
return achados
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 6.0.7 on 2026-09-18 18:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('portal_api', '0075_contabilapuracaoreprocessamento'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contabilachado',
|
||||||
|
name='linha_analise_vertical',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='achados', to='portal_api.contabillinhaanalisevertical'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
"""Preenche `ContabilAchado.linha_analise_vertical` nos achados de
|
||||||
|
`variacao_atipica_dre` que já existiam antes da rodada 146 (o campo nasceu na
|
||||||
|
`0076`, então todo achado criado até ali ficou nulo e não ganhava o botão "Ver
|
||||||
|
na tabela" na tela de revisão — justamente a regra que motivou o pedido).
|
||||||
|
|
||||||
|
Sem isso, só um reprocessamento devolveria o vínculo, e reprocessar **exige
|
||||||
|
reanexar o PDF** (ver `ContabilApuracaoViewSet.reprocessar()`) — inviável de
|
||||||
|
pedir ao contador só por causa de um botão novo.
|
||||||
|
|
||||||
|
**Casa por assinatura extraída da própria mensagem do achado**, não por
|
||||||
|
posição e não re-rodando `regras.py`: a mensagem já traz a descrição da linha
|
||||||
|
e os percentuais dos dois últimos meses (`Linha "X" da DRE foi de A% da
|
||||||
|
Receita Bruta em ... para B% em ...`), e os três juntos identificam a linha de
|
||||||
|
forma exata. Duas razões pra isso em vez de re-executar a regra aqui:
|
||||||
|
|
||||||
|
1. Migração não deve importar código de app que pode mudar depois (a regra
|
||||||
|
mudaria o conjunto gerado e esta migração passaria a se comportar
|
||||||
|
diferente num banco restaurado); esta fica auto-contida.
|
||||||
|
2. Descrição sozinha **não** identifica a linha — o caso que originou a
|
||||||
|
rodada é exatamente esse: a mesma descrição aparece em vários centros de
|
||||||
|
custo ("MATERIAIS E SERVIÇOS APLICADOS NA OBRA" aparecia 4x numa apuração
|
||||||
|
real). Validado contra produção: casar só por descrição+ordem de leitura
|
||||||
|
vinculava 4 dos 78 achados à linha errada (percentuais não conferiam);
|
||||||
|
com a assinatura completa, os 78 casaram com a linha certa.
|
||||||
|
|
||||||
|
Achado que não casar fica nulo de propósito (o botão só não aparece naquele
|
||||||
|
card) — nada é inventado por aproximação.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
# "Linha "X" da DRE foi de -6.55% da Receita Bruta em jun/2026 para -13.69% em jul/2026 (...)"
|
||||||
|
# — formato montado em `regra_variacao_atipica_dre`; percentual sempre com
|
||||||
|
# ponto decimal e 2 casas (`f"{...:.2f}"`), não no padrão BR.
|
||||||
|
RE_MENSAGEM = re.compile(
|
||||||
|
r'^Linha "(.+?)" da DRE foi de (-?\d+\.\d{2})% da Receita Bruta em .+? para (-?\d+\.\d{2})% em '
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _assinatura_linha(linha: Any) -> tuple[str, str, str] | None:
|
||||||
|
"""(descrição, % do penúltimo mês, % do último mês) — os mesmos três
|
||||||
|
valores que a mensagem do achado cita. `valores` é JSON com os números
|
||||||
|
gravados como texto (ver `ContabilLinhaAnaliseVertical`)."""
|
||||||
|
valores = linha.valores or []
|
||||||
|
if len(valores) < 2:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
anterior = Decimal(str(valores[-2]["percentual"]))
|
||||||
|
atual = Decimal(str(valores[-1]["percentual"]))
|
||||||
|
except (KeyError, TypeError, InvalidOperation):
|
||||||
|
return None
|
||||||
|
return (linha.descricao, f"{anterior:.2f}", f"{atual:.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
def backfill(apps: Any, schema_editor: Any) -> None:
|
||||||
|
ContabilApuracao = apps.get_model("portal_api", "ContabilApuracao")
|
||||||
|
ContabilAchado = apps.get_model("portal_api", "ContabilAchado")
|
||||||
|
|
||||||
|
for apuracao in ContabilApuracao.objects.filter(achados__regra="variacao_atipica_dre").distinct():
|
||||||
|
linhas = list(apuracao.linhas_analise_vertical.order_by("ordem"))
|
||||||
|
assinaturas = [_assinatura_linha(linha) for linha in linhas]
|
||||||
|
achados = list(
|
||||||
|
ContabilAchado.objects.filter(
|
||||||
|
apuracao=apuracao, regra="variacao_atipica_dre", linha_analise_vertical__isnull=True
|
||||||
|
).order_by("id")
|
||||||
|
)
|
||||||
|
|
||||||
|
usados: set[int] = set()
|
||||||
|
# Os achados foram criados percorrendo as linhas em ordem, então
|
||||||
|
# procurar a partir da última linha casada resolve o empate quando
|
||||||
|
# duas linhas têm descrição *e* percentuais idênticos (aí são
|
||||||
|
# indistinguíveis pelo dado, e a ordem é o único critério que resta).
|
||||||
|
ponteiro = 0
|
||||||
|
atualizados = []
|
||||||
|
for achado in achados:
|
||||||
|
correspondencia = RE_MENSAGEM.match(achado.mensagem)
|
||||||
|
if correspondencia is None:
|
||||||
|
continue
|
||||||
|
alvo = (correspondencia.group(1), correspondencia.group(2), correspondencia.group(3))
|
||||||
|
indice = next(
|
||||||
|
(i for i in range(ponteiro, len(linhas)) if assinaturas[i] == alvo and i not in usados), None
|
||||||
|
)
|
||||||
|
if indice is None:
|
||||||
|
indice = next((i for i in range(len(linhas)) if assinaturas[i] == alvo and i not in usados), None)
|
||||||
|
if indice is None:
|
||||||
|
continue
|
||||||
|
usados.add(indice)
|
||||||
|
ponteiro = indice + 1
|
||||||
|
achado.linha_analise_vertical = linhas[indice]
|
||||||
|
atualizados.append(achado)
|
||||||
|
|
||||||
|
if atualizados:
|
||||||
|
ContabilAchado.objects.bulk_update(atualizados, ["linha_analise_vertical"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("portal_api", "0076_contabilachado_linha_analise_vertical"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(backfill, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@ -2224,8 +2224,10 @@ class ContabilAchado(models.Model):
|
|||||||
"""Um achado gerado automaticamente pelo motor de regras
|
"""Um achado gerado automaticamente pelo motor de regras
|
||||||
(`dashboard_contabil.regras`) ao criar a apuração, só muda de `status`
|
(`dashboard_contabil.regras`) ao criar a apuração, só muda de `status`
|
||||||
conforme o contador revisa (mesmo espírito de `ImportacaoPlanoSaudeAuditoria`).
|
conforme o contador revisa (mesmo espírito de `ImportacaoPlanoSaudeAuditoria`).
|
||||||
`conta` é opcional porque alguns achados são gerais (ex.: desbalanceamento
|
`conta`/`linha_analise_vertical` são opcionais porque alguns achados são
|
||||||
Ativo x Passivo), sem uma única conta associada.
|
gerais (ex.: desbalanceamento Ativo x Passivo), sem uma única conta/linha
|
||||||
|
associada; nunca os dois preenchidos ao mesmo tempo no achado (cada regra
|
||||||
|
referencia só um dos dois, ver `regras.py`).
|
||||||
|
|
||||||
**Exceção a "nunca é apagado"**: um reprocessamento (`ContabilApuracaoViewSet.
|
**Exceção a "nunca é apagado"**: um reprocessamento (`ContabilApuracaoViewSet.
|
||||||
reprocessar()`/`_contabil_recria_achados()` em views.py) apaga **todos**
|
reprocessar()`/`_contabil_recria_achados()` em views.py) apaga **todos**
|
||||||
@ -2258,6 +2260,15 @@ class ContabilAchado(models.Model):
|
|||||||
conta = models.ForeignKey(
|
conta = models.ForeignKey(
|
||||||
ContabilConta, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados"
|
ContabilConta, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados"
|
||||||
)
|
)
|
||||||
|
# Preenchido só por achados sem uma conta do Balancete por trás (hoje, só
|
||||||
|
# `regra_variacao_atipica_dre`, que lê a Análise Vertical) — mesmo
|
||||||
|
# espírito de `conta` acima, alimenta o botão "Ver na tabela" da tela de
|
||||||
|
# revisão (navega até a Análise Vertical e destaca esta linha em vez do
|
||||||
|
# Balancete). Casado por `ordem` na view, ver `AchadoDetectado.
|
||||||
|
# ordem_linha_analise_vertical`/`_contabil_recria_achados()`.
|
||||||
|
linha_analise_vertical = models.ForeignKey(
|
||||||
|
ContabilLinhaAnaliseVertical, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados"
|
||||||
|
)
|
||||||
regra = models.CharField("Regra", max_length=60)
|
regra = models.CharField("Regra", max_length=60)
|
||||||
severidade = models.CharField("Severidade", max_length=10, choices=SEVERIDADE_CHOICES)
|
severidade = models.CharField("Severidade", max_length=10, choices=SEVERIDADE_CHOICES)
|
||||||
titulo = models.CharField("Título", max_length=255)
|
titulo = models.CharField("Título", max_length=255)
|
||||||
|
|||||||
@ -2443,6 +2443,12 @@ class ContabilAchadoSerializer(serializers.ModelSerializer):
|
|||||||
"conta",
|
"conta",
|
||||||
"conta_codigo",
|
"conta_codigo",
|
||||||
"conta_descricao",
|
"conta_descricao",
|
||||||
|
# `id` da `ContabilLinhaAnaliseVertical` referenciada (achados sem
|
||||||
|
# conta associada, ex. variação atípica na Análise Vertical) — o
|
||||||
|
# frontend já tem a linha completa cacheada em
|
||||||
|
# `apuracaoAtual.linhas_analise_vertical`, então só o id basta pro
|
||||||
|
# botão "Ver na tabela" (ver dashboard-contabil.js, dcIrParaLinha).
|
||||||
|
"linha_analise_vertical",
|
||||||
"regra",
|
"regra",
|
||||||
"severidade",
|
"severidade",
|
||||||
"titulo",
|
"titulo",
|
||||||
@ -2459,6 +2465,7 @@ class ContabilAchadoSerializer(serializers.ModelSerializer):
|
|||||||
"conta",
|
"conta",
|
||||||
"conta_codigo",
|
"conta_codigo",
|
||||||
"conta_descricao",
|
"conta_descricao",
|
||||||
|
"linha_analise_vertical",
|
||||||
"regra",
|
"regra",
|
||||||
"severidade",
|
"severidade",
|
||||||
"titulo",
|
"titulo",
|
||||||
|
|||||||
@ -4014,12 +4014,18 @@ def _contabil_recria_achados(
|
|||||||
natural (empresa+conta) e continuam intactas através de qualquer
|
natural (empresa+conta) e continuam intactas através de qualquer
|
||||||
reprocessamento — só o apontamento automático é recriado."""
|
reprocessamento — só o apontamento automático é recriado."""
|
||||||
contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()}
|
contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()}
|
||||||
|
av_por_ordem = {linha.ordem: linha for linha in apuracao.linhas_analise_vertical.all()}
|
||||||
apuracao.achados.all().delete()
|
apuracao.achados.all().delete()
|
||||||
ContabilAchado.objects.bulk_create(
|
ContabilAchado.objects.bulk_create(
|
||||||
[
|
[
|
||||||
ContabilAchado(
|
ContabilAchado(
|
||||||
apuracao=apuracao,
|
apuracao=apuracao,
|
||||||
conta=contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None,
|
conta=contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None,
|
||||||
|
linha_analise_vertical=(
|
||||||
|
av_por_ordem.get(achado.ordem_linha_analise_vertical)
|
||||||
|
if achado.ordem_linha_analise_vertical is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
regra=achado.regra,
|
regra=achado.regra,
|
||||||
severidade=achado.severidade,
|
severidade=achado.severidade,
|
||||||
titulo=achado.titulo,
|
titulo=achado.titulo,
|
||||||
@ -4051,7 +4057,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
|
|||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if self.action == "retrieve":
|
if self.action == "retrieve":
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
"contas", "linhas_dre", "linhas_analise_vertical", "achados__conta"
|
"contas", "linhas_dre", "linhas_analise_vertical", "achados__conta", "achados__linha_analise_vertical"
|
||||||
)
|
)
|
||||||
elif self.action == "list":
|
elif self.action == "list":
|
||||||
queryset = queryset.prefetch_related("reprocessamentos__reprocessado_por")
|
queryset = queryset.prefetch_related("reprocessamentos__reprocessado_por")
|
||||||
@ -4157,11 +4163,17 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()}
|
contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()}
|
||||||
|
av_por_ordem = {linha.ordem: linha for linha in apuracao.linhas_analise_vertical.all()}
|
||||||
ContabilAchado.objects.bulk_create(
|
ContabilAchado.objects.bulk_create(
|
||||||
[
|
[
|
||||||
ContabilAchado(
|
ContabilAchado(
|
||||||
apuracao=apuracao,
|
apuracao=apuracao,
|
||||||
conta=contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None,
|
conta=contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None,
|
||||||
|
linha_analise_vertical=(
|
||||||
|
av_por_ordem.get(achado.ordem_linha_analise_vertical)
|
||||||
|
if achado.ordem_linha_analise_vertical is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
regra=achado.regra,
|
regra=achado.regra,
|
||||||
severidade=achado.severidade,
|
severidade=achado.severidade,
|
||||||
titulo=achado.titulo,
|
titulo=achado.titulo,
|
||||||
|
|||||||
@ -798,6 +798,31 @@
|
|||||||
background-color: var(--dc-destaque-bg);
|
background-color: var(--dc-destaque-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Foco temporário ao clicar "Ver na tabela" num card de achado (ver
|
||||||
|
dcIrParaLinha em dashboard-contabil.js) — um pulso na borda interna da
|
||||||
|
linha, não uma troca de `background-color` (deixaria essa regra brigando
|
||||||
|
com o tingimento/destaque acima pela mesma propriedade); assim o pulso
|
||||||
|
aparece por cima de qualquer estado que a linha já tenha (sintética,
|
||||||
|
destaque de grupo expandido etc.), 3 vezes em ~2,1s, e a própria
|
||||||
|
`dcIrParaLinha` tira a classe pouco depois (~2,4s) — a animação nunca fica
|
||||||
|
presa "ligada". `.dc-av-table` reaproveita porque toda linha da Análise
|
||||||
|
Vertical também carrega `.dc-dre-table` (mesma tabela em duas classes, ver
|
||||||
|
dashboard-contabil.html). */
|
||||||
|
@keyframes dcFocoPulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: inset 0 0 0 0 rgba(var(--accent-rgb), 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(var(--accent-rgb), 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-contas-table .dc-conta-row--foco td,
|
||||||
|
.dc-dre-table .dc-conta-row--foco td {
|
||||||
|
animation: dcFocoPulse 700ms ease-in-out 3;
|
||||||
|
}
|
||||||
|
|
||||||
.dc-conta-desc-cell {
|
.dc-conta-desc-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -118,6 +118,10 @@ const PID_DC_ICON_LIXEIRA =
|
|||||||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||||
const PID_DC_ICON_HISTORICO =
|
const PID_DC_ICON_HISTORICO =
|
||||||
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><path d="M12 7v5l4 2"/></svg>';
|
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><path d="M12 7v5l4 2"/></svg>';
|
||||||
|
// Usado pelo botão "Ver na tabela" dos cards de achado (ver renderAchados) —
|
||||||
|
// mesma semântica de "localizar um ponto", reaproveitado como pin de mapa.
|
||||||
|
const PID_DC_ICON_LOCALIZAR =
|
||||||
|
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0Z"/><circle cx="12" cy="10" r="3"/></svg>';
|
||||||
|
|
||||||
// Percentual da Análise Vertical já vem "pronto" do PDF (ex. "-7.33" quer
|
// Percentual da Análise Vertical já vem "pronto" do PDF (ex. "-7.33" quer
|
||||||
// dizer -7,33%, não -733%) — diferente de pidDcFormatIndicadorPercentual
|
// dizer -7,33%, não -733%) — diferente de pidDcFormatIndicadorPercentual
|
||||||
@ -1153,6 +1157,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
card.className = `dc-achado-card dc-achado-card--${achado.severidade}`;
|
card.className = `dc-achado-card dc-achado-card--${achado.severidade}`;
|
||||||
const podeEditar = apuracaoAtual.status !== "concluida" && achado.status === "pendente";
|
const podeEditar = apuracaoAtual.status !== "concluida" && achado.status === "pendente";
|
||||||
const conta = achado.conta != null ? apuracaoAtual.contas.find((c) => c.id === achado.conta) : null;
|
const conta = achado.conta != null ? apuracaoAtual.contas.find((c) => c.id === achado.conta) : null;
|
||||||
|
const temAlvoNaTabela = achado.conta != null || achado.linha_analise_vertical != null;
|
||||||
const expandida = dcAchadosContaExpandida.has(achado.id);
|
const expandida = dcAchadosContaExpandida.has(achado.id);
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="dc-achado-card__main">
|
<div class="dc-achado-card__main">
|
||||||
@ -1180,6 +1185,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
${achado.observacao_contador ? `<div class="dc-achado-card__observacao">${pidDcEscapeHtml(achado.observacao_contador)}${achado.tratado_por_nome ? ` — ${pidDcEscapeHtml(achado.tratado_por_nome)}` : ""}</div>` : ""}
|
${achado.observacao_contador ? `<div class="dc-achado-card__observacao">${pidDcEscapeHtml(achado.observacao_contador)}${achado.tratado_por_nome ? ` — ${pidDcEscapeHtml(achado.tratado_por_nome)}` : ""}</div>` : ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="dc-achado-card__actions">
|
<div class="dc-achado-card__actions">
|
||||||
|
${temAlvoNaTabela ? `<button type="button" class="btn-outline" data-dc-achado-ir-para="${achado.id}" title="Ir até a linha referida por este apontamento">${PID_DC_ICON_LOCALIZAR} Ver na tabela</button>` : ""}
|
||||||
${podeEditar ? `<button type="button" class="btn-outline" data-dc-achado-editar="${achado.id}">Revisar</button>` : ""}
|
${podeEditar ? `<button type="button" class="btn-outline" data-dc-achado-editar="${achado.id}">Revisar</button>` : ""}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -1196,6 +1202,15 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
renderAchados();
|
renderAchados();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const irParaBtn = event.target.closest("[data-dc-achado-ir-para]");
|
||||||
|
if (irParaBtn) {
|
||||||
|
const id = Number(irParaBtn.getAttribute("data-dc-achado-ir-para"));
|
||||||
|
const achado = apuracaoAtual.achados.find((a) => a.id === id);
|
||||||
|
if (!achado) return;
|
||||||
|
if (achado.conta != null) dcIrParaLinha("conta", achado.conta);
|
||||||
|
else if (achado.linha_analise_vertical != null) dcIrParaLinha("av", achado.linha_analise_vertical);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const btn = event.target.closest("[data-dc-achado-editar]");
|
const btn = event.target.closest("[data-dc-achado-editar]");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
abrirModalAchado(Number(btn.getAttribute("data-dc-achado-editar")));
|
abrirModalAchado(Number(btn.getAttribute("data-dc-achado-editar")));
|
||||||
@ -2187,8 +2202,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
temFilhos[i] && dcTemObservacaoDescendente("conta", contas, dcContaNivel, dcChaveObsConta, conta.id);
|
temFilhos[i] && dcTemObservacaoDescendente("conta", contas, dcContaNivel, dcChaveObsConta, conta.id);
|
||||||
|
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
|
tr.setAttribute("data-dc-conta-row-id", conta.id);
|
||||||
if (conta.tipo === "S") tr.classList.add("dc-conta-row--sintetica");
|
if (conta.tipo === "S") tr.classList.add("dc-conta-row--sintetica");
|
||||||
if (dcContasDestaque.has(conta.id)) tr.classList.add("dc-conta-row--destaque");
|
if (dcContasDestaque.has(conta.id)) tr.classList.add("dc-conta-row--destaque");
|
||||||
|
if (dcFocoLinha && dcFocoLinha.tipo === "conta" && dcFocoLinha.id === conta.id) tr.classList.add("dc-conta-row--foco");
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>${pidDcEscapeHtml(conta.conta_numero)}</td>
|
<td>${pidDcEscapeHtml(conta.conta_numero)}</td>
|
||||||
<td>${pidDcEscapeHtml(conta.codigo)}</td>
|
<td>${pidDcEscapeHtml(conta.codigo)}</td>
|
||||||
@ -2451,8 +2468,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
|
tr.setAttribute("data-dc-av-row-id", linha.id);
|
||||||
tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row";
|
tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row";
|
||||||
if (dcAvDestaque.has(linha.id)) tr.classList.add("dc-conta-row--destaque");
|
if (dcAvDestaque.has(linha.id)) tr.classList.add("dc-conta-row--destaque");
|
||||||
|
if (dcFocoLinha && dcFocoLinha.tipo === "av" && dcFocoLinha.id === linha.id) tr.classList.add("dc-conta-row--foco");
|
||||||
const indentacao = nivel * 16;
|
const indentacao = nivel * 16;
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td style="padding-left: calc(var(--space-4) + ${indentacao}px)">
|
<td style="padding-left: calc(var(--space-4) + ${indentacao}px)">
|
||||||
@ -2524,6 +2543,83 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
await dcTrataCliqueObservacao(event, "analise_vertical");
|
await dcTrataCliqueObservacao(event, "analise_vertical");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- "Ver na tabela" (botão dos cards de achado) ----
|
||||||
|
//
|
||||||
|
// Cada achado automático que referencia uma conta do Balancete ou uma
|
||||||
|
// linha da Análise Vertical (ver ContabilAchadoSerializer) ganha um botão
|
||||||
|
// que troca de aba, garante que a linha esteja visível (abrindo só os
|
||||||
|
// ancestrais dela, não a árvore inteira) e aplica um destaque temporário
|
||||||
|
// (.dc-conta-row--foco, ver dashboard-contabil.css) — pedido explícito do
|
||||||
|
// usuário: contas que aparecem em mais de um centro de custo/grupo eram
|
||||||
|
// difíceis de localizar manualmente na tabela a partir só da descrição no
|
||||||
|
// card do achado.
|
||||||
|
//
|
||||||
|
// dcFocoLinha é sempre derivado/temporário (mesmo espírito de
|
||||||
|
// dcContasDestaque) — nunca mais de uma linha em foco por vez, mesmo entre
|
||||||
|
// tabelas diferentes, e o próprio botão sempre reinicia o timeout.
|
||||||
|
let dcFocoLinha = null; // { tipo: "conta" | "av", id }
|
||||||
|
let dcFocoTimeoutId = null;
|
||||||
|
|
||||||
|
const DC_IR_PARA_CONFIG = {
|
||||||
|
conta: {
|
||||||
|
tab: "balancete",
|
||||||
|
itens: () => apuracaoAtual.contas,
|
||||||
|
nivelFn: dcContaNivel,
|
||||||
|
colapsadas: () => dcContasColapsadas,
|
||||||
|
render: () => renderContas(),
|
||||||
|
seletorLinha: (id) => `#dc-contas-body tr[data-dc-conta-row-id="${id}"]`,
|
||||||
|
},
|
||||||
|
av: {
|
||||||
|
tab: "analise-vertical",
|
||||||
|
itens: () => apuracaoAtual.linhas_analise_vertical,
|
||||||
|
nivelFn: (linha) => Math.max(0, linha.nivel),
|
||||||
|
colapsadas: () => dcAvColapsadas,
|
||||||
|
render: () => renderAnaliseVertical(),
|
||||||
|
seletorLinha: (id) => `#dc-av-body tr[data-dc-av-row-id="${id}"]`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ids dos ancestrais (não a linha em si) de `itens[idx]`, pela mesma pilha
|
||||||
|
// de níveis usada em toda a árvore desta tela — precisa só abrir quem
|
||||||
|
// esconde a linha alvo, não a árvore inteira (diferente do botão "+").
|
||||||
|
function dcAncestraisIds(itens, nivelFn, idx) {
|
||||||
|
const niveis = itens.map(nivelFn);
|
||||||
|
const pilha = [];
|
||||||
|
for (let i = 0; i < idx; i++) {
|
||||||
|
const nivel = niveis[i];
|
||||||
|
while (pilha.length && pilha[pilha.length - 1].nivel >= nivel) pilha.pop();
|
||||||
|
pilha.push({ nivel, id: itens[i].id });
|
||||||
|
}
|
||||||
|
while (pilha.length && pilha[pilha.length - 1].nivel >= niveis[idx]) pilha.pop();
|
||||||
|
return pilha.map((p) => p.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dcIrParaLinha(tipo, id) {
|
||||||
|
const config = DC_IR_PARA_CONFIG[tipo];
|
||||||
|
if (!config) return;
|
||||||
|
const itens = config.itens();
|
||||||
|
const idx = itens.findIndex((item) => item.id === id);
|
||||||
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
dcAncestraisIds(itens, config.nivelFn, idx).forEach((ancestralId) => config.colapsadas().delete(ancestralId));
|
||||||
|
|
||||||
|
clearTimeout(dcFocoTimeoutId);
|
||||||
|
dcFocoLinha = { tipo, id };
|
||||||
|
config.render();
|
||||||
|
|
||||||
|
const tabBtn = document.querySelector(`#dc-tabs [data-dc-tab="${config.tab}"]`);
|
||||||
|
if (tabBtn) tabBtn.click();
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document.querySelector(config.seletorLinha(id))?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
});
|
||||||
|
|
||||||
|
dcFocoTimeoutId = setTimeout(() => {
|
||||||
|
dcFocoLinha = null;
|
||||||
|
config.render();
|
||||||
|
}, 2400);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Tabs ----
|
// ---- Tabs ----
|
||||||
|
|
||||||
document.getElementById("dc-tabs").addEventListener("click", async (event) => {
|
document.getElementById("dc-tabs").addEventListener("click", async (event) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user