diff --git a/.claude/settings.json b/.claude/settings.json index 66d2d34..084e609 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -96,7 +96,9 @@ "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\")" + "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\")", + "Bash(awk '/class ContabilApuracao\\(List|Detail\\)Serializer/,/^class [A-Z]/' portal_api/serializers.py)", + "Bash(grep -n \"Colapsadas.size ? new Set\\(\\)\\\\|Colapsadas = dcContasColapsadas.size\\\\|Colapsadas.size$\" static/js/dashboard-contabil.js)" ] } } diff --git a/portal_api/dashboard_contabil/CHANGELOG.md b/portal_api/dashboard_contabil/CHANGELOG.md index 6c9133f..c1e692e 100644 --- a/portal_api/dashboard_contabil/CHANGELOG.md +++ b/portal_api/dashboard_contabil/CHANGELOG.md @@ -391,3 +391,27 @@ Usuário reportou que o botão não apareceu, mesmo reiniciando o servidor e for 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. + +### 147. Excluir análise bloqueado depois de concluída + +Pedido explícito do usuário (com print do histórico, numa linha "Concluída"): "quando estiver Concluída, a exclusão deve ficar bloqueada." Fecha a última ação destrutiva que ainda escapava da trava de "concluiu, não se mexe mais" — reprocessar, editar observação e tratar achado já eram bloqueados, mas excluir a análise inteira continuava liberado, o que é justamente o mais destrutivo dos três. + +`ContabilApuracaoViewSet.perform_destroy()` recusa com 400 quando `status == "concluida"` (mensagem própria, não o `_contabil_garante_em_revisao`, cuja frase fala em editar observações/achados). Na lista, o ícone de lixeira some na linha concluída — mesmo padrão que o botão de reprocessar ao lado já usava, pra não misturar "some" e "aparece desabilitado" na mesma linha de ações. O clique em excluir também ganhou `try/catch` + `pidAlert`: o botão não aparece numa análise concluída, mas outra pessoa pode ter concluído depois da lista ter sido carregada, e aí o motivo precisa chegar na tela em vez de o clique não fazer nada. + +Validado via `Client.force_login()` contra dados reais: DELETE numa apuração concluída → 400 e o registro continua no banco; DELETE numa em revisão → 204, seguiu funcionando normalmente. + +**Incidente durante essa validação** (registrado de propósito, ver também a rodada 148 abaixo): o teste rodou dentro de `transaction.atomic()` com rollback forçado, o padrão usado o tempo todo aqui, mas `perform_destroy()` chama `instance.arquivo.delete(save=False)` — e **apagar arquivo do storage não é revertido por rollback de transação**. O registro da apuração 26 (PURE TECH ENERGY) voltou pelo rollback, o PDF anexado dela não. Nenhum dado analítico se perdeu (contas, DRE, Análise Vertical, os 57 achados, observações e validações estavam todos no banco) e nenhuma funcionalidade depende desse arquivo — `arquivo` não é exposto em nenhum serializer e `reprocessar()` sempre grava um upload novo, nunca lê o antigo —, mas o anexo original precisa ser reposto reanexando o mesmo PDF pelo botão "Reprocessar". Lição pra qualquer teste futuro de exclusão nesta base: rollback só protege o banco; efeito colateral em disco exige apuração descartável ou storage isolado. + +### 148. Tabelas abrem com tudo expandido; o "+" virou "−" + +Pedido explícito do usuário: "por padrão, trazer as tabelas com a expansão de todas as contas até o último nível. O comportamento do botão de + deve virar um − e recolher os níveis para que retorne no atual padrão que abre a tabela. O botão de reverter deve reverter ao novo padrão (todas expandidas)." Inverte o padrão de abertura que valia desde as primeiras rodadas nas 3 tabelas da revisão (Balancete, DRE e Análise Vertical). + +Na prática o estado inicial passou a ser exatamente o que o antigo "+" produzia — por isso a mudança é pequena: `renderRevisao()` passa `new Set()` nos três conjuntos de colapso e o botão da primeira coluna aplica `dcColapsoPadrao()` (a visão compacta, que era o padrão de abertura). O atributo e a classe foram renomeados (`data-dc-expandir-tudo`/`dc-expandir-tudo-btn` → `data-dc-recolher-grupos`/`dc-recolher-grupos-btn`), já que "expandir tudo" passaria a descrever o oposto do que o botão faz; o ícone perdeu o traço vertical do "+" e os textos de tooltip/acessibilidade foram reescritos. + +**Virou toggle logo em seguida, ainda na mesma rodada**: o usuário testou e pediu que "após apertar uma vez, o botão de − vira um + e retorna para o padrão anterior" — na primeira versão ele só recolhia, e voltar exigia achar o botão de restaurar na outra ponta do cabeçalho. O handler passou a alternar (`colapsadas.size ? new Set() : dcColapsoPadrao(...)`) e `dcAtualizaBotaoArvore()` troca ícone/tooltip/rótulo de acessibilidade no fim de cada render. A face do botão é **derivada** de quantos grupos estão recolhidos, não de um flag próprio: recolher uma única conta pelo toggle da própria linha já faz o botão virar "+", que devolve a tabela inteira. Simulado contra o Balancete real da apuração 27 (564 contas): abre em "−" com 564 visíveis → clique vira "+" com 29 → clique volta pra "−" com 564, estável em ciclo. Efeito colateral: o botão de restaurar passou a fazer o mesmo que a face "+" — mantido de propósito, é a "borracha" já conhecida de outras telas do Portal, mas dá pra remover se o usuário preferir. + +O destaque das linhas não precisou de ajuste nenhum: `dcUltimaLevaVisivel()` com o conjunto vazio já marca as folhas, que é o comportamento que o "+" tinha. Simulado contra as 4 apurações reais de produção (replicando `dcColapsoPadrao`/`dcUltimaLevaVisivel` em Python, sem tocar no banco): no padrão novo todas as linhas ficam visíveis e o destaque bate exatamente com as folhas (Balancete da apuração 27: 564 visíveis, 443 destacadas = as 443 folhas), e o "−" reduz pra visão compacta (as mesmas 564 caem pra 29). Efeito esperado e aceito: as tabelas abrem bem mais longas do que antes. + +**O relatório "Gerar Dashboard" não foi tocado** — continua nascendo recolhido (`colapsado_padrao` calculado server-side em `_contabil_arvore_contexto()`). É o documento que vai pro cliente, tem só o botão de restaurar (nunca teve "+"/"−") e mudar o que o cliente vê não foi pedido; virou o único lugar onde `dcColapsoPadrao`/`_CONTABIL_NIVEL_ABERTO_PADRAO` ainda é padrão de abertura. Se o usuário quiser o mesmo lá, é um ajuste separado. + +Mudança só em `.js`/`.html`/`.css` — não exige reiniciar o `runserver`. diff --git a/portal_api/dashboard_contabil/CLAUDE.md b/portal_api/dashboard_contabil/CLAUDE.md index 9c23460..17cacfa 100644 --- a/portal_api/dashboard_contabil/CLAUDE.md +++ b/portal_api/dashboard_contabil/CLAUDE.md @@ -74,7 +74,9 @@ Diferente de `IndicadorApuracaoViewSet`/`ImportacaoPlanoSaudeViewSet` (onde a ch **Balancete e DRE usam a mesma árvore recolhível** (`dashboard-contabil.js`): o Balancete já construía uma árvore expansível a partir do nível de indentação derivado do código de classificação (`dcContaNivel()`, contando segmentos separados por `.`) — a DRE não tem código de classificação (ver "Extração do PDF" acima), mas já carregava `nivel` pronto do backend (`ContabilLinhaDre.nivel`, derivado do `x0` de cada linha no PDF), então `renderDre()` reaproveita exatamente o mesmo algoritmo de `renderContas()` (pilha de níveis recolhidos, "tem filhos" = a próxima linha tem nível maior) só que sobre `linha.nivel` direto, sem precisar de um `dcContaNivel` equivalente. Reaproveita as mesmas classes CSS do toggle (`.dc-conta-toggle`/`.dc-conta-toggle-spacer`/`.dc-conta-desc-cell`, `dashboard-contabil.css`) — o nome genérico ("conta") já cobre as duas árvores, não precisou de classe nova. Estado de colapso é independente por aba (`dcContasColapsadas`/`dcDreColapsadas`, dois `Set()`). -**Nasce recolhida a partir do 3º segmento do código** (pedido explícito do usuário, pra reduzir a poluição visual de uma apuração com muitas contas analíticas — limiar ajustado numa rodada seguinte, ver abaixo): em vez de `renderRevisao()` reiniciar os dois `Set()` vazios (tudo expandido), `dcColapsoPadrao(itens, nivelFn)` os pré-popula com os ids de todo item que tem filhos **e** está no nível `PID_DC_NIVEL_ABERTO_PADRAO` (`2`) ou além — ex. a conta `1.01.01` (3 segmentos, nível 2) aparece aberta, mas seus filhos (`1.01.01.001`, nível 3) ficam ocultos até o contador clicar pra expandir; se expandido, um filho de nível 3 que também tenha netos nasce recolhido de novo pelo mesmo critério, então "nível 3 em diante" precisa sempre de um clique a mais, não só a primeira camada. Mesmo limiar aplicado à DRE, sobre `Math.max(0, linha.nivel)` — decisão confirmada com o usuário (a princípio o pedido citava só o Balancete, mas como a DRE reaproveita o mesmo algoritmo, o mesmo comportamento faz sentido nela também). O relatório "Gerar Dashboard" replica esse mesmo estado inicial (ver abaixo) — não é só a tela de revisão. `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (`views.py`) são a mesma constante conceitual duplicada nos dois lados (um é SPA, o outro HTML renderizado uma vez) — mudar o limiar exige ajustar os dois. +**Nasce com a árvore inteira expandida** (rodada 148, pedido explícito do usuário — inverte o padrão descrito no parágrafo seguinte, que valia até então): `renderRevisao()` reinicia os três `Set()` de colapso vazios, e o estado compacto de `dcColapsoPadrao()` virou o botão "−" do cabeçalho, aplicado sob demanda (ver "Cabeçalho: dois botões" abaixo). + +**Estado compacto (`dcColapsoPadrao`), hoje só sob demanda pelo botão "−"** (era o padrão de abertura até a rodada 148, e continua sendo o padrão do relatório "Gerar Dashboard", que é server-side e não mudou): `dcColapsoPadrao(itens, nivelFn)` popula o `Set` com os ids de todo item que tem filhos **e** está no nível `PID_DC_NIVEL_ABERTO_PADRAO` (`2`) ou além — ex. a conta `1.01.01` (3 segmentos, nível 2) aparece aberta, mas seus filhos (`1.01.01.001`, nível 3) ficam ocultos até o contador clicar pra expandir; se expandido, um filho de nível 3 que também tenha netos nasce recolhido de novo pelo mesmo critério, então "nível 3 em diante" precisa sempre de um clique a mais, não só a primeira camada. Mesmo limiar aplicado à DRE, sobre `Math.max(0, linha.nivel)` — decisão confirmada com o usuário (a princípio o pedido citava só o Balancete, mas como a DRE reaproveita o mesmo algoritmo, o mesmo comportamento faz sentido nela também). O relatório "Gerar Dashboard" continua **nascendo** nesse estado (ver abaixo) — desde a rodada 148 é o único lugar onde ele é o padrão de abertura. `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (`views.py`) são a mesma constante conceitual duplicada nos dois lados (um é SPA, o outro HTML renderizado uma vez) — mudar o limiar exige ajustar os dois. **Destaque acompanha todo grupo já expandido** (pedido explícito do usuário, mesma rodada do ajuste de limiar acima, com o mecanismo revisado na rodada 140 abaixo): ao expandir uma conta/linha, as linhas que acabaram de ficar visíveis (só os filhos **diretos**, não os netos — que continuam recolhidos pelo limiar acima) ganham um realce dourado (`.dc-conta-row--destaque`), e **mais nada** fica destacado enquanto houver grupo aberto. Dois `Set()` por árvore (os 6 resetados em `renderRevisao()` e no botão "Restaurar formatação"): `dcContasExpandidos`/`dcDreExpandidos`/`dcAvExpandidos` guardam os ids dos grupos que o contador expandiu manualmente e que continuam expandidos, e `dcContasDestaque`/`dcDreDestaque`/`dcAvDestaque` guardam o resultado **derivado** (as linhas de fato destacadas), sempre recalculado por `dcDestaqueGrupos(itens, nivelFn, colapsadas, expandidos)`: @@ -90,9 +92,11 @@ O handler de clique do toggle guarda `estavaColapsada = colapsadas.has(id)` **an Correção final é a descrita acima (`dcDestaqueGrupos()` com a base padrão **descartada** enquanto houver grupo aberto). Validada simulando o algoritmo em Python contra as contas reais da apuração `2021`/`08-2026`, reproduzindo o roteiro exato do usuário: expandir "CAIXA E EQUIVALENTES DE CAIXA" destaca só seus 2 filhos (resto da tabela limpo); expandir "CLIENTES" em seguida mantém os 2 primeiros e soma "DUPLICATAS A RECEBER"; fechar "CLIENTES" volta aos 2 primeiros; fechar "CAIXA" devolve exatamente o `Set` de destaque inicial (comparação de igualdade entre os dois conjuntos bateu). Mesmo ajuste replicado em `pidDcrArvore()` (`dashboard-contabil-relatorio.html`, réplica em JS puro desta mesma lógica no relatório "Gerar Dashboard", com `calculaDestaque()`/`expandidos`/`descendentes()` espelhando as funções do JS) — os dois lados são mantidos manualmente em sincronia, ver "Balancete e D.R.E. têm árvore recolhível igual à tela de revisão" abaixo. -**Botão "+" (expandir tudo) no cabeçalho da primeira coluna** (pedido explícito do usuário, rodada seguinte — a ideia inicial incluía um "−" que recolhesse um nível por vez, descartada pelo próprio usuário na mesma conversa, "poderia apenas um botão"): `.dc-expandir-tudo-btn` (`data-dc-expandir-tudo="balancete|dre|analise-vertical"`), dentro de um `.dc-th-linha.dc-th-linha--inicio` (modificador novo, só troca o `justify-content` pra `flex-start` — o botão vem **antes** do rótulo, ao contrário do "Restaurar formatação", que fica colado na borda direita da última coluna). Abre a árvore inteira de uma vez (`dc*Colapsadas = new Set()`), até a conta analítica mais funda, nas 3 tabelas (cada uma com o seu botão, mesmo motivo do botão de restaurar: estados de colapso independentes). **Não é um toggle** — o caminho de volta é o botão "Restaurar formatação padrão" que já existe na coluna "Observação" da mesma tabela. +**Cabeçalho: dois botões por tabela, em pontas opostas** — `.dc-recolher-grupos-btn` (`data-dc-recolher-grupos="balancete|dre|analise-vertical"`) na primeira coluna, dentro de um `.dc-th-linha.dc-th-linha--inicio` (modificador que só troca o `justify-content` pra `flex-start` — este botão vem **antes** do rótulo, ao contrário do "Restaurar formatação", colado na borda direita da última coluna), e `.dc-reset-formatacao-btn` na coluna "Observação". Cada uma das 3 tabelas tem o seu par, porque os estados de colapso são independentes. -Ponto não-óbvio: expandir tudo **zera** `dc*Expandidos` em vez de populá-lo com todos os grupos. Com o conjunto vazio, `dcDestaqueGrupos()` cai na leva padrão (`dcUltimaLevaVisivel`), que sem nada recolhido marca exatamente as **folhas** — validado contra a apuração real `2021`/`08-2026`: 133 contas visíveis, 74 destacadas, exatamente as 74 contas sem filhos (igualdade de conjuntos conferida). Popular `dc*Expandidos` com todos os grupos destacaria quase toda linha da tabela, que é o mesmo problema de "tudo destacado, nada se destaca" da tentativa falha nº 1 acima. O cabeçalho da Análise Vertical é montado em JS (`renderAnaliseVerticalHead()`, número de colunas varia com os meses), então lá o botão nasce no template string — nos outros dois é HTML fixo em `dashboard-contabil.html`; os listeners são delegados no ``, então sobrevivem ao `innerHTML` ser refeito a cada render. +O botão da primeira coluna nasceu como "+" (expandir tudo) numa rodada em que as tabelas abriam recolhidas; na rodada 148 o padrão de abertura virou "tudo expandido" e ele virou um **toggle** de duas faces: mostra "−" e aplica `dcColapsoPadrao()` (a visão compacta) enquanto nada está recolhido, mostra "+" e zera (`dc*Colapsadas = new Set()`) assim que existe qualquer grupo recolhido. A face sai de `dcAtualizaBotaoArvore(chave, colapsadas, nomeTabela)`, chamada no fim de cada `render*()` — é **derivada de `colapsadas.size`**, não de um flag próprio, então continua correta quando o contador recolhe/expande uma linha pelo toggle dela, sem passar pelo cabeçalho. O botão de restaurar passou a fazer o mesmo que a face "+"; ficou redundante e foi mantido de propósito, por ser a "borracha" que o usuário já conhece de outras telas do Portal. + +Ponto não-óbvio: os dois **zeram** `dc*Expandidos` em vez de populá-lo com todos os grupos. Com o conjunto vazio, `dcDestaqueGrupos()` cai na leva padrão (`dcUltimaLevaVisivel`), que sem nada recolhido marca exatamente as **folhas** — validado contra a apuração real `2021`/`08-2026`: 133 contas visíveis, 74 destacadas, exatamente as 74 contas sem filhos (igualdade de conjuntos conferida). Popular `dc*Expandidos` com todos os grupos destacaria quase toda linha da tabela, que é o mesmo problema de "tudo destacado, nada se destaca" da tentativa falha nº 1 acima. O cabeçalho da Análise Vertical é montado em JS (`renderAnaliseVerticalHead()`, número de colunas varia com os meses), então lá o botão nasce no template string — nos outros dois é HTML fixo em `dashboard-contabil.html`; os listeners são delegados no ``, então sobrevivem ao `innerHTML` ser refeito a cada render. **Card de achado expande a conta usada no apontamento** (`renderAchados()`, pedido explícito do usuário): `achado.conta` (id, já vem no payload de `/api/contabil-apuracoes/{id}/`, junto de `conta_codigo`/`conta_descricao`) é resolvido pra objeto completo procurando em `apuracaoAtual.contas` (`.find((c) => c.id === achado.conta)`) — sem chamada de API extra, já que a apuração inteira (contas + linhas de DRE + achados) já vem de uma vez só nesse endpoint. Só achados vinculados a uma conta específica ganham o botão "Ver conta usada no apontamento" (`.dc-achado-card__toggle-conta`) — as duas regras gerais (`balanceamento_ativo_passivo`/`debito_credito_divergente`, `conta` nulo no model) não têm uma conta única por trás, então não mostram o toggle. Expandido, mostra código/descrição/saldo anterior/débito/crédito/saldo atual da conta (`.dc-achado-card__conta`, um `
` em grid). Estado de expansão (`dcAchadosContaExpandida`, um `Set()` de ids de achado) segue o mesmo padrão de `dcContasColapsadas`/`dcDreColapsadas` — reiniciado em `renderRevisao()`. @@ -518,7 +522,7 @@ Pedido explícito do usuário a partir de um caso real: a regra `regra_variacao_ **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. +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 "Restaurar formatação", 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 `` 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. @@ -531,3 +535,24 @@ Pedido explícito do usuário a partir de um caso real: a regra `regra_variacao_ **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. + +### Excluir análise bloqueado depois de concluída (rodada 147) + +Pedido explícito do usuário. `ContabilApuracaoViewSet.perform_destroy()` levanta `ValidationError` (400) quando `status == ContabilApuracao.STATUS_CONCLUIDA` — mensagem própria em vez de reusar `_contabil_garante_em_revisao()`, cuja frase fala em editar observações/achados. Fecha a última ação destrutiva fora da trava de "concluiu, não se mexe mais": reprocessar e editar observação/achado já eram bloqueados, excluir a análise inteira (a mais destrutiva das três) não era. + +Frontend (`renderList()`): o ícone de lixeira **some** na linha concluída, mesmo padrão que o botão de reprocessar ao lado já usava — misturar "some" (reprocessar) e "aparece desabilitado" (excluir) na mesma linha de ações seria incoerente. O handler de `[data-dc-excluir]` ganhou `try/catch` + `pidAlert(e.message)`: a trava do servidor continua valendo pra uma lista carregada antes de outra pessoa concluir a análise, e nesse caso o motivo precisa aparecer na tela em vez de o clique não fazer nada. + +**Cuidado ao testar exclusão nesta ViewSet**: `perform_destroy()` chama `instance.arquivo.delete(save=False)`, e apagar arquivo do storage **não é revertido** por `transaction.set_rollback(True)` — o padrão de teste usado no resto desta documentação protege só o banco. Validar o caminho "204" de exclusão contra uma apuração real custou o PDF anexado dela (o registro voltou pelo rollback, o arquivo não; ver CHANGELOG da rodada 147). Nenhum dado analítico depende desse arquivo (`arquivo` não é exposto em serializer nenhum e `reprocessar()` sempre grava um upload novo), mas um teste futuro desse caminho precisa de apuração descartável ou storage isolado. + +### Tabelas abrem com tudo expandido; "+" virou "−" (rodada 148) + +Pedido explícito do usuário: "por padrão, trazer as tabelas com a expansão de todas as contas até o último nível. O comportamento do botão de + deve virar um − e recolher os níveis para que retorne no atual padrão que abre a tabela. O botão de reverter deve reverter ao novo padrão (todas expandidas)." + +Inverte o padrão que valia desde as primeiras rodadas (`dcColapsoPadrao()` no `renderRevisao()`): `renderRevisao()` passa `new Set()` nos três `dc*Colapsadas`, e o estado compacto virou ação sob demanda. O atributo/classe foram renomeados (`dc-expandir-tudo-btn` → `dc-recolher-grupos-btn`) porque "expandir tudo" descreveria o oposto do que o botão faz no estado inicial. + +**Refinamento pedido logo em seguida, na mesma rodada** ("após apertar uma vez, o botão de − vira um + e retorna para o padrão anterior"): a primeira versão não era toggle — o "−" só recolhia, e voltar exigia o botão de restaurar, na outra ponta do cabeçalho. Agora o handler alterna (`colapsadas.size ? new Set() : dcColapsoPadrao(...)`) e `dcAtualizaBotaoArvore()` troca ícone/`title`/`aria-label` a cada render. Simulado contra o Balancete real da apuração 27 (564 contas): abre em "−" com 564 linhas visíveis, clique → "+" com 29, clique → "−" com 564, estável em ciclo; e recolhendo **uma** conta pelo toggle da própria linha o botão já vira "+", que devolve as 564 — o efeito de derivar a face do estado em vez de guardar um flag. + +O destaque não precisou de nenhum ajuste: `dcUltimaLevaVisivel()` com o `Set` vazio já marca as folhas, que é o comportamento que o "+" tinha e que o usuário aprovou. Simulado contra as 4 apurações reais de produção (replicando `dcColapsoPadrao`/`dcUltimaLevaVisivel` em Python): no padrão novo todas as linhas ficam visíveis e o destaque bate com as folhas (Balancete da apuração 27: 564 visíveis, 443 destacadas = as 443 folhas), e o botão "−" reduz pra visão compacta (as mesmas 564 caem pra 29 visíveis). Efeito colateral esperado e aceito: as tabelas abrem bem mais longas (o Balancete dessa apuração passa de 29 pra 564 linhas de saída). + +**O relatório "Gerar Dashboard" não mudou** — continua nascendo recolhido pelo `colapsado_padrao` calculado server-side (`_contabil_arvore_contexto()`, `_CONTABIL_NIVEL_ABERTO_PADRAO`). É a única tela onde `dcColapsoPadrao` ainda é padrão de abertura, e não foi tocada de propósito: é o documento que vai pro cliente, tem só o botão de restaurar (nunca teve o "+"/"−"), e mudar o que o cliente vê não foi pedido. `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (Python) continuam sendo a mesma constante conceitual duplicada, agora com papéis diferentes em cada lado (sob demanda na revisão, padrão no relatório). + diff --git a/portal_api/views.py b/portal_api/views.py index b2e5929..eb2aaa6 100644 --- a/portal_api/views.py +++ b/portal_api/views.py @@ -4069,6 +4069,18 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet): return ContabilApuracaoDetailSerializer def perform_destroy(self, instance: ContabilApuracao) -> None: + """Análise **Concluída** não pode mais ser excluída — pedido explícito + do usuário, mesma trava de "concluiu, não se mexe mais" que já valia + pra reprocessar e pra editar observação/achado + (`_contabil_garante_em_revisao`). Mensagem própria em vez de reusar o + helper porque a dele fala em editar observações/achados, que não é o + caso aqui. Esconder o botão na lista é só UX — quem decide é isto, + inclusive pra uma tela aberta antes de outra pessoa concluir a + análise.""" + if instance.status == ContabilApuracao.STATUS_CONCLUIDA: + raise ValidationError( + {"detail": "Esta análise já foi concluída — não é mais possível excluí-la."} + ) instance.arquivo.delete(save=False) instance.delete() diff --git a/static/css/dashboard-contabil.css b/static/css/dashboard-contabil.css index 538a4d2..8f42d04 100644 --- a/static/css/dashboard-contabil.css +++ b/static/css/dashboard-contabil.css @@ -1924,7 +1924,7 @@ /* Menor que o .icon-btn padrão (38px) — vive dentro de uma célula de cabeçalho de tabela, não pode competir em tamanho com o texto da coluna. */ .dc-reset-formatacao-btn, -.dc-expandir-tudo-btn { +.dc-recolher-grupos-btn { width: 22px; height: 22px; flex-shrink: 0; diff --git a/static/js/dashboard-contabil.js b/static/js/dashboard-contabil.js index 1172a8a..7a46277 100644 --- a/static/js/dashboard-contabil.js +++ b/static/js/dashboard-contabil.js @@ -118,6 +118,14 @@ const PID_DC_ICON_LIXEIRA = ''; const PID_DC_ICON_HISTORICO = ''; +// As duas faces do botão de árvore no cabeçalho da primeira coluna das 3 +// tabelas da revisão (ver dcAtualizaBotaoArvore) — é um toggle: "−" enquanto +// a árvore está toda aberta (o padrão), "+" assim que existe qualquer grupo +// recolhido. +const PID_DC_ICON_RECOLHER_ARVORE = + ''; +const PID_DC_ICON_EXPANDIR_ARVORE = + ''; // 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 = @@ -554,9 +562,13 @@ document.addEventListener("DOMContentLoaded", async () => { ` } - + ${ + apuracao.status === "concluida" + ? "" + : `` + } `; body.appendChild(tr); @@ -761,8 +773,17 @@ document.addEventListener("DOMContentLoaded", async () => { const excluirBtn = event.target.closest("[data-dc-excluir]"); if (excluirBtn) { if (!(await pidConfirm("Excluir esta análise? Essa ação não pode ser desfeita.", { perigoso: true }))) return; - await pidExcluirApuracaoContabil(Number(excluirBtn.getAttribute("data-dc-excluir"))); + // O botão nem aparece numa análise concluída, mas o servidor recusa de + // novo (ver ContabilApuracaoViewSet.perform_destroy) — pode ter sido + // concluída por outra pessoa depois desta lista ter sido carregada, e aí + // o motivo precisa chegar na tela em vez de o clique não fazer nada. + try { + await pidExcluirApuracaoContabil(Number(excluirBtn.getAttribute("data-dc-excluir"))); + } catch (e) { + await pidAlert(e.message); + } await carregarLista(); + return; } const reprocessarBtn = event.target.closest("[data-dc-reprocessar-abrir]"); @@ -952,14 +973,18 @@ document.addEventListener("DOMContentLoaded", async () => { document.getElementById("dc-concluida-aviso").hidden = !concluida; document.getElementById("dc-concluir-btn").hidden = concluida; - dcContasColapsadas = dcColapsoPadrao(apuracao.contas, dcContaNivel); - dcDreColapsadas = dcColapsoPadrao(apuracao.linhas_dre, (linha) => Math.max(0, linha.nivel)); - // Destaque inicial = a última leva já visível de cada ramo (grupo - // colapsado por padrão OU folha genuína sem filho nenhum — ver - // dcUltimaLevaVisivel()) — nasce destacada sem precisar de nenhum - // clique, mesmo ajuste já feito no relatório "Gerar Dashboard" (ver - // pidDcrArvore() em dashboard-contabil-relatorio.html), pedido explícito - // do usuário pra não abrir a tela inteira na cor de grupo/total. + // Padrão de abertura: árvore **inteira expandida**, até a conta analítica + // mais funda (pedido explícito do usuário — antes nascia com os grupos + // recolhidos por dcColapsoPadrao(), que agora é o que o botão "−" do + // cabeçalho aplica sob demanda; ver a seção dos botões mais abaixo). + dcContasColapsadas = new Set(); + dcDreColapsadas = new Set(); + // Destaque inicial = a última leva já visível de cada ramo (com nada + // recolhido, são exatamente as folhas — ver dcUltimaLevaVisivel()) — + // nasce destacada sem precisar de nenhum clique, mesmo ajuste já feito no + // relatório "Gerar Dashboard" (ver pidDcrArvore() em + // dashboard-contabil-relatorio.html), pedido explícito do usuário pra não + // abrir a tela inteira na cor de grupo/total. dcContasDestaque = dcUltimaLevaVisivel(apuracao.contas, dcContaNivel, dcContasColapsadas); dcDreDestaque = dcUltimaLevaVisivel(apuracao.linhas_dre, (linha) => Math.max(0, linha.nivel), dcDreColapsadas); dcContasExpandidos = new Set(); @@ -986,7 +1011,7 @@ document.addEventListener("DOMContentLoaded", async () => { panel.hidden = panel.getAttribute("data-dc-panel") !== "achados"; }); } - dcAvColapsadas = dcColapsoPadrao(apuracao.linhas_analise_vertical, (linha) => Math.max(0, linha.nivel)); + dcAvColapsadas = new Set(); // mesmo padrão de abertura do Balancete/DRE acima (tudo expandido) dcAvDestaque = dcUltimaLevaVisivel( apuracao.linhas_analise_vertical, (linha) => Math.max(0, linha.nivel), @@ -1255,12 +1280,14 @@ document.addEventListener("DOMContentLoaded", async () => { return conta.codigo.split(".").length - 1; } - // A partir deste nível (equivalente ao 3º segmento do código de - // classificação, ex. "1.01.01") o Balancete/DRE nascem recolhidos — - // pedido explícito do usuário pra reduzir a poluição visual da árvore - // completa; o contador expande sob demanda pra ver o nível analítico - // abaixo. Mesmo limiar usado no relatório "Gerar Dashboard" - // (_CONTABIL_NIVEL_ABERTO_PADRAO em views.py). + // Nível (equivalente ao 3º segmento do código de classificação, ex. + // "1.01.01") a partir do qual o botão "−" do cabeçalho recolhe os grupos — + // a visão compacta, só com os totais de cada nível. Até uma rodada anterior + // este era o estado em que as tabelas **nasciam**; hoje elas abrem com tudo + // expandido (pedido explícito do usuário, ver renderRevisao) e esta + // constante só vale sob demanda. O relatório "Gerar Dashboard" segue com + // seu próprio padrão, server-side e inalterado + // (_CONTABIL_NIVEL_ABERTO_PADRAO em views.py, mesmo valor). const PID_DC_NIVEL_ABERTO_PADRAO = 2; function dcColapsoPadrao(itens, nivelFn) { @@ -2240,6 +2267,7 @@ document.addEventListener("DOMContentLoaded", async () => { } }); + dcAtualizaBotaoArvore("balancete", dcContasColapsadas, "do Balancete"); renderContasObsResumo(); } @@ -2351,6 +2379,7 @@ document.addEventListener("DOMContentLoaded", async () => { } }); + dcAtualizaBotaoArvore("dre", dcDreColapsadas, "da DRE"); renderDreObsResumo(); } @@ -2401,9 +2430,7 @@ document.addEventListener("DOMContentLoaded", async () => { row.innerHTML = ` - + Descrição @@ -2415,7 +2442,7 @@ document.addEventListener("DOMContentLoaded", async () => { .join("")} Observação - @@ -2498,6 +2525,7 @@ document.addEventListener("DOMContentLoaded", async () => { } }); + dcAtualizaBotaoArvore("analise-vertical", dcAvColapsadas, "da Análise Vertical"); renderAnaliseVerticalObsResumo(); } @@ -2633,33 +2661,58 @@ document.addEventListener("DOMContentLoaded", async () => { if (tab === "dashboard") await renderDashboardTab(); }); - // ---- Cabeçalho: "Expandir tudo" e "Restaurar formatação padrão" ---- - // Dois botões por tabela, em pontas opostas da linha de cabeçalho: "+" na - // primeira coluna abre a árvore inteira (nada recolhido, até a conta - // analítica mais funda), e o de restaurar, na coluna "Observação", descarta - // todo expandir/recolher manual e recalcula o mesmo estado inicial de - // dcColapsoPadrao()/dcUltimaLevaVisivel() usado quando a apuração é aberta - // (ver renderRevisao) — é o caminho de volta do "+", já que ele não é um - // toggle. Cada árvore (Balancete/DRE/Análise Vertical) tem o seu par, - // porque cada uma tem seu próprio estado de colapso/destaque. + // ---- Cabeçalho: toggle da árvore e "Restaurar formatação padrão" ---- + // Dois botões por tabela, em pontas opostas da linha de cabeçalho. O padrão + // de abertura é a árvore **inteira expandida** (ver renderRevisao), e o botão + // da primeira coluna é um **toggle** entre esse padrão e a visão compacta de + // dcColapsoPadrao() (só os totais de cada nível): // - // Expandir tudo zera `dc*Expandidos` junto (não popula com todos os grupos): - // com o conjunto vazio o destaque volta a sair de dcUltimaLevaVisivel(), que - // sem nada recolhido marca exatamente as folhas — as contas analíticas do - // último nível, que é o que interessa destacar com a árvore toda aberta. - // Popular `dc*Expandidos` com todos os grupos destacaria quase toda linha da - // tabela, o que na prática é o mesmo que não destacar nada (ver - // dcDestaqueGrupos acima). + // - Mostra "−" e recolhe, enquanto nada estiver recolhido. + // - Mostra "+" e volta ao padrão (tudo aberto), assim que houver qualquer + // grupo recolhido. + // + // A face do botão é **derivada** de `colapsadas.size` a cada render (ver + // dcAtualizaBotaoArvore), não de um flag próprio — assim ela continua certa + // mesmo quando o contador recolhe/expande uma conta pelo toggle da linha, sem + // passar por aqui. O botão de restaurar, na coluna "Observação", faz o mesmo + // que o "+" (volta ao padrão); ficou redundante quando este virou toggle, e + // foi mantido de propósito — é a "borracha" que o usuário já conhece das + // outras telas do Portal. + // + // Cada árvore (Balancete/DRE/Análise Vertical) tem o seu par, porque cada uma + // tem seu próprio estado de colapso/destaque. + // + // Os dois zeram `dc*Expandidos` junto (não populam com todos os grupos): com + // o conjunto vazio o destaque volta a sair de dcUltimaLevaVisivel(), que + // marca a última leva visível de cada ramo — as folhas, quando nada está + // recolhido. Popular `dc*Expandidos` destacaria quase toda linha da tabela, o + // que na prática é o mesmo que não destacar nada (ver dcDestaqueGrupos acima). + function dcAtualizaBotaoArvore(chave, colapsadas, nomeTabela) { + const btn = document.querySelector(`[data-dc-recolher-grupos="${chave}"]`); + if (!btn) return; + const temGrupoRecolhido = colapsadas.size > 0; + btn.innerHTML = temGrupoRecolhido ? PID_DC_ICON_EXPANDIR_ARVORE : PID_DC_ICON_RECOLHER_ARVORE; + btn.title = temGrupoRecolhido + ? "Expandir todos os níveis (padrão desta tela)" + : "Recolher os grupos (mostrar só os totais de cada nível)"; + btn.setAttribute( + "aria-label", + temGrupoRecolhido ? `Expandir todos os níveis ${nomeTabela}` : `Recolher os grupos ${nomeTabela}` + ); + } + document.getElementById("dc-contas-thead").addEventListener("click", (event) => { - if (event.target.closest("[data-dc-expandir-tudo]")) { - dcContasColapsadas = new Set(); + if (event.target.closest("[data-dc-recolher-grupos]")) { + dcContasColapsadas = dcContasColapsadas.size + ? new Set() + : dcColapsoPadrao(apuracaoAtual.contas, dcContaNivel); dcContasExpandidos = new Set(); dcContasDestaque = dcUltimaLevaVisivel(apuracaoAtual.contas, dcContaNivel, dcContasColapsadas); renderContas(); return; } if (!event.target.closest("[data-dc-reset-formatacao]")) return; - dcContasColapsadas = dcColapsoPadrao(apuracaoAtual.contas, dcContaNivel); + dcContasColapsadas = new Set(); dcContasExpandidos = new Set(); dcContasDestaque = dcUltimaLevaVisivel(apuracaoAtual.contas, dcContaNivel, dcContasColapsadas); renderContas(); @@ -2667,15 +2720,15 @@ document.addEventListener("DOMContentLoaded", async () => { document.getElementById("dc-dre-thead").addEventListener("click", (event) => { const nivelFn = (linha) => Math.max(0, linha.nivel); - if (event.target.closest("[data-dc-expandir-tudo]")) { - dcDreColapsadas = new Set(); + if (event.target.closest("[data-dc-recolher-grupos]")) { + dcDreColapsadas = dcDreColapsadas.size ? new Set() : dcColapsoPadrao(apuracaoAtual.linhas_dre, nivelFn); dcDreExpandidos = new Set(); dcDreDestaque = dcUltimaLevaVisivel(apuracaoAtual.linhas_dre, nivelFn, dcDreColapsadas); renderDre(); return; } if (!event.target.closest("[data-dc-reset-formatacao]")) return; - dcDreColapsadas = dcColapsoPadrao(apuracaoAtual.linhas_dre, nivelFn); + dcDreColapsadas = new Set(); dcDreExpandidos = new Set(); dcDreDestaque = dcUltimaLevaVisivel(apuracaoAtual.linhas_dre, nivelFn, dcDreColapsadas); renderDre(); @@ -2683,15 +2736,17 @@ document.addEventListener("DOMContentLoaded", async () => { document.getElementById("dc-av-thead").addEventListener("click", (event) => { const nivelFn = (linha) => Math.max(0, linha.nivel); - if (event.target.closest("[data-dc-expandir-tudo]")) { - dcAvColapsadas = new Set(); + if (event.target.closest("[data-dc-recolher-grupos]")) { + dcAvColapsadas = dcAvColapsadas.size + ? new Set() + : dcColapsoPadrao(apuracaoAtual.linhas_analise_vertical, nivelFn); dcAvExpandidos = new Set(); dcAvDestaque = dcUltimaLevaVisivel(apuracaoAtual.linhas_analise_vertical, nivelFn, dcAvColapsadas); renderAnaliseVertical(); return; } if (!event.target.closest("[data-dc-reset-formatacao]")) return; - dcAvColapsadas = dcColapsoPadrao(apuracaoAtual.linhas_analise_vertical, nivelFn); + dcAvColapsadas = new Set(); dcAvExpandidos = new Set(); dcAvDestaque = dcUltimaLevaVisivel(apuracaoAtual.linhas_analise_vertical, nivelFn, dcAvColapsadas); renderAnaliseVertical(); diff --git a/templates/dashboard-contabil.html b/templates/dashboard-contabil.html index d422bdc..10d3efe 100644 --- a/templates/dashboard-contabil.html +++ b/templates/dashboard-contabil.html @@ -462,8 +462,8 @@ - Conta @@ -476,7 +476,7 @@ Saldo Atual Observação - @@ -505,8 +505,8 @@ - Descrição @@ -514,7 +514,7 @@ Valor Observação -