portal_publico/portal_api/dashboard_contabil/CHANGELOG.md

98 KiB
Raw Blame History

Changelog — Dashboard Contábil

Histórico específico desta aplicação, extraído de plano.md (mesma numeração de rodada usada lá, para referência cruzada).

92. Dashboard Contábil (Relatórios > Contabilidade) — v1: execução, auditoria e análise

Pedido: otimizar a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (ITD-FISCO-7513). Nova aplicação em Relatórios > Contabilidade: o contador anexa o PDF de Balancete + DRE (modelo Questor, mesmo relatório hoje enviado ao cliente), a ferramenta extrai as contas e roda um motor de 10 regras de auditoria (balanceamento Ativo x Passivo, débito ≠ crédito, saldo negativo de caixa, contas transitórias/genéricas com saldo, contas que deveriam ficar zeradas, sinal de saldo invertido, variação atípica de saldo/DRE mês a mês, percentual custo/receita fora do padrão da própria empresa), apresentando os achados numa tela de revisão com observações por conta e conclusão da análise.

Decisões confirmadas com o usuário: entrada só em PDF (levantada a alternativa de XLSX estruturado do Questor, mais confiável de extrair, mas mantido PDF como pedido originalmente); histórico de variação mês a mês fica no próprio banco do Portal, não depende de nenhum relatório complementar anexado; nasce restrita ao perfil "Inovação" (dado financeiro de cliente sensível, mesmo padrão de "Não Conformidades"); o botão "Gerar Dashboard HTML" (que substituiria o BI Contábil por um relatório final ao administrador da empresa, com exportação em XLSX) fica desabilitado nesta rodada, escopo de uma rodada futura.

Desafio técnico principal: o relatório Questor de Balancete/DRE desenha cada caractere em posição própria e inclui, por baixo do texto real, uma grade densa de caracteres de espaço cobrindo toda a linha — isso quebra a extração padrão do pdfplumber (extract_words/extract_text), que trata esses espaços como separadores reais e fragmenta números em dígitos isolados. Resolvido reconstruindo cada linha direto de page.chars, ignorando espaços literais e reinserindo um só quando o vão horizontal indica uma quebra de campo real — validado rodando de fato contra o PDF real de referência do usuário antes de escrever o parser definitivo. 4 models novos (ContabilApuracao/ContabilConta/ContabilLinhaDre/ContabilAchado, migração 0055), pacote portal_api/dashboard_contabil/ sem ORM (parser.py/regras.py/pipeline.py/modelos.py), subgrupo "Contabilidade" novo em catalogo.py dentro de relatorios. Detalhe técnico completo no CLAUDE.md desta pasta.

93. Dashboard Contábil — DRE agrupada por árvore + botão "Gerar Dashboard" implementado

A DRE (aba "DRE" da revisão) ganhou a mesma árvore recolhível que o Balancete já tinha (renderDre() reaproveita o algoritmo de renderContas(), agora sobre ContabilLinhaDre.nivel). O botão "Gerar Dashboard" (renomeado de "Gerar Dashboard HTML") saiu do estado desabilitado da rodada 92: gera um documento HTML autocontido, com a marca do escritório, com indicadores financeiros (ROA, ROE, Kanitz, EBIT, EBITDA, Liquidez Corrente/Seca/Geral, Composição/Grau de Endividamento, IPL), gráfico de evolução do Resultado Líquido (Chart.js via CDN), DRE/Balancete agrupados e as observações que o contador já registrou na aplicação. Escopo confirmado com o usuário: sempre uma apuração por vez (sem "Filial"/consolidação multi-empresa do BI antigo que este relatório substitui, fora de escopo).

Novo módulo indicadores.py (funções puras): grupos do Balancete por código fixo calibrado contra o balancete de referência (mesmo risco já aceito em regra_saldo_negativo_caixa), com o Passivo Não Circulante calculado por eliminação (sempre exato, não precisa de mais um código). Liquidez Corrente/Seca, Composição/Grau de Endividamento e IPL validados byte a byte contra a captura de tela do usuário (bateram exatamente). EBITDA fica indisponível na primeira apuração de uma empresa (precisa da variação de Depreciação/Amortização entre dois meses). Kanitz usa a fórmula padrão, não validada contra o BI antigo (valor de referência do usuário fora da faixa clássica do índice) — marcado como estimativa no relatório.

O relatório é dividido em 3 abas (Balancete → D.R.E. → Indicadores, nessa ordem pedida pelo usuário), com "Observações da Análise" fora das abas, sempre visível; na impressão a barra de abas some e as 3 aparecem seguidas, cada uma numa página.

Nova action ContabilApuracaoViewSet.dashboard() renderiza templates/dashboard-contabil-relatorio.html via render_to_string e devolve text/html puro — primeiro documento HTML autocontido do Portal (até então só PDF/CSV/ZIP). Novo portal_api/templatetags/contabil_extras.py (primeiro uso de template tags customizadas no projeto) com filtros moeda/percentual/indice/competencia. Detalhe técnico completo (todas as fórmulas e limitações conhecidas) no CLAUDE.md desta pasta.

Dois ajustes, ainda na mesma rodada, antes do usuário testar de fato: Balancete e D.R.E. do relatório ganharam a mesma árvore recolhível da tela de revisão (calculada no servidor — _contabil_arvore_contexto() em views.py — já que o relatório é HTML estático, não uma SPA); e a logo do escritório, que não estava carregando, foi corrigida trocando a action de POST pra GET — a causa raiz era o frontend abrir o resultado via fetch+blob (URL.createObjectURL), e um documento carregado de uma URL blob: tem origem sintética, quebrando a URL relativa {% static %} da logo; com GET, o frontend só faz window.open() direto na URL da API (navegação de verdade, sem blob).

Terceiro ajuste, mesmo dia: usuário achou o visual simples demais e pediu mais bonito/dinâmico/animado. Fontes "Manrope"/"Inter", gradiente+glow no cabeçalho, ícones SVG, abas com indicador deslizante, contagem animada nos cards (sempre terminando no valor exato já formatado pelos filtros Django, nunca recalculado em JS) e cor por sinal/limiar só onde é seguro sem inventar nada (ROA/ROE/EBIT/EBITDA por sinal, Liquidez por ≥1 — convenções de mercado; Kanitz/Endividamento/IPL ficam sem cor, sem limiar validado). Toda animação de entrada segue a regra de nunca fixar opacity:0 fora de @keyframes, pra @media print bastar sozinho pra devolver tudo ao normal na impressão. Corrigido também um bug de condição de corrida entre dois listeners de beforeprint que competiam entre si quando a aba "Indicadores" nunca tinha sido aberta. Detalhe completo no CLAUDE.md desta pasta ("Visual").

94. Dashboard Contábil — observações do relatório redistribuídas por aba

Pedido: a seção única "Observações da Análise" (fora de todas as abas) misturava observações de Balancete, D.R.E. e achados de auditoria juntas, sem separar por contexto. Passou a ficar assim: a aba Balancete termina com "Observações do Balancete" (só as observações de conta); a aba D.R.E. termina com "Observações da D.R.E." (só as observações de linha); a aba Indicadores termina com "Todas as Observações da Análise" — as três listas juntas (contas + linhas de DRE + achados), cada item prefixado com a origem ("Balancete — ...", "D.R.E. — ...", "Auditoria — ..."), já que ali não há mais uma aba pra dar esse contexto sozinha. Puramente reorganização de template (dashboard-contabil-relatorio.html) — as três listas já vinham prontas do backend (views.py), nenhuma mudança de backend foi necessária.

Correção ainda na mesma rodada: o prefixo do terceiro grupo tinha nascido "Achado de Auditoria —", contrariando um pedido já feito antes pelo usuário de nunca expor a palavra "achado" em texto visível (mesmo motivo pelo qual a aba de revisão já se chama "Observações", não "Achados" — dashboard-contabil.html, data-dc-tab="achados" com o texto "Observações"). Trocado pra "Auditoria —", no mesmo padrão dos outros dois grupos (nomeado pela origem/demonstração, não pelo tipo de registro interno). Vale como regra geral pra qualquer texto novo desta aplicação: achado/Achado só em nome de variável/model/classe CSS, nunca em texto visível ao usuário. Detalhe completo no CLAUDE.md desta pasta.

95. Card de achado (tela de revisão) expande a conta usada no apontamento

Pedido: nos cards de "Observações" (aba Achados da revisão), permitir expandir e ver a conta do Balancete que embasou aquele apontamento — até então só o título/mensagem da regra apareciam, sem o dado de origem. renderAchados() (dashboard-contabil.js) resolve achado.conta (id) pra objeto completo procurando em apuracaoAtual.contas, já carregado junto na mesma resposta de /api/contabil-apuracoes/{id}/ — nenhuma chamada de API nova, nenhuma mudança de backend. Um botão "Ver conta usada no apontamento" aparece só nos achados vinculados a uma conta específica (achados das regras 3-7, que têm conta preenchida); as duas regras gerais (balanceamento Ativo x Passivo, débito ≠ crédito) não mostram o botão, já que não têm uma conta única por trás. Expandido, mostra código/descrição/saldo anterior/débito/crédito/saldo atual da conta, num <dl> novo (.dc-achado-card__conta, dashboard-contabil.css). Detalhe completo no CLAUDE.md desta pasta.

96. Lista de Observações ordenada por severidade

Pedido: na aba "Observações" da revisão, as observações apareciam na ordem em que as regras rodaram (mistura de Alta/Média/Baixa), sem prioridade visual — o usuário pediu Alta primeiro, depois Média, depois Baixa. achadosFiltrados() (dashboard-contabil.js) ganhou um .sort() por PID_DC_SEVERIDADE_ORDEM ({alta: 0, media: 1, baixa: 2}) depois do filtro já existente (severidade/status); dentro de uma mesma severidade a ordem original é preservada (sort é estável). Só ordenação de exibição, nenhuma mudança de backend/modelo.

97. Resumo por categoria (donut + cards) no topo da aba Observações

Pedido: o usuário trouxe capturas de tela da auditoria de outro sistema (checklist de checagens + cards por categoria com a tabela de apontamentos + gráfico de distribuição) perguntando se dava pra estruturar algo parecido. Alinhado por AskUserQuestion que: (a) várias checagens daquele sistema dependem de dado que não vem do Balancete/DRE anexado (folha, vencimentos de fornecedor/cliente/imposto, saldo bancário) — fora do escopo já documentado desta ferramenta, não replicadas; (b) o formato escolhido foi "cards por categoria com tabela de apontamentos" + "gráfico de distribuição por categoria", só na tela de revisão do Portal (não no checklist pass/fail, não no relatório "Gerar Dashboard").

Nova seção .dc-achados-resumo no topo da aba Observações (dashboard-contabil.html), acima dos chips de filtro já existentes: um donut (CSS puro, conic-gradient — sem Chart.js/dependência nova nesta tela interativa, diferente do relatório estático que já usa Chart.js via CDN) com o total de observações no centro e legenda por categoria, mais uma grade de cards — um por regra de auditoria (as mesmas 10 de regras.py, PID_DC_REGRAS em dashboard-contabil.js — enumeradas sempre as 10, mesmo as que não geraram achado nesta apuração, mesmo espírito do "Nenhum registro encontrado" do sistema de referência) com a contagem e uma mini-tabela das contas/linhas apontadas (código+descrição da conta quando achado.conta está preenchido, "Geral" quando não — mesmas duas regras gerais que já não mostram conta no card expandido, ver rodada 95) e o status de cada uma. renderAchadosResumo() roda sempre sobre apuracaoAtual.achados completo, não sobre achadosFiltrados() — visão geral estável, independente dos chips Alta/Média/Baixa/Pendentes/Todos da lista detalhada logo abaixo. Cores das 10 categorias reaproveitam os tokens --accent-rgb/--danger-rgb/--gold-rgb/--teal-rgb/--slate-rgb/--coral-rgb já existentes (theme-aware) completados até 10 com color-mix(), sem hex novo hardcoded. Puramente frontend — nenhuma mudança de backend/model (o campo regra já existia em ContabilAchado). Detalhe completo no CLAUDE.md desta pasta.

98. Donut do resumo trocado pra severidade + donut/cards clicáveis (filtram a lista)

Dois pedidos, mesma rodada: (a) o donut da rodada 97 mostrava distribuição por categoria/regra — o usuário pediu pra mostrar por nível de complexidade (Alta/Média/Baixa) em vez disso; (b) permitir clicar no gráfico ou nos cards pra filtrar os apontamentos relacionados na lista detalhada abaixo.

Donut reconstruído em SVG puro (técnica clássica de <circle r="15.9155">, circunferência ≈ 100, então stroke-dasharray/stroke-dashoffset já valem como percentual sem precisar de pathLength) — 3 segmentos (Alta/Média/Baixa, mesmas cores dos badges: --danger/--gold/rgb(var(--slate-rgb))), cada um um <circle> clicável (pointer-events de SVG só considera o pixel realmente pintado pelo stroke, então cada fatia responde só na própria área, sem hit-test manual por ângulo). Clicar numa fatia (ou item da legenda) chama pidDcSelecionarSeveridade() — extraída da lógica que os chips "Alta"/"Média"/"Baixa" já tinham (mesmo efeito de clicar o chip: atualiza filtroSeveridade, .is-active e re-renderiza).

Cards de categoria (regra) continuam mostrando a contagem por regra, mas agora clicáveis também: alternam um novo estado filtroRegra (achado.regra exata ou null, clicar de novo no mesmo card limpa) — achadosFiltrados() ganhou essa terceira condição, combinando por E lógico com severidade/status. Como não há chip próprio pra esse filtro, uma faixa nova (#dc-regra-filtro-ativo) aparece entre os chips e a lista quando filtroRegra está ativo, com o nome da categoria e um botão "Limpar". Clicar em qualquer um dos dois (donut/card) dá um scroll suave até a lista (pidDcScrollParaLista()). Puramente frontend, nenhuma mudança de backend. Detalhe completo no CLAUDE.md desta pasta.

99. Exportar Balancete/DRE em XLSX a partir do relatório "Gerar Dashboard"

Pedido: permitir exportar Balancete ou DRE em XLSX a partir do relatório HTML. Novo módulo portal_api/dashboard_contabil/exportacao.py (funções puras, openpyxl, mesmo espírito de indicadores.py/regras.py — sem tocar no ORM) com gera_xlsx_balancete()/gera_xlsx_dre(), recebendo dataclasses (LinhaBalanceteXlsx/LinhaDreXlsx) já resolvidas pela view. Nova action ContabilApuracaoViewSet.exportar_xlsx() (GET /api/contabil-apuracoes/{id}/exportar-xlsx/?parte=balancete ou ?parte=dre, mesma permissão de toggle único) monta essas linhas reaproveitando a mesma fórmula de nível/indentação já usada por dashboard().

Cada planilha nasce com cabeçalho (empresa/CNPJ/competência), cabeçalho de colunas com a mesma paleta roxo/dourado dos outros documentos gerados pelo escritório, conta sintética/linha totalizadora em negrito+fundo dourado claro, indentação de hierarquia via Alignment(indent=nivel) e colunas monetárias com number_format brasileiro (valor gravado como número, não texto — continua editável/somável no Excel). Botão "Exportar XLSX" novo em cada seção do relatório (.dcr-export-btn, dashboard-contabil-relatorio.html) é um link direto pra API, sem JS — o browser já baixa o arquivo pelo Content-Disposition da resposta; escondido na impressão junto do botão "Imprimir". openpyxl já era dependência do projeto (usado pra leitura em outras ferramentas), essa é a primeira vez que o projeto escreve um XLSX com ele. Detalhe completo no CLAUDE.md desta pasta.

100. Resumo da rodada 97 reorganizado em 3 cards por grupo temático

Pedido: os 10 cards por regra do resumo (rodada 97), cada um com uma mini-tabela de conta+status por achado, ficaram "muito poluídos visualmente" na prática (captura de tela real anexada pelo usuário). Alinhado por AskUserQuestion (com preview) o agrupamento das 10 regras em 3 temas fixos: "Divergências de Saldo" (balanceamento Ativo x Passivo, débito x crédito, caixa negativo, sinal de saldo invertido), "Contas Atípicas" (contas transitórias, contas que deveriam zerar, descrição genérica) e "Variações e Indicadores" (variação atípica de saldo, variação atípica na DRE, percentual custo/receita).

PID_DC_GRUPOS (nova constante, dashboard-contabil.js) substitui a iteração antes feita direto sobre PID_DC_REGRAS na grade de cards — agora 1 card por grupo (cabeçalho com o total do grupo) contendo uma linha por regra (label + contagem, sem mini-tabela de conta/status). A mini-tabela de conta+status por achado saiu do resumo (era a maior fonte de poluição visual), mas continua disponível na lista completa logo abaixo, ao clicar numa linha de regra — o clique por regra individual (não por grupo) foi preservado, mesmo comportamento de filtro da rodada 98. Puramente frontend (JS + CSS), nenhuma mudança de backend/model. Detalhe completo no CLAUDE.md desta pasta.

101. Aba "Dashboard" na tela de revisão — ocultar cards/observações antes de gerar o relatório

Pedido: depois da aba DRE, uma aba "Dashboard" que mostra os indicadores e observações que vão pro relatório "Gerar Dashboard" antes de gerá-lo, onde o contador pode ocultar um card de indicador ou uma observação — o que estiver oculto não entra no relatório gerado.

Modelos novos: ContabilConta.oculta_no_relatorio/ContabilLinhaDre.oculta_no_relatorio/ContabilAchado.oculto_no_relatorio (booleanos, só afetam a seção "Observações" do relatório, a linha em si continua normal no Balancete/DRE/Observações da revisão) e ContabilApuracao.indicadores_ocultos (lista de chaves de IndicadoresFinanceiros, ex. ["kanitz"]) — migração 0059. Endpoints novos: GET /api/contabil-apuracoes/{id}/indicadores/ (mesmo cálculo do relatório, extraído pro helper _contabil_calcula_indicadores), POST .../indicadores-ocultos/ (substitui a lista inteira) e POST /api/contabil-achados/{id}/alternar-oculto/ (separado do fluxo de tratar/ignorar, que sempre exige status+justificativa); oculta_no_relatorio de conta/linha da DRE já entra pelo PATCH genérico que essas duas telas já tinham. dashboard() filtra as 3 listas de observações por esses campos e envolve cada um dos 11 cards do relatório num {% if "chave" not in indicadores_ocultos %} (mais 2 booleanos de grupo, pra não sobrar um <h2> de seção sem nenhum card embaixo).

Nova aba data-dc-tab="dashboard" em dashboard-contabil.html, carregada sob demanda (renderDashboardTab(), só na primeira vez que é aberta) pra não pagar o custo de recalcular os indicadores em toda apuração aberta. Cada card/observação ganha um botão de olho (ícone SVG inline, reaproveitando .icon-btn) que alterna o oculto e atualiza só o item local, sem recarregar a apuração inteira; desabilitado quando a apuração já está concluída. Detalhe completo no CLAUDE.md desta pasta.

102. Banco de indicadores personalizados — fórmulas, componentes e "Ver fórmula"

Pedido: cada card de indicador devia ser "um indicador efetivo calculado a partir das contas contábeis" — o contador poder ver a fórmula de qualquer card (inclusive os 11 de sistema) e criar indicador novo, escolhendo contas do Balancete/linhas da DRE/variação entre apurações/outros indicadores já existentes como componentes de uma fórmula de verdade. Escopo alinhado por AskUserQuestion antes de implementar: os 11 de sistema continuam com o cálculo Python fixo de sempre (zero risco de mudar um valor já calibrado), só ganharam metadados de exibição; a fórmula personalizada é avaliada por um interpretador restrito (ast, nunca eval()); "selecionar conta ou grupo" é marcar uma ou mais contas específicas via checklist, não digitar um prefixo de código.

Modelos novos IndicadorContabilDefinicao/IndicadorContabilComponente (migração 0060) — componente guardado por código de conta/descrição de linha da DRE (nunca FK a uma linha de uma apuração específica), reaplicado em qualquer apuração/empresa que o relatório for gerado. Novo módulo dashboard_contabil/formula.py (avalia_formula()/valida_formula()) — expressão aritmética (+, -, *, /, parênteses) sobre as chaves dos componentes, com None propagando como "indisponível" (nunca vira 0) e divisão por zero também virando None. Resolução iterativa em views.py (_contabil_calcula_indicadores_personalizados()) cobre encadeamento entre indicadores personalizados e referência a indicador de sistema, sem quebrar em caso de ciclo (fica None) — validado manualmente contra a produção com uma transação revertida (rollback), cobrindo os três casos.

CRUD novo IndicadorContabilDefinicaoViewSet (/api/contabil-indicadores-definicoes/) — mesma permissão de toggle único do resto da ferramenta (qualquer contador com acesso pode criar, não é restrito ao perfil "Inovação"); sempre substitui o indicador inteiro (nome/descrição/fórmula/formato/componentes) a cada criação/edição, nunca um diff incremental; bloqueia exclusão se outro indicador o referencia pela fórmula. GET /contabil-apuracoes/{id}/indicadores/ ganhou metadados (nome/grupo/formato/descrição/fórmula em texto, uniforme pra indicador de sistema ou personalizado) — única fonte de verdade que o frontend usa agora pra agrupar/formatar cada card, substituindo as constantes hardcoded da rodada 101.

Metadados dos 11 de sistema (indicadores.METADADOS_CARDS) vieram do glossário de KPI que o usuário forneceu, com 2 ajustes pra bater com o que o código realmente calcula: Grau de Endividamento/IPL (o texto original tinha um "×100" que já é só a exibição em %, não uma conta a mais) e EBIT (o texto original é a definição-livro-texto "de cima pra baixo", o código calcula "de baixo pra cima" a partir do Lucro Líquido — os dois tendem a convergir mas não foram provados equivalentes).

Frontend: card agora é clicável (fora dos botões) e abre um modal "Ver fórmula"; indicador personalizado ganha um botão de editar (lápis) além do de ocultar. Modal "Novo/Editar Indicador" com um construtor de componentes dinâmico, reaproveitando .checklist-box/.checklist-item/.checklist-search (mesmo padrão dos checklists de Perfis/Departamento) pros pickers de conta/linha da DRE — necessário porque uma apuração real tem ~100-150 contas/linhas. Bug real pego antes do usuário testar: editar um indicador configurado a partir de outra apuração apagaria silenciosamente qualquer código/descrição que não existisse na apuração atualmente aberta (não aparecia no checklist pra continuar marcado); corrigido mostrando esses como item extra "não encontrado nesta apuração, mantido" no topo da lista. Detalhe completo no CLAUDE.md desta pasta.

103. Indicador padrão vs. não padrão + "Gerenciar Indicadores" + modal com scroll

Pedido: (a) o modal "Novo/Editar Indicador" da rodada 102 não tinha scroll — com vários componentes, só dava pra alcançar "Salvar" dando zoom out no navegador; (b) precisava de um jeito de ver/editar qualquer indicador já criado, não só os que já estão aparecendo na apuração aberta; (c) indicador personalizado devia poder ser "padrão" (aparece automaticamente em toda apuração, comportamento que já existia) ou "não padrão" (fica salvo/editável mas só aparece numa apuração quando selecionado ali).

.modal-card (components.css) ganhou max-height/overflow-y:auto — correção global, sem efeito em modal que já cabia na tela; a lista de componentes também rola sozinha (.dc-ind-componentes-lista, 320px), pra não precisar rolar duas vezes. Modelo IndicadorContabilDefinicao.padrao (default True, sem regressão pros já criados) e ContabilApuracao.indicadores_selecionados (migração 0061). _contabil_calcula_indicadores_personalizados() passou a filtrar por padrao=True ou chave selecionada nesta apuração antes de calcular qualquer coisa — um indicador não padrão não selecionado não aparece nem no relatório nem na aba "Dashboard", mas continua existindo/editável. Endpoint novo POST /api/contabil-apuracoes/{id}/indicadores-selecionados/ (mesmo padrão de indicadores_ocultos/).

Frontend: o botão "Novo Indicador" virou "Gerenciar Indicadores", abrindo um modal-hub com a lista completa (padrão e não padrão) — cada linha com nome clicável ("Ver fórmula"), editar, e (só não padrão) um checkbox "ativo nesta apuração". O form de criar/editar agora abre empilhado por cima do hub (.modal-overlay--top, mesmo mecanismo do modal de confirmação genérico) e ganhou um checkbox "Indicador padrão". Detalhe completo no CLAUDE.md desta pasta.

104. Migração dos 11 indicadores de sistema pro banco de indicadores

Pedido: reversão da decisão de escopo da rodada 102 (que tinha deixado os 11 indicadores "de sistema" — ROA/ROE/Kanitz/EBIT/EBITDA/Liquidez Corrente/Liquidez Seca/Liquidez Geral/Composição do Endividamento/Grau de Endividamento/IPL — fora do banco, calculados em Python fixo, pelo risco de mapear uma conta errado). Confirmado por AskUserQuestion (risco explicado antes) que o usuário queria a fórmula de verdade editável, não só o texto de exibição.

Os 11 viraram IndicadorContabilDefinicao de verdade, com as mesmas chaves de antes (preserva indicadores_ocultos já salvo) — não são mais um caso especial em lugar nenhum do código. Novo tipo de componente resultado_liquido (sem nenhum campo de referência, resolve sempre pra última linha da DRE por posição) — necessário porque essa linha muda de rótulo ("LUCRO" vs "PREJUÍZO") conforme o sinal do resultado, confirmado contra balancete real; um componente comum de linha da DRE (que casa por texto exato) quebraria assim que o resultado de uma empresa mudasse de sinal entre apurações. Novo campo IndicadorContabilDefinicao.grupo (não exposto no formulário — só a migração de dados setou os 2 grupos originais pros 11 migrados; indicador novo nasce em "Indicadores Personalizados").

Verificação antes de produção: dry-run revertido (transação com rollback forçado) criou os 11 e comparou o motor novo contra o cálculo Python antigo pra uma apuração real — bateram exatos até a 20ª casa decimal, inclusive o None do EBITDA sem apuração anterior. Só depois disso o script rodou de verdade. Testado em seguida contra a API/relatório reais (incluindo editar um componente resultado_liquido via PATCH).

dashboard_contabil/indicadores.py teve CHAVES_CARDS/METADADOS_CARDS removidos (zero consumidor); calcula_indicadores() e companhia foram mantidos deliberadamente, mesmo sem chamador em produção, como referência/auditoria pra recalcular e comparar se algum valor um dia parecer suspeito — única exceção neste projeto à convenção de apagar código sem uso, justificada pelo risco financeiro. O relatório "Gerar Dashboard" perdeu os 11 .dcr-card hardcoded, substituídos por um {% for %} genérico único (mesmo caminho que já servia indicador personalizado) — perda real e aceita conscientemente: cada um dos 11 tinha um ícone SVG próprio, agora todo indicador usa o mesmo ícone genérico. Detalhe completo no CLAUDE.md desta pasta.

105. Scroll aninhado no construtor de componentes + "Calcular com esta apuração"

Dois pedidos na sequência, testando a migração da rodada 104: (a) captura de tela mostrando que a lista de componentes tinha um scroll (320px) por cima do scroll do checklist de contas (160px) — dois scrolls aninhados deixavam a área minúscula, nem um componente inteiro cabia sem rolar duas vezes; (b) além da fórmula em texto, mostrar os valores exatos que cada componente busca e o resultado calculado, pro contador conferir se a fórmula está certa.

.dc-ind-componentes-lista perdeu o max-height/overflow-y (volta a crescer no fluxo normal do modal, que já rola inteiro); .checklist-box de cada componente aumentou de 160px pra 260px — sobra só um scroll aninhado (o checklist em si, necessário pelas ~100-150 contas/linhas de uma apuração real).

Novo botão "Calcular com esta apuração" no modal de indicador, entre "Fórmula" e as ações — chama POST /api/contabil-apuracoes/{id}/pre-visualizar-indicador/ (ContabilApuracaoViewSet.pre_visualizar_indicador()) com o formulário como está na tela (ainda não salvo), calcula contra a apuração aberta e devolve o valor de cada componente + o resultado final, sem persistir nada. Reaproveita a mesma validação (IndicadorContabilDefinicaoInputSerializer) e o mesmo motor de cálculo (_contabil_resolve_componente_personalizado()/avalia_formula()) de criar/editar de verdade, só que sobre componentes construídos em memória, nunca salvos. Detalhe completo no CLAUDE.md desta pasta.

106. Bug real — exclusão de indicador não padrão travava o hub inteiro

Usuário reportou o modal de aviso "Chave(s) de indicador não padrão inexistente(s): diferença." ao tentar alternar um indicador no hub "Gerenciar Indicadores". Causa: um indicador não padrão de chave diferença tinha sido excluído em algum momento, mas IndicadorContabilDefinicaoViewSet.perform_destroy() só bloqueava/limpava referência pela fórmula (indicador_referenciado), nunca pelas referências em ContabilApuracao.indicadores_selecionados/indicadores_ocultos — a chave ficou órfã na apuração que a tinha selecionada. Como o frontend sempre reenvia a lista inteira a cada alternância de checkbox (pidDcRenderHubIndicadores/evento change em dashboard-contabil.js), e ContabilIndicadoresSelecionadosSerializer/ContabilIndicadoresOcultosSerializer rejeitavam a lista inteira se qualquer chave nela não existisse mais, o usuário ficava travado sem conseguir alternar nenhum indicador na apuração afetada, não só o excluído.

Dois ajustes, um pro sintoma já existente e outro pra causa raiz: (a) as duas validações passaram a descartar silenciosamente chave inexistente/órfã em vez de rejeitar a lista inteira — é só estado de exibição (quais cards aparecem/estão ocultos), não dado auditado, então autocorrigir é seguro e resolve o travamento já em produção sem precisar de um script de correção manual; (b) perform_destroy() agora também limpa a chave excluída de toda ContabilApuracao que a referenciava em indicadores_selecionados/indicadores_ocultos, pra não deixar mais nenhuma referência órfã nova daqui pra frente.

107. Balancete/D.R.E. nascem recolhidos a partir do "grupo 4"

Pedido: a árvore do Balancete (e, por extensão confirmada com o usuário, da D.R.E.) nascia sempre totalmente expandida, poluindo visualmente uma apuração com muitas contas analíticas. Passou a nascer recolhida a partir do nível equivalente ao "grupo 4" do código de classificação (ex. 1.01.01.001, 4 segmentos) — essa conta aparece aberta, mas seus filhos (1.01.01.001.001 em diante) ficam ocultos até o contador clicar pra expandir; o mesmo limiar de nível é aplicado à D.R.E., sobre ContabilLinhaDre.nivel.

Aplicado nos dois lugares que já compartilhavam o mesmo algoritmo de árvore recolhível: a tela de revisão (dashboard-contabil.js, dcColapsoPadrao() pré-popula dcContasColapsadas/dcDreColapsadas em renderRevisao(), em vez de nascerem como Set() vazio) e o relatório "Gerar Dashboard" (_contabil_arvore_contexto() em views.py ganhou o campo colapsado_padrao por item, calculado a partir da nova constante _CONTABIL_NIVEL_ABERTO_PADRAO = 3; dashboard-contabil-relatorio.html usa esse campo pra não marcar is-expanded no botão e pidDcrArvore() semeia o estado colapsadas a partir do atributo data-dcr-colapsado-padrao antes da primeira renderização). A impressão continua forçando toda linha a aparecer (tr[hidden] { display: table-row !important } em @media print), então o comportamento de "documento impresso nunca esconde conta atrás de um grupo recolhido" não muda. Nenhuma mudança de modelo/migração — é só o estado inicial da mesma árvore que já existia.

108. Fundo uniforme na tabela do Balancete (revisão)

Pedido: a linha sintética do Balancete (grupo do plano de contas, ex. "1.02.05 IMOBILIZADO") tinha um fundo elevado (--bg-surface-raised) além do negrito, criando um efeito de cores alternadas entre linha de grupo e linha analítica — o usuário pediu pra unificar, no mesmo padrão já usado na D.R.E. (que só usa negrito na linha totalizadora, sem fundo diferente). Removida a regra .dc-conta-row--sintetica td { background: var(--bg-surface-raised); } em dashboard-contabil.css, mantendo só o negrito (.dc-conta-row--sintetica { font-weight: 600; }) — a tabela do Balancete passa a ter uma única cor de fundo, igual à D.R.E.

109. Limiar de recolhimento ajustado pra nível 3 + destaque acompanha o grupo expandido

Pedido: (a) o limiar de recolhimento padrão da rodada 107 ("grupo 4", ex. 1.01.01.001 aberto) ficou permissivo demais — passou a recolher a partir do 3º segmento do código (ex. 1.01.01 aberto, 1.01.01.001 em diante só sob demanda). PID_DC_NIVEL_ABERTO_PADRAO (dashboard-contabil.js) e _CONTABIL_NIVEL_ABERTO_PADRAO (views.py, usada pelo relatório "Gerar Dashboard") foram de 3 pra 2 — mesma dupla de constantes já documentada na rodada 107, ajustadas juntas.

(b) Novo mecanismo de destaque: ao expandir uma conta/linha, só as linhas que acabaram de ficar visíveis (filhos diretos, não os netos — que continuam recolhidos pelo limiar acima) ganham um realce dourado; expandir uma delas em seguida move o realce pra seus filhos e o grupo destacado antes volta à cor padrão, nunca acumulando mais de uma "leva" ao mesmo tempo — ajuda o contador a acompanhar visualmente onde ele acabou de descer na árvore. dcContasDestaque/dcDreDestaque (dashboard-contabil.js, dois Set() resetados em renderRevisao()) guardam só a leva mais recente; dcFilhosDiretos() (função genérica, reaproveitada pelas duas árvores) calcula os filhos diretos de um item a partir da lista plana. Classe nova .dc-conta-row--destaque (dashboard-contabil.css, fundo rgba(var(--gold-rgb), .16)). Só na tela de revisão — o relatório "Gerar Dashboard" não ganhou esse destaque (é uma navegação estática, sem o mesmo conceito de "acabei de expandir isto agora"). Puramente visual/em memória, sem persistência nem mudança de modelo. Nota de rodada seguinte (110): essa última frase não vale mais — o relatório ganhou o mesmo destaque logo depois.

110. Código interno da empresa nas telas do Dashboard Contábil

Pedido explícito do usuário, com captura de tela do histórico mostrando só o nome da empresa ("GUARANI MUSICAL INSTRUMENTOS MUSICAIS LTDA - EPP") — o Portal sempre referencia empresa pelo código interno (codigo_empresa), mesmo padrão já usado em Importação de Plano de Saúde (${codigo_empresa} - ${nome_empresa}). O backend já expunha codigo_empresa nos dois serializers usados pela tela de revisão (ContabilApuracaoListSerializer/ContabilApuracaoDetailSerializer) — faltava só o frontend exibir. Dois pontos ajustados em dashboard-contabil.js: coluna "Empresa" do histórico (linha da tabela) e o cabeçalho da tela de Revisão, ambos passaram de nome_empresa sozinho pra codigo_empresa - nome_empresa. Não alterado de propósito: o relatório "Gerar Dashboard" (dashboard-contabil-relatorio.html, título/<h1>) e a planilha XLSX exportada continuam só com o nome — são documentos entregues ao cliente com a identidade do escritório, não telas internas do Portal, então o código de referência interno provavelmente não faz sentido lá (a confirmar com o usuário se for pedido depois).

111. Cor do destaque de expansão ajustada por tema + destaque restaura ao recolher

Duas rodadas de ajuste fino sobre o destaque da rodada 109 (.dc-conta-row--destaque), só na tela de revisão:

(a) A cor dourada fixa (rgba(var(--gold-rgb), .16)) destoava do tema escuro e ficava quase invisível no claro. Duas variáveis novas em dashboard-contabil.css (--dc-destaque-bg/--dc-row-tint-bg, redefinidas em :root[data-theme="light"]) parametrizam a cor por tema: no escuro, o destaque usa --card-bg-hover (mesmo tom neutro do hover das linhas — tentativa anterior com rgba(var(--accent-rgb), .18) foi descartada por pedido do usuário, achou o hover mais discreto/agradável); no claro, o efeito é invertido — as linhas fora do destaque ganham um tingimento roxo sutil (rgba(var(--accent-rgb), .08)) e a leva recém-revelada fica em branco puro (--bg-surface), lida como "em foco" em vez de tingida. Bug real encontrado no meio do caminho: a regra genérica de tingimento (.dc-contas-table tbody tr td, 3 seletores de tipo) tinha especificidade CSS maior que a regra de destaque original (.dc-conta-row--destaque td, 1 seletor de tipo) e sempre vencia, apagando o destaque por completo — corrigido aumentando a especificidade da regra de destaque (duas classes: .dc-contas-table .dc-conta-row--destaque td).

(b) Ao recolher uma leva destacada, o destaque devia voltar pra leva anterior (a que estava destacada antes de expandir), não simplesmente desaparecer. dcContasDestaqueHistorico/dcDreDestaqueHistorico (dois arrays, um por árvore) empilham o destaque atual a cada expandir e desempilham a cada recolher — mesmo espírito de undo por nível, sem tentar rastrear relação de parentesco entre expansões não relacionadas.

112. Destaque replicado no relatório "Gerar Dashboard" (revertendo a nota da rodada 109)

Pedido explícito do usuário: aplicar as mesmas mudanças de cor/comportamento da rodada 111 também no relatório gerado pro cliente (dashboard-contabil-relatorio.html), que até aqui não tinha esse destaque nenhum. Só a variante clara existe aqui (o relatório não tem alternância de tema).

Implementado com uma custom property por linha (--dcr-row-bg, lida por um único seletor .dcr-tabela tbody tr td { background: var(--dcr-row-bg, transparent); }, escrita por três regras de mesma especificidade — tingimento padrão/.dcr-linha-total/.dcr-destaque, nessa ordem, a última declarada vence o empate) em vez de competir por especificidade CSS entre elas — mesmo bug de especificidade da rodada 111 apareceu aqui de um jeito mais sério: como a maioria das linhas reveladas ao expandir um grupo real também são grupos (tipo="S", com filhos mais fundo ainda; confirmado consultando o banco: "BENS NUMERÁRIOS" é tipo="S" de verdade), excluir .dcr-linha-total do destaque (tentativa inicial, calcada na tela de revisão) fazia o efeito nunca aparecer na prática — o destaque final não exclui linhas de grupo/total (só o tingimento padrão de repouso as exclui, pra manter o dourado fixo delas fora do hover/destaque). pidDcrArvore() ganhou a mesma dupla empilha/desempilha da rodada 111 (historico/destaque locais à função, chamada uma vez por tabela).

Ajuste final, mesmo pedido de "ficar mais bonito" que abriu a rodada: o Balancete nascia com a tela inteira na cor de grupo/total (todo o plano de contas real é tipo="S" até um nível bem profundo) enquanto a D.R.E. já nascia com essa aparência "gostosa" (poucas linhas são totalizador). destaque agora nasce pré-populado com as linhas marcadas colapsado_padrao no servidor (o último nível já visível por padrão, ver rodada 109) — nasce com o mesmo efeito de "acabei de expandir até aqui", sem precisar de nenhum clique.

113. Ícone selecionável por indicador + flip card no relatório

Pedido explícito do usuário, escopado por AskUserQuestion só pro relatório "Gerar Dashboard" (a aba "Dashboard" da revisão não ganhou flip/ícone, pra não competir com os botões de olho/lápis que já existem em cada card ali). Reverte a "perda aceita conscientemente" da rodada 104 (ícone único genérico pra todo indicador). Detalhe completo no CLAUDE.md desta pasta ("Ícone selecionável + flip card no relatório"): campo novo IndicadorContabilDefinicao.icone (migração 0063, default "barras" — nenhum indicador já cadastrado muda de aparência sem edição manual), 11 ícones curados tipo KPI, seletor visual no modal "Novo/Editar Indicador", e o card do relatório virou um flip 3D (hover revela descrição + fórmula cadastradas em "Gerenciar Indicadores"). O desenho de cada ícone existe em duas cópias mantidas manualmente em sincronia (Python em views.py, JS em dashboard-contabil.js) — o relatório é HTML puro sem acesso ao JS do app, e o modal de cadastro é só JS estático sem contexto de servidor.

114. Fórmula do verso separada da fórmula de cálculo + esclarecimento sobre o relatório ser uma foto estática

Dois pontos levantados pelo usuário testando a rodada 113: (a) o card de EBIT mostrava "Sem descrição cadastrada" mesmo já tendo descrição salva — não era bug, era o relatório sendo uma foto estática do momento em que foi gerado (editar o indicador depois não atualiza um relatório já aberto/baixado, precisa gerar de novo); (b) a fórmula do verso mostrava a expressão técnica de cálculo (resultado_liquido - despesas_financeiras, chaves internas dos componentes), inadequada pro cliente final.

Campo novo IndicadorContabilDefinicao.formula_exibicao (CharField, blank=True, migração 0064) — texto livre, sem validação de sintaxe (não passa por avalia_formula()), editável no modal de indicador logo abaixo do campo técnico (que ganhou o rótulo "Fórmula (cálculo interno)", pra diferenciar do novo "Fórmula (como aparece ao cliente)"). _contabil_monta_cards_indicadores() resolve o fallback no servidor (formula_exibicao preenchida vence; em branco cai pra formula mesmo) — nenhum indicador já cadastrado muda de comportamento até alguém preencher essa preferência pela tela. Detalhe completo no CLAUDE.md desta pasta.

115. Ajustes de acabamento do flip card + destaque inicial também na tela de revisão

Três pedidos na sequência, testando as rodadas 113/114: (a) a fonte do valor na frente do card (R$ -143.648,54) quebrava linha — .dcr-card__value de 1.55rem pra 1.3rem; (b) o verso do flip card tinha um scroll aninhado (a descrição rolava sozinha, separada da fórmula abaixo) — removido flex:1/overflow-y:auto de .dcr-card-face__descricao, agora descrição e fórmula fluem juntas num único bloco contínuo (a face inteira já rolava, .dcr-card-face--back); (c) pedido pra levar o mesmo destaque inicial do relatório (rodada 112) também pra tela de revisão do Balancete/D.R.E, reduzindo a poluição visual de abrir a tela inteira na cor de grupo/total.

renderRevisao() (dashboard-contabil.js) agora inicializa dcContasDestaque/dcDreDestaque como cópia de dcContasColapsadas/dcDreColapsadas (new Set(dcContasColapsadas)) em vez de new Set() vazio — mesma lógica já usada no relatório: as linhas colapsadas por padrão são o último nível já visível de cada ramo, então já nascem destacadas sem precisar de clique nenhum. dcContasDestaqueHistorico/dcDreDestaqueHistorico continuam nascendo vazios (o destaque inicial não empilha nada — só a partir da primeira expansão manual).

116. Botão de observação virou sempre ícone + cor do destaque invertida no tema escuro

Dois pedidos testando a rodada 115: (a) mostrar o texto da observação já preenchida direto na tabela também poluía a coluna (o "+ Observação" das linhas vazias já tinha virado ícone antes, mas a linha com "Cliente não envia o estoque." continuava em texto) — agora o botão é sempre o mesmo ícone, só muda de cor (.dc-conta-observacao-btn--preenchida, --text-muted vira --accent) conforme há observação ou não; o texto continua acessível por tooltip/clique, só sumiu da célula. (b) A cor do destaque no tema escuro estava invertida do que o usuário queria — trocado --dc-destaque-bg/--dc-row-tint-bg de lugar: destaque agora usa --bg-canvas (mais escuro) e o resto --bg-surface-raised (mais claro, mas ainda diferente do hover). O tema claro não mudou (já tinha sido invertido por pedido anterior, contrariamente ao escuro). Detalhe completo no CLAUDE.md desta pasta.

117. Bug real — folha genuína ficava de fora do destaque inicial (D.R.E.)

Usuário reportou, testando a rodada 116, que várias linhas-folha da D.R.E. ((-) DE VENDAS DE MERCADORIAS MERCADO INTERNO, (-) SIMPLES NACIONAL, DESCONTOS OBTIDOS e outras linhas de resultado financeiro) não ficavam com o destaque escuro esperado, mesmo sendo visualmente "de baixo" quanto um grupo colapsado vizinho no mesmo nível. Causa: o destaque inicial (rodada 112/115) usava só o conjunto de linhas colapsadas por padrão — que só marca linha com filho escondido —, deixando de fora qualquer folha genuína (sem filho nenhum), que nunca entra nesse conjunto mas é igualmente "o fim do ramo" visualmente.

Corrigido com um critério novo, mesmo algoritmo nas duas cópias (dcUltimaLevaVisivel() em dashboard-contabil.js, calculaDestaqueInicial() em dashboard-contabil-relatorio.html): reconstrói a lista de linhas realmente visíveis e marca destaque em toda linha cuja próxima linha visível não seja mais profunda — cobre grupo colapsado e folha genuína com a mesma regra. Validado manualmente contra os níveis reais da apuração id=4 antes de aplicar. Detalhe completo no CLAUDE.md desta pasta.

118. Acabamento do flip card (texto centralizado, fonte) + confirmação ao fechar o modal de indicador sem salvar

Dois pedidos independentes: (a) o verso do flip card, com uma captura de tela do equivalente que o usuário já usa no Power BI como referência — texto centralizado (text-align:center em .dcr-card-face--back), fontes um pouco menores, e a fórmula ganhou "JetBrains Mono" em vez de só "Courier New", monospace; (b) o modal "Novo/Editar Indicador" fechava direto ao clicar fora (overlay), sem perguntar nada — pidDcFecharIndicadorModalComConfirmacao() (nova) pergunta "Sair sem salvar as alterações?" (pidConfirm) antes de fechar de verdade, ligada ao overlay e ao "Cancelar"; fechar depois de salvar/excluir com sucesso continua direto, sem pergunta. Detalhe completo no CLAUDE.md desta pasta.

119. Resumo do Fechamento — texto rico do contador antes dos indicadores

Pedido explícito do usuário, com um exemplo real de carta que a De Paula já manda ao cliente hoje (considerações/saldos/variações do fechamento) como referência do que deveria caber aqui. A aba "Indicadores" do relatório "Gerar Dashboard" virou "Resumo" (mesmo nome também na aba "Dashboard" da revisão) — passa a carregar duas coisas: o texto livre do contador ("Resumo do Fechamento", no topo) e, embaixo, os grupos de cards de indicador de sempre.

Campo novo ContabilApuracao.resumo_fechamento (TextField, blank=True, migração 0065) — texto rico (HTML sanitizado por nh3, mesma allowlist de AcessoGeral.observacoes/AjudaAplicacao.texto), preenchido via editor contenteditable com colar/arrastar imagem (mesmo padrão dos outros dois campos ricos do projeto, duplicado de propósito) na aba "Dashboard" da revisão. Nova @action POST /api/contabil-apuracoes/{id}/resumo-fechamento/ (ContabilApuracaoViewSet não tem PATCH genérico, de propósito) substitui o campo inteiro de uma vez, validado por ContabilResumoFechamentoSerializer. No relatório, {{ apuracao.resumo_fechamento|safe }} — primeiro |safe de template Django do projeto (até agora todo texto rico só existia via innerHTML em tela SPA), seguro porque já vem sanitizado antes de salvar; seção some inteira quando o campo está vazio. Testado de ponta a ponta via shell, inclusive confirmando que um <script> embutido é removido pelo nh3. Detalhe completo no CLAUDE.md desta pasta.

120. Rebatizado pra "Relatório Contábil" + validação por conta/linha + observação virou editor inline (sem popup) + resumo por aba

Pedido explícito do usuário: deixar a ferramenta soar como um aliado do trabalho do contador, não mais um processo novo. Quatro mudanças na mesma rodada:

  1. Rename só de rótulo visível — "Dashboard Contábil" virou "Relatório Contábil" no menu (catalogo.py), <title>/<h1>/cabeçalhos de dashboard-contabil.html e do relatório, e o botão "Gerar Dashboard" virou "Gerar Relatório". Nada técnico mudou (pasta, arquivos, classes, rotas continuam com o nome antigo) — detalhe completo no CLAUDE.md desta pasta.
  2. Checkbox "validado" — campo novo validado (BooleanField, default False, migração 0066) em ContabilConta/ContabilLinhaDre; um segundo botão (ícone de check, cor --teal) ao lado do de observação em cada linha do Balancete/DRE, puramente informativo (não afeta achado/observação/relatório), pro contador marcar que já conferiu aquela conta durante a revisão.
  3. Observação virou editor inline — o popup único #dc-observacao-modal foi removido; clicar no ícone de observação abre uma linha extra dentro da própria tabela (textarea + checkbox "Mostrar esta observação ao cliente no relatório" + Cancelar/Salvar), evitando perder o texto por causa de um clique acidental fora de um popup. Esse checkbox controla o já existente oculta_no_relatorio, cujo default também mudou de False pra True (migração 0066) — uma observação nova só vai pro relatório do cliente depois que o contador confirmar isso explicitamente, nunca por padrão.
  4. Resumo por aba — Balancete e DRE ganharam cada um sua própria seção de resumo (quantidade + lista de observações) no final da tabela, além da lista combinada que já existia na aba "Dashboard" (mantida, sem mudança — servem propósitos diferentes).

Detalhe completo no CLAUDE.md desta pasta.

121. Correções de acabamento da rodada 120 + observação também no relatório do cliente

Três pedidos testando a rodada 120: (a) o checkbox "Mostrar ao cliente" do editor inline nascia marcado mesmo numa conta sem observação nenhuma (arrastava o oculta_no_relatorio=False antigo de contas criadas antes do default mudar) — corrigido pra só refletir o valor salvo quando já existe observação (conta.observacao && !conta.oculta_no_relatorio), senão nasce sempre desmarcada; (b) a caixa de texto do editor tinha max-width:640px, cortando antes do fim da linha — removido, agora estica a linha inteira; (c) um bug real de renderização — a célula dos 2 botões (validado + observação) tinha display:flex aplicado direto no <td>, o que tira a célula do comportamento normal de tabela (fundo/altura não acompanhavam mais a linha, dando a impressão de "quebrar antes dos botões") — corrigido movendo o flex pra uma <div> dentro do <td>. De quebra, também corrigido um bug real no destaque: linhas de resultado consecutivas no nível raiz da DRE (RESULTADO ANTES DA CS E IR etc.) estavam todas ficando escuras só por serem vizinhas no mesmo nível — dcUltimaLevaVisivel()/calculaDestaqueInicial() agora só aplicam destaque no nível raiz quando o item de fato esconde algo (grupo colapsado), validado contra os dados reais da apuração.

Pedido separado na mesma rodada: o relatório "Gerar Dashboard" ganhou o mesmo ícone de observação da tela de revisão — coluna "Observação" nova no Balancete/DRE do relatório, ícone só quando há observação marcada como visível ao cliente, clique abre um painel inline abaixo da linha (pidDcrObs(), nova função em dashboard-contabil-relatorio.html). Detalhe completo no CLAUDE.md desta pasta.

122. Botão "validado" de uma conta/linha sintética virou tri-state (nenhum/parcial/completo)

Pedido explícito do usuário: numa conta/linha com filhos (sintética), marcar o próprio check não deveria mais só alternar True/False — passou a ter 3 estados calculados a partir dos descendentes (dcEstadoValidacaoGrupo(), nova): amarelo enquanto nem todos os descendentes estiverem validados (mesmo já tendo marcado a própria sintética), verde quando 100% dos descendentes estiverem validados. Ciclo de clique implementado em dcClicarValidadoConta()/dcClicarValidadoLinha() (novas): 1º clique marca só a sintética (fica amarela); clicar de novo (já amarela) pergunta "Deseja validar todas as contas deste grupo?" e, confirmado, valida tudo em lote (fica verde); clicar de novo (já verde) desmarca o grupo inteiro sem perguntar. Folhas (sem filhos) continuam com o toggle simples de antes. Detalhe completo no CLAUDE.md desta pasta.

123. Demonstração Mensal (Análise Vertical) — nova aba na revisão e no relatório

Pedido explícito do usuário: a seção "Demonstração Mensal (Análise Vertical)" do mesmo PDF (páginas finais, histórico de 3 meses com valor+variação percentual por linha, mesma árvore da DRE) — até então ignorada de propósito pelo parser — passou a ser extraída, persistida e exibida. Escopo confirmado por AskUserQuestion antes de implementar: aba própria (não embutida na aba D.R.E.) e mesmos recursos por linha que Balancete/D.R.E. (observação inline, tri-state "validado", ocultar do relatório).

Extração validada rodando de fato contra o PDF de referência do usuário (792 - balancete 072026.pdf) antes de escrever o parser definitivo — mesmo cuidado de sempre (nunca desenhar regex só de texto colado). Model novo ContabilLinhaAnaliseVertical (mesma árvore/descrição/nível da DRE, valores como JSONField de texto — um {valor, percentual} por mês, alinhado por posição com ContabilApuracao.analise_vertical_meses) + ContabilLinhaAnaliseVerticalViewSet (réplica de ContabilLinhaDreViewSet). Nova aba na tela de revisão (renderAnaliseVertical(), réplica de renderDre() com N colunas dinâmicas de Valor/Variação) e no relatório "Gerar Dashboard" (4ª aba, entre D.R.E. e Resumo) — as duas somem por completo quando a apuração não tem essa seção (relatório antigo). Dois filtros de template novos (moeda_av/percentual_av) porque o percentual desta seção já vem "pronto" do PDF (não é uma fração como os indicadores). Testado ponta a ponta via Client.force_login() contra o PDF real (extração, persistência, PATCH de observação/validado, relatório gerado), sem deixar resíduo em produção. Detalhe completo no CLAUDE.md desta pasta.

124. Botão "Reprocessar" — anexar um PDF novo pra mesma empresa/competência sem perder observação/validação/achado

Pedido explícito do usuário: até aqui, corrigir uma apuração com o arquivo errado/incompleto exigia excluir e recriar do zero, perdendo toda observação/validação/achado já tratado. Botão novo (ícone ao lado de "Abrir" na lista, só em apuração "Em revisão") abre um modal com um campo de arquivo; o PDF precisa ser da mesma empresa/competência (senão 400 — trocar de empresa é uma análise nova). Escopo confirmado com o usuário: conta/linha sem mudança mantém observação/validado como estavam; conta/linha que mudou volta pra validado=False e ganha um alerta visual (campo novo alterada_reprocessamento, migração 0068, nos 3 models de linha); achados nunca são apagados nem têm status/justificativa sobrescritos (confirmado via AskUserQuestion), mesmo os que não disparam mais com os dados novos — mantém o histórico de tratativa completo.

Decisão de design central: as 4 funções novas de sincronização (_contabil_sincroniza_contas/_linhas_dre/_linhas_analise_vertical/_achados, views.py) atualizam registros no lugar (mesmo id), em vez de apagar tudo e recriar como create() faz — crucial pra achado nenhum perder a FK pra sua conta. Casamento por codigo (Balancete) ou (descricao, nivel) (DRE/Análise Vertical, mesma convenção de chave natural já usada pelo histórico de variação em regras.py) pras contas/linhas; achados casados por (regra, código da conta). Testado com os 3 casos reais (mesmo arquivo duas vezes — nada muda; arquivo de outra empresa — 400; apuração Concluída — bloqueado) e com um dry-run isolado das 4 funções de sincronização (conta/linha inalterada, alterada, nova, removida + achado que continua e que para de disparar), sem deixar resíduo em produção. Detalhe completo no CLAUDE.md desta pasta.

Dois ajustes de UX pedidos pelo usuário depois de ver o badge funcionando: (1) marcar a conta/linha como validada de novo não limpa mais o alerta — ele só muda de cor (vermelho → verde), pra dar pra ver depois quais itens já foram reprocessados E revalidados; (2) o tooltip do badge agora mostra o valor de antes do reprocessamento (valor_anterior_reprocessamento/valores_anterior_reprocessamento, migração 0069, gravado por _contabil_sincroniza_* sempre que a conta/linha muda, null/limpo quando não muda). Ambos os campos novos são read_only no serializer.

125. PDF de fonte atípica — título de seção sem acento + aviso ao contador

Cliente novo (1751 - Balancete 07.2026.pdf) deu 400 "Nenhuma linha de DRE encontrada" ao processar. Causa raiz confirmada rodando pdfplumber de verdade contra o arquivo: a fonte embutida nesse PDF perde o til do "Ã" ao extrair "DEMONSTRAÇÃO DO RESULTADO DO EXERCÍCIO" (sai "DEMONSTRAÇAO..."), e parser.py comparava esse título por igualdade exata — a seção DRE nunca era reconhecida. Corrigido com _normaliza_titulo() (remove acento antes de comparar), mesmo espírito de _RE_PERIODO já aceitar Per[ií]odo pra essa mesma classe de variação de fonte entre clientes/instalações do Questor.

O mesmo PDF também tem algumas descrições de conta com palavras coladas (ex. "BANCÁRIOSA VISTA") — investigado a fundo (medição real dos vãos entre caracteres, tentativa de usar os espaços literais do PDF como sinal), mas não há correção automática segura: o espaçamento dessa fonte é inconsistente a ponto de um vão "dentro de palavra" às vezes ser maior que um vão real "entre palavras". Cheguei a propor um botão de lápis pra edição manual da descrição, mas o usuário suspendeu essa ideia e pediu algo mais simples: ContabilApuracao.fonte_pdf_atipica (migração 0070) fica True quando a normalização de acento foi realmente necessária pra reconhecer a seção — sinal indireto de que este PDF usa fonte diferente da de referência, calculado em extrai_balancete_dre()/persistido por create()/reprocessar(). Frontend mostra um ícone de aviso (cor --gold) ao lado do nome da empresa, na lista e no cabeçalho da revisão, avisando pra conferir os nomes de conta com atenção — puramente informativo. Validado True só no PDF com o problema, False nos dois PDFs de referência já confirmados corretos (792, 2017). Detalhe completo no CLAUDE.md desta pasta.

126. Observação virou histórico por empresa+conta, atravessando competências

Pedido explícito do usuário: uma observação registrada num mês (ex. um ajuste de estoque) precisava reaparecer na análise do mês seguinte, assinada por quem escreveu e com a data, bloqueada pra edição por ser registro histórico, com três caminhos pro contador (manter o histórico, que é o padrão; ocultar das próximas execuções; incluir uma observação nova) e filtro de visibilidade ao cliente em todas elas. Antes disso a observação era um campo da linha da apuração, então morria junto com a competência.

Estrutura alinhada com o usuário antes de implementar (três decisões confirmadas por AskUserQuestion): toda observação propaga por padrão (encerrar é uma ação explícita, não existe "fixar"); o histórico cobre Balancete/D.R.E./Análise Vertical, e a justificativa de tratativa dos itens de auditoria continua presa à apuração como já era; e o "mostrar ao cliente" continua alternável mesmo numa observação já travada, porque é decisão editorial de cada relatório, não parte do registro histórico.

Model novo ContabilObservacao (migração 0071, que também copia as observações já existentes e remove os campos observacao/oculta_no_relatorio dos três models de linha): escopo codigo_empresa + chave natural do alvo (codigo no Balancete, descricao|nivel na DRE/Análise Vertical, as mesmas chaves do reprocessamento), competencia_origem/encerrada_em_competencia definindo a vigência, criado_por/criado_em como assinatura visível e mostrar_ao_cliente no lugar do antigo oculta_no_relatorio. ContabilObservacaoViewSet (POST/PATCH/DELETE + encerrar/reativar) e GET /api/contabil-apuracoes/{id}/observacoes/ para o recorte de vigência. Como a observação não mora mais na linha, reprocessar uma apuração deixou de ter qualquer risco sobre ela.

Na tela, o editor inline virou uma thread: o histórico da conta em cima (cada item com autor, data, competência de origem, selos de "Histórico"/"Encerrada"/"Aparece ao cliente" e ações de olho, editar, excluir e encerrar) e o campo de observação nova embaixo; o botão da coluna "Observação" ganhou um contador. Os resumos por aba e a lista consolidada da aba "Dashboard" passaram a listar observações vigentes com assinatura, e ganharam os chips "Todas / Visíveis ao cliente / Internas". No relatório "Gerar Dashboard", cada observação visível aparece assinada e, quando vem de um mês anterior, com a competência em que foi registrada.

Testado ponta a ponta via Client.force_login() dentro de uma transação com rollback (listagem por vigência, criação com chave derivada no servidor, alvo de outra apuração recusado, edição, bloqueio do texto quando a origem está concluída, visibilidade ainda alternável nesse caso, encerrar/reativar com as fronteiras de competência, herança numa competência seguinte e o relatório nos dois meses) — nada gravado em produção além da migração em si. Detalhe completo no CLAUDE.md desta pasta.

127. Botão de excluir observação removido da thread

Pedido explícito do usuário, ainda na mesma funcionalidade da rodada 126: a exclusão de uma observação pela tela ficou limitada ao botão de "ocultar das próximas competências" (encerrar) — o botão de lixeira ao lado de cada item da thread (dcObsItemHtml()) e seu handler (dcTrataCliqueObservacao()) foram removidos, junto da função pidExcluirObservacaoContabil() que ficou sem consumidor. O DELETE /api/contabil-observacoes/{id}/ do backend não foi alterado (continua sujeito à mesma regra de imutabilidade de sempre), só deixou de ser chamado pelo frontend.

128. Selo "Editada" + histórico de edições de uma observação

Pedido explícito do usuário, complementando a rodada 127: sem a opção de excluir, uma edição de texto por cima do que já estava escrito virou uma forma indireta de "apagar" uma observação importante sem deixar rastro — "evitamos a ocultação de observações importantes". Model novo ContabilObservacaoEdicao (migração 0072, FK CASCADE pra ContabilObservacao) grava texto_anterior/texto_novo/editado_por/editado_em toda vez que um PATCH muda o texto de fato (ContabilObservacaoViewSet.partial_update(), comparando antes de salvar — reenviar o mesmo texto ou só alternar mostrar_ao_cliente/encerrar/reativar não geram registro). ContabilObservacaoSerializer ganhou editada (booleano) e edicoes (histórico completo, aninhado) — só usados pela tela, o relatório HTML pro cliente nunca os expõe.

Na tela, cada observação editada ganha o selo "Editada" ao lado dos demais ("Histórico"/"Encerrada"/"Aparece ao cliente"/"Interna") e um botão de relógio que abre um modal listando texto anterior (riscado) → texto novo, autor e data de cada edição, mais recente primeiro — usa o edicoes já carregado junto da observação, sem chamada de API própria. Validado ponta a ponta com Client.force_login() em transação com rollback forçado (detalhe completo no CLAUDE.md desta pasta).

129. Motor de regras de auditoria: 2 removidas, 1 nova, 2 ajustadas

Pedido explícito do usuário, revisitando as 10 regras da rodada 92: removeu tolerância de centavos de balanceamento_ativo_passivo/debito_credito_divergente (agora exigem diferença exatamente zero); adicionou lucro_balancete_diverge_dre (alta) — resultado do exercício precisa ser o mesmo valor no Balancete (conta "2.04.13.002", código fixo calibrado contra os 2 balancetes reais já em produção) e na DRE; ajustou conta_transitoria_com_saldo pra não confundir "dinheiro em trânsito" (NUMERÁRIOS EM TRANSITO) com conta transitória de verdade; e reescreveu variacao_atipica_dre pra usar a seção "Demonstração Mensal (Análise Vertical)" do próprio PDF em vez do histórico de apurações anteriores do Portal — passou a rodar mesmo na 1ª apuração de uma empresa, desde que o PDF traga essa seção. variacao_atipica_saldo (Balancete) e percentual_custo_receita_atipico foram removidas: a Análise Vertical não cobre contas do Balancete, e a granularidade maior da nova variacao_atipica_dre cobriu em espírito a segunda. Motor passou de 10 pra 9 regras.

Duas descobertas testando contra os 2 balancetes reais já em produção antes de decidir a implementação (AskUserQuestion em ambos os casos, não implementado de cabeça): (1) o código de classificação certo pro "lucro do balancete" é o da linha sintética "LUCROS/PREJUÍZOS DO EXERCÍCIO" (2.04.13.002, que agrega "LUCROS DO EXERCÍCIO" ou "(-) PREJUÍZOS DO EXERCÍCIO" conforme o resultado do mês), negada de sinal (convenção Passivo/PL) — bateu exato nos 2 arquivos antes de virar código; (2) a ideia original de trocar o trecho buscado de "TRANSIT" pra "TRANSITOR" (pra excluir "trânsito") quebraria a regra de verdade num PDF real (1751): a fonte desse cliente corrompe o acento de "TRANSITÓRIA" num caractere ilegível na extração, então "TRANSITOR" nunca bateria com uma conta transitória real que tem saldo (R$ 4.141,25) — corrigido excluindo só a palavra isolada "TRANSITO" (\bTRANSITO\b) do match de "TRANSIT", em vez de trocar o critério positivo inteiro.

Também confirmado testando os 2 arquivos reais: a Análise Vertical do PDF já vem com o valor isolado por mês (não acumulado desde janeiro como a DRE principal) e o percentual é a análise vertical de verdade (% da linha sobre a Receita Bruta daquele mês, não uma variação mês a mês) — a nova regra compara esse percentual entre os 2 meses mais recentes da própria tabela. Frontend (dashboard-contabil.js): PID_DC_REGRAS/PID_DC_GRUPOS atualizados pras 9 chaves atuais, "Lucro Balancete x DRE" entrou no grupo "Divergências de Saldo", "Variações e Indicadores" ficou só com "Variação Atípica na DRE". Detalhe completo (incluindo por que historico/_contabil_monta_historico continuam existindo mesmo sem regra nenhuma os usando hoje) no CLAUDE.md desta pasta.

130. Relatório do cliente: observações acima da tabela + clique leva até a conta

Pedido explícito do usuário: no relatório "Gerar Dashboard" (o HTML enviado ao cliente), a seção "Observações do Balancete/D.R.E./Análise Vertical" vinha depois da tabela em cada aba — passou pra antes, nas três abas. Além disso, clicar numa observação agora rola até a conta/linha que ela referencia, "se houver" — uma observação histórica cuja conta saiu do plano de contas desta apuração continua aparecendo normalmente, só sem virar link.

ContabilApuracaoViewSet.dashboard() calcula, pra cada observação visível, uma ancora (atributo Python setado direto no objeto, não um campo do model) igual ao data-dcr-id já usado pela linha correspondente na árvore (conta-{id}/linha-{id}/av-{id}), casando pela mesma chave natural que já indexa as observações por conta/linha — None quando a conta/linha não existe mais nesta apuração. No template, pidDcrArvore() passou a devolver { expandeAte } (reabre só a cadeia de ancestrais colapsados até uma linha, sem mexer no resto da árvore) e uma função nova, pidDcrObsResumo(), liga o clique/teclado num item de observação a isso + scrollIntoView + um flash de destaque de 1,2s na linha. Validado ponta a ponta (Client.force_login(), rollback forçado): observação com conta real ganhou a âncora certa, observação órfã não ganhou âncora nem virou clicável, e as duas seções aparecem antes da tabela no HTML gerado. Detalhe completo no CLAUDE.md desta pasta.

Bug reportado pelo usuário testando em produção, mesma rodada: "clicar nas observações ainda não funciona". Validei de novo com um Chromium headless (Playwright) contra o relatório real gerado da apuração da própria empresa do print (dados reais, sem alterar nada) e o clique funcionou perfeitamente — o que apontou pra fora do código. Causa real: a mudança que faltava (cálculo da ancora) mora em views.py, e o runserver do usuário ainda não tinha reiniciado pra carregar essa versão — a reordenação de HTML (só template, recarregado a cada request) já aparecia, mascarando que só a parte dependente do .py estava desatualizada. Reiniciar o runserver resolveu. Lição registrada na memória do projeto pra rodadas futuras.

131. Relatório do cliente: "Limpar formatação" por tabela + "Voltar ao topo"

Pedido explícito do usuário, complementando a rodada 130: um botão "Limpar formatação" em cada tabela (Balancete/D.R.E./Análise Vertical, no cabeçalho da seção) desfaz qualquer expandir/recolher/destaque que o clique numa observação (ou o próprio usuário) tenha deixado, voltando pro estado inicial de quando a página carregou — sem precisar recarregar o relatório inteiro. E um botão flutuante "Voltar ao topo" (canto inferior direito, aparece só depois de rolar ~320px) rola a página de volta ao início.

pidDcrArvore() ganhou resetar() (devolve colapsadas/destaque/historico pro estado de data-dcr-colapsado-padrao e limpa qualquer .dcr-row-flash restante) e pidDcrObs() ganhou fecharTudo() (fecha todo painel de observação inline aberto) — os dois retornados junto de expandeAte/nada, respectivamente. O botão de cada tabela (data-dcr-reset="dcr-{balancete,dre,av}-body") chama os dois de uma vez, só na própria tabela (não afeta as outras abas). "Voltar ao topo" é puramente window.scrollY/scrollTo, sem nenhuma dependência de dado. Os dois botões somem em @media print, mesmo padrão de "Imprimir"/"Exportar XLSX". Detalhe completo no CLAUDE.md desta pasta.

132. Relatório do cliente: exportar Resumo em PDF avulso + gráfico de evolução removido

Dois pedidos explícitos do usuário na mesma rodada. (1) Removido o gráfico "Evolução do Resultado Líquido" da aba Resumo — "temos a análise vertical para esta visualização", a aba Análise Vertical já cobre esse tipo de comparação mês a mês. Saiu o <script> do Chart.js (CDN), o <canvas>, inicializaGrafico(), o {{ evolucao|json_script }} e o loop que montava essa lista em dashboard() (views.py) — _contabil_monta_historico_completo() continua existindo, só perdeu esse consumidor (o cálculo de Depreciação/Amortização do EBITDA continua usando).

(2) Novo botão "Exportar PDF" na aba Resumo (ContabilApuracaoViewSet.resumo_pdf(), GET /api/contabil-apuracoes/{id}/resumo-pdf/) gera um PDF avulso com Resumo do Fechamento (texto rico convertido em flowables do reportlab, incluindo listas e imagens embutidas) + Indicadores (uma tabela por grupo) + Observações da Análise (mesmo conteúdo/agrupamento de "Todas as Observações da Análise") — sem o resto do relatório. Identidade visual do escritório (banner roxo/dourado com logo-branco.png), mesmo padrão de custo_contratacao/pdf.py/indicadores/recibo.py. _contabil_dados_resumo() (nova função em views.py) foi extraída de dentro de dashboard() pra ser reaproveitada pelos dois caminhos (indicadores + observações visíveis). Novo módulo portal_api/dashboard_contabil/resumo_pdf.py — HTML→reportlab via BeautifulSoup (já era dependência transitiva do projeto, nenhuma adicionada), cobrindo só o allowlist fechado de tags que o editor do Resumo já permite (RICHTEXT_ALLOWED_TAGS). Validado com apuração real via Client.force_login() e testes unitários isolados (negrito/itálico/lista/imagem, resumo vazio, zero observações) — todos os PDFs conferidos com pdfplumber. Detalhe completo no CLAUDE.md desta pasta.

133. Limiar de variacao_atipica_dre ajustado pra 65% + severidade rebaixada pra baixa

Pedido explícito do usuário: variação atípica na DRE (Análise Vertical, ver rodada 129) só deveria disparar acima de 65% de variação relativa (antes 50%) e, quando disparar, entrar como prioridade baixa (antes média). VARIACAO_LIMIAR_PERCENTUAL (regras.py) foi de Decimal("0.5") pra Decimal("0.65"), e regra_variacao_atipica_dre passou a usar SEVERIDADE_BAIXA em vez de SEVERIDADE_MEDIA. VARIACAO_AV_PONTOS_PERCENTUAIS_MINIMO (piso de 1 ponto percentual) não mudou. Só constante+severidade — nenhuma mudança de estrutura, endpoint ou frontend (a regra continua com a mesma chave variacao_atipica_dre, mesmo grupo "Variações e Indicadores" em PID_DC_GRUPOS/dashboard-contabil.js, e a ordenação por severidade da aba Observações já lida com qualquer severidade automaticamente). Efeito prático: menos achados disparam (limiar mais alto) e os que disparam aparecem por último na lista ordenada por severidade (rodada 96) e contam pro terço "Baixa" do donut do resumo (rodada 98), não mais pro terço "Média".

134. Relatório do cliente: "Limpar formatação" virou ícone dentro do cabeçalho da tabela

Pedido explícito do usuário: o botão "Limpar formatação" (rodada 131), até então um botão com texto solto dentro do <h2> de cada seção (Balancete/D.R.E./Análise Vertical), passou a ser um ícone pequeno dentro da própria célula de cabeçalho "Observação" da tabela — mesmo padrão do botão "Restaurar formatação padrão" que já existe na tela de revisão (icon-btn dc-reset-formatacao-btn dentro de .dc-th-linha, dashboard-contabil.html/.css). dashboard-contabil-relatorio.html ganhou as classes equivalentes (.dcr-th-linha/.dcr-th-reset-btn) e o <th>Observação</th> das três tabelas virou <th><span class="dcr-th-linha">Observação<button class="dcr-th-reset-btn" data-dcr-reset="...">...</button></span></th>. data-dcr-reset (o atributo que o JS já usava, document.querySelectorAll("[data-dcr-reset]")) foi mantido, então nenhuma linha de script mudou — só HTML/CSS. .dcr-reset-btn (a classe antiga, com texto+borda+padding de pílula) foi removida por completo, sem consumidor restante. Detalhe completo no CLAUDE.md desta pasta.

Bug real, reportado pelo usuário testando a mudança acima: na aba Análise Vertical, a coluna "Observação" (e o ícone dentro dela) aparecia cortada — não dava nem pra ler "Observação" por completo, nem pra ver o botão. Causa: .dcr-page (o container do relatório) tem max-width:1140px, e a tabela de Análise Vertical pode ter bem mais colunas que Balancete/D.R.E. (Descrição + 2 por mês + Observação, normalmente 8 no total pra 3 meses) — com .dcr-tabela-wrap { overflow: hidden }, o navegador espremia cada coluna até quebrar o texto do cabeçalho em 2-3 linhas e cortar a última coluna pra fora da área visível. Corrigido trocando pra overflow-x: auto (mantendo overflow-y: hidden, mesmo comportamento vertical de sempre) e white-space: nowrap em table.dcr-tabela th — agora a tabela cresce além do container quando precisa (ativando rolagem horizontal) em vez de espremer/cortar colunas. Balancete/D.R.E., que já cabiam sem aperto, não mudaram de aparência.

Pedido explícito do usuário, ainda na mesma rodada: a rolagem horizontal resolvia o corte, mas o usuário preferiu que o texto diminuísse o bastante pra caber tudo sem precisar arrastar a tabela, pelo menos no caso comum (3 meses). Nova classe dcr-tabela--compacta, só na tabela de Análise Vertical (Balancete/D.R.E. mantidos do tamanho original — já cabiam sem aperto): padding menor (9px 14px → 5px 7px), fonte menor (corpo 0.85rem → 0.74rem, cabeçalho 0.72rem → 0.6rem, letter-spacing reduzido) e os ícones de dentro da tabela (toggle de expandir, "Limpar formatação", observação) encolhidos proporcionalmente. Novo filtro de template mes_curto (contabil_extras.py) corta o ano do mês pra 2 dígitos ("mai/2026" → "mai/26") só no cabeçalho dessa tabela — o texto repetido "— Valor"/"— Variação" em cada uma das colunas por mês era o maior consumidor de largura. overflow-x: auto do ajuste anterior continua como rede de segurança (uma apuração com mais de 3 meses ainda pode precisar rolar), mas o caso comum (3 meses) passa a caber inteiro sem rolagem nenhuma. Detalhe completo no CLAUDE.md desta pasta.

135. Bug real: observação "vazando" entre contas com a mesma classificação + ícone da conta "mãe" destacado

Usuário reportou, com print de um balancete real: 6 bancos diferentes (Banco do Brasil, Inter, Itaú, Mercado Pago, PagSeguro, Sicredi) todos sob o mesmo código de classificação "1.01.01.002.001" ("Depósitos Bancários à Vista") — uma observação escrita num deles aparecia em todos os outros. Causa raiz: ContabilObservacao.chave_conta() (chave natural do histórico de observações, ver rodada 126) usava só o codigo de classificação, que o Questor reaproveita entre várias contas analíticas de mesma natureza — não é único dentro de uma apuração. Corrigido trocando a chave pra "codigo|descricao" (models.py, dashboard-contabil.js) — mesmo espírito de chave_linha() (DRE/Análise Vertical, que já usa "descricao|nivel"). _contabil_sincroniza_contas() (views.py, reprocessamento) também passou a casar contas por (codigo, descricao) em vez de só codigo, mesmo trade-off que a sincronização de DRE/Análise Vertical já aceitava (conta renomeada = conta "nova", a antiga é excluída). Migração de dados 0073 recalculou o alvo_chave das observações já em produção (2 de conta) a partir do próprio alvo_rotulo (a descrição já congelada em cada registro no momento em que foi escrita) — não precisou reconstruir nada a partir da apuração de origem.

Segundo pedido, mesma rodada: quando uma conta/linha "filha" tem observação e o grupo está recolhido, o contador não tinha como saber sem expandir. O ícone de observação da sintética "mãe" agora ganha um destaque (.dc-conta-observacao-btn--descendente, cor --gold) sempre que algum descendente (não só filho direto) tem observação vigente e a própria sintética não tem observação própria (dcTemObservacaoDescendente(), nova, reaproveita dcDescendentes() já usada pelo tri-state do botão "validado") — nas três árvores (Balancete/D.R.E./Análise Vertical). Detalhe completo no CLAUDE.md desta pasta.

136. Coluna "Conta" da revisão mostrava a Classificação, não o número da conta

Usuário comparou com o PDF original (que tem as duas colunas, "Conta" e "S Classificação") e reportou que a aba Balancete da tela de revisão só mostrava a Classificação (codigo) numa coluna rotulada "Conta" — o número interno da conta no Questor (conta_numero, já extraído e salvo desde sempre, só nunca exibido) não aparecia em lugar nenhum. Confirmado com o usuário que o pedido era só pra tela de revisão (não pro relatório "Gerar Dashboard" que vai pro cliente). dashboard-contabil.html ganhou uma coluna nova ("Conta", conta_numero) antes da já existente, renomeada pra "Classificação" (codigo); renderContas() (dashboard-contabil.js) passou a emitir as duas células. Ajustes de acompanhamento: colspan do editor inline de observação (7 → 8) e os seletores CSS que dependiam de posição de coluna (.dc-contas-table td:nth-child(...), alinhamento numérico das colunas de valor e o estilo apagado/nowrap da(s) coluna(s) de identificação da conta) deslocados em uma posição.

137. Histórico de execuções ganhou ordenação e filtro por coluna, igual Importação de Plano de Saúde

Usuário pediu que a tabela de execuções (histórico de análises, #dc-list-table) tivesse a mesma ordenação/filtro por coluna já existente na tabela equivalente de Importação de Plano de Saúde, com a última execução aparecendo primeiro por padrão e um botão de limpar filtros. Mecanismo portado 1:1 (mesmas classes/lógica, prefixo dc- em vez de ips-): funil "estilo Excel" no cabeçalho de Empresa/Status/Criado por (busca + checklist de valores distintos), as 6 colunas ordenáveis por clique no <th>, ordenação padrão por criado_em decrescente (antes disso a ordem vinha só do Meta.ordering do model, que prioriza competência antes de data de criação) e botão "borracha" que limpa os filtros e volta a ordenação pro padrão de uma vez. carregarLista() passou a só buscar a API; a filtragem/ordenação/render virou uma função separada (renderList()) que roda sobre o array já carregado, sem round-trip novo a cada clique. Sem paginação, diferente da outra tela (não foi pedida, e o histórico tende a ser mais curto).

Ajuste na mesma rodada, antes do usuário terminar de testar: "Competência" também ganhou o funil de filtro (nasceu só ordenável) — indexado pela data ISO bruta, com o popup mostrando o rótulo MM/AAAA já usado no resto da tela. De brinde, a ordenação dos valores dentro de qualquer popup de filtro passou a usar o valor bruto em vez do rótulo formatado, porque ordenar "Competência" pelo rótulo (localeCompare de "MM/AAAA") agruparia por mês antes do ano; a data ISO já ordena cronologicamente certo por comparação de string. Detalhe técnico completo no CLAUDE.md desta pasta.

138. Bug real: valores em R$ dentro do texto dos achados sem formatação brasileira

Usuário reportou, com print da aba Observações da revisão, que valores monetários dentro do texto dos achados apareciam sem padronização (ex.: "R$ 643545.85" em vez de "R$ 643.545,85"), inconsistente com o padrão já usado em Balancete/DRE/relatório "Gerar Dashboard". Causa raiz: regras.py interpola Decimal direto num f-string (f"R$ {conta.saldo_atual}") pra montar AchadoDetectado.mensagem — usa a formatação padrão do Python (sem separador de milhar, ponto decimal), nunca o padrão BR. Corrigido com um helper novo, _moeda() (regras.py), duplicado localmente em vez de importado — mesmo padrão já usado (por arquivo) em indicadores/recibo.py/custo_contratacao/pdf.py/templatetags/contabil_extras.py, já que este pacote é Python puro. As 9 mensagens de achado que interpolavam R$ {valor} direto foram todas ajustadas.

Só vale pra achados gerados dali em diante — um achado já persistido mantém o texto antigo até a apuração ser reprocessada ou recriada; sem migração de dados pra reformatar texto já gravado (parsear número dentro de frase livre por regex arriscaria confundir valor monetário com código de conta, ex. "1.01.01.001"). Fora do escopo desta rodada: regra_variacao_atipica_dre ainda formata percentual com ponto decimal ("12.34%", não "12,34%") — mesma família de bug, não pedida desta vez. Detalhe técnico completo no CLAUDE.md desta pasta.

139. Bug real: cor "validado" não subia pra conta "mãe" quando só os "netos" eram marcados

Usuário reportou, com print de uma árvore de 3 níveis (Balancete), que uma conta "mãe" (ex. "CAIXA E EQUIVALENTES DE CAIXA") não ficava verde mesmo depois de toda conta-folha dentro dela ("netos", ex. os 4 bancos dentro de "DEPÓSITOS BANCÁRIOS A VISTA") já estarem validadas uma a uma — mesmo a conta "filha" intermediária ("DEPÓSITOS BANCÁRIOS A VISTA") já aparecendo corretamente verde. Causa raiz: dcEstadoValidacaoGrupo() (dashboard-contabil.js, tri-state do botão "validado" de uma sintética, ver rodada de "Tri-state numa conta/linha sintética") decidia o estado "completo" checando descendentes.every(d => d.validado) sobre todos os descendentes (filhos, netos, bisnetos), não só os de folha — quando os netos são marcados diretamente (sem clicar na própria "filha" intermediária pra cascatear), o campo validado da "filha" no banco continua False mesmo que ela já apareça "completa" visualmente (calculada a cada render); a "mãe", ao olhar todos os descendentes, incluía essa "filha" com validado=False e nunca fechava o grupo.

Corrigido restringindo o critério de "completo" só aos descendentes folha (sem filhos) — dcDescendentes() ganhou um quarto parâmetro opcional (somenteFolhas), reaproveitando o mesmo cálculo de "tem filho" já usado por temFilhos; dcEstadoValidacaoGrupo(item, descendentes, descendentesFolhas) passou a checar descendentesFolhas.every(d => d.validado) pro estado "completo", mantendo descendentes (todos) pro "parcial" (uma sintética marcada sozinha, sem cascatear, ainda deve sinalizar "em andamento" num ancestral). Os 4 pontos que calculam o estado (dcValidadoInfo() + os 3 handlers de clique — Balancete/D.R.E./Análise Vertical) foram atualizados pra passar os dois conjuntos. Puramente lógica de exibição/cálculo no frontend — nenhuma mudança de model/endpoint.

140. Bug real: destaque de um grupo já expandido desaparecia ao expandir outro

Usuário reportou, com print de uma árvore de 3 níveis (Balancete), que expandir uma segunda conta apagava visualmente o destaque dourado da primeira já expandida — o esperado era as duas ficarem destacadas ao mesmo tempo, pra facilitar a validação em sequência de várias contas abertas de uma vez. Causa: o destaque das linhas recém-reveladas ao expandir (ver rodada "Destaque acompanha o último grupo expandido") era desenhado pra substituir a leva anterior por design — cada expandir trocava dcContasDestaque/dcDreDestaque/dcAvDestaque pelos filhos diretos do grupo recém-aberto, empilhando o destaque anterior numa pilha (dc*DestaqueHistorico) só restaurada ao recolher o mesmo grupo.

Duas tentativas falhas antes de acertar (as duas reportadas pelo usuário testando em produção — sempre com hard refresh e restart do servidor, então nunca foi cache):

  1. Somar os filhos revelados ao Set de destaque já existente: não mudou nada visualmente. Causa raiz, só encontrada depois de simular o algoritmo em Python contra as contas reais da apuração do usuário: o destaque inicial (dcUltimaLevaVisivel) já marca praticamente toda conta de nível ≥ 2 — todas nascem recolhidas pelo limiar padrão, então cada uma conta como "fim de ramo visível". Somar os filhos revelados a essa base deixava a tabela inteira destacada, e com tudo destacado nada se destaca. O comportamento original só parecia funcionar porque o "substituir" limpava todo o resto da tabela junto.
  2. Destacar a própria conta expandida em vez dos filhos: o usuário esclareceu que destacar os filhos sempre esteve certo — a única mudança pedida era acumular mais de um grupo.

Correção final: dcDestaqueGrupos(itens, nivelFn, colapsadas, expandidos) (nova) passa a derivar o destaque sempre do zero, a partir de um Set novo por árvore (dcContasExpandidos/dcDreExpandidos/dcAvExpandidos) com os grupos que o contador expandiu e que continuam abertos: sem nenhum grupo aberto, vale a leva padrão de sempre (dcUltimaLevaVisivel); com algum aberto, o destaque é só a união dos filhos diretos desses grupos, descartando a leva padrão — é esse descarte que devolve o contraste. Recolher um grupo remove ele e todos os descendentes dele do conjunto (um grupo aninhado que estava aberto dentro dele deixa de contar), garantindo que fechar tudo devolva exatamente o estado inicial. As três pilhas de histórico (dc*DestaqueHistorico) foram removidas — recolher não precisa mais restaurar nada de uma pilha.

Validado simulando o algoritmo em Python contra as contas reais da apuração 2021/08-2026, reproduzindo o roteiro exato descrito pelo 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 e soma "DUPLICATAS A RECEBER"; fechar "CLIENTES" volta aos 2 primeiros; fechar "CAIXA" devolve exatamente o conjunto de destaque inicial (igualdade entre os dois conjuntos conferida). Mesmo ajuste replicado em pidDcrArvore() (dashboard-contabil-relatorio.html), que reimplementa esta lógica em JS puro pro relatório "Gerar Dashboard" — os dois lados são mantidos manualmente em sincronia, sem fonte única (um é SPA, o outro HTML estático renderizado uma vez). Puramente lógica de exibição no frontend, sem mudança de model/endpoint. Detalhe técnico completo no CLAUDE.md desta pasta.

Lição registrada na memória do projeto: nas duas primeiras tentativas a resposta ao "não funcionou" incluiu a hipótese de cache do navegador/JS antigo em memória — o usuário deixou claro que sempre testa com hard refresh e restart do servidor antes de reportar. A causa era sempre bug real, e só apareceu quando o algoritmo foi simulado com os dados reais (script Python ad-hoc lendo as contas da apuração pelo ORM) em vez de analisado estaticamente.

141. Botão "+" (expandir tudo) no cabeçalho das tabelas da revisão

Pedido explícito do usuário: um botão pequeno no canto esquerdo do cabeçalho da primeira coluna que expande todas as contas até o último nível de uma vez. A ideia inicial tinha também um "−" que recolheria um nível por vez, descartada pelo próprio usuário na mesma conversa ("poderia apenas um botão, o de +").

.dc-expandir-tudo-btn (data-dc-expandir-tudo) nas 3 tabelas da revisão (Balancete, D.R.E. e Análise Vertical — cada uma com o seu, já que cada árvore tem estado de colapso próprio, mesmo motivo do botão "Restaurar formatação" que já existia), dentro do novo modificador .dc-th-linha--inicio (só troca o justify-content pra flex-start, já que aqui o botão vem antes do rótulo, e não colado na borda direita como o de restaurar). Não é um toggle: o caminho de volta é o próprio "Restaurar formatação padrão", na última coluna da mesma tabela.

Detalhe não-óbvio da implementação: expandir tudo zera o conjunto de grupos expandidos (dc*Expandidos) em vez de populá-lo com todos os grupos — com ele vazio, o destaque volta a sair de dcUltimaLevaVisivel(), que sem nada recolhido marca exatamente as folhas (as contas analíticas finais). Conferido contra a apuração real 2021/08-2026: 133 contas visíveis e 74 destacadas, exatamente as 74 contas sem filhos. Popular o conjunto com todos os grupos destacaria quase toda linha da tabela, recaindo no mesmo "tudo destacado, nada se destaca" da rodada 140. Só frontend (HTML/CSS/JS), sem mudança de model/endpoint; o relatório "Gerar Dashboard" não ganhou esse botão nesta rodada.

142. Log de reprocessamentos — quem reprocessou e quantas vezes

Pedido explícito do usuário: "quando o usuário reprocessar a análise, o ideal é que registre um log... pra que seja possível identificar quem reprocessou e quantos reprocessamentos já ocorreu."

Model novo ContabilApuracaoReprocessamento (reprocessado_por FK SET_NULL + reprocessado_em, migração 0075) — mesmo espírito de ContabilObservacaoEdicao, um registro por reprocessamento bem-sucedido. ContabilApuracaoViewSet.reprocessar() grava o registro dentro da mesma transação da resincronização (logo depois de sincronizar os achados), então uma tentativa que falhar no meio do caminho não deixa registro órfão. ContabilApuracaoListSerializer (a lista de execuções, de onde o botão "Reprocessar" é aberto) ganhou reprocessamentos (nested, mais recente primeiro), com prefetch_related novo só pra essa action, evitando N+1.

Frontend: o modal "Reprocessar análise" ganhou uma seção (nasce escondida numa apuração nunca reprocessada) com a contagem e a lista "Nome · dd/mm/aaaa hh:mm" — sem nenhuma chamada de API própria, já que os dados vêm da lista que a tela já carregou. Novo formatador pidDcFormatDataHora() (os demais formatadores de data desta tela só mostram o dia, sem hora — aqui a hora importa porque mais de um reprocessamento pode acontecer no mesmo dia). Validado com um script Python isolado (transação com rollback forçado, contra uma apuração real) confirmando ordem/serialização; nada persistido em produção além da migração. Detalhe técnico completo no CLAUDE.md desta pasta.

143. Achados voltam a ser recriados do zero a cada reprocessamento (revisão da decisão da rodada 124)

Pedido explícito do usuário: "sempre que o usuário reprocessar um documento, os apontamentos que são realizados pela própria aplicação e constam na tela de Observações devem ser refeitos. Isso já ressalta para o usuário que não há mais inconsistências para validar e pode verificar apenas se surgiram novos. As observações e comentários que ele realizou nas contas devem permanecer independente do reprocessamento." Reverte por completo a decisão da rodada 124 — até aqui, um achado (apontamento automático de auditoria) já tratado pelo contador continuava marcado como "tratado" mesmo depois de a inconsistência sumir dos dados do PDF novo, contrariando o próprio propósito da tela (sinalizar o que ainda precisa de atenção).

_contabil_sincroniza_achados() (que casava por (regra, código da conta) e preservava status/observacao_contador/tratado_por/tratado_em/oculto_no_relatorio) virou _contabil_recria_achados() — apaga todos os achados da apuração (apuracao.achados.all().delete()) e recria do zero a partir do motor de regras rodado sobre o PDF novo (mesmo bulk_create() de create()). Todo achado nasce pendente, mesmo que a mesma combinação regra+conta já tivesse sido tratada com justificativa antes do reprocessamento — os valores mudaram, então a tratativa antiga deixa de fazer sentido. A preocupação original da rodada 124 (delete+recria quebraria a FK conta de um achado preservado) não se aplica mais: não existe mais achado "preservado" através do delete, a FK é sempre resolvida fresca contra as contas já sincronizadas na mesma transação.

ContabilObservacao (comentário/observação do contador numa conta, sistema separado desde a rodada 126) não é tocada — nunca teve relação com achados, é casada por chave natural (empresa+conta) e sobrevive a qualquer reprocessamento, exatamente como pedido. Validado com um script Python isolado (transaction.atomic() com rollback forçado, contra uma apuração real já em produção): um achado marcado manualmente como "tratado" com justificativa desapareceu depois de simular _contabil_recria_achados() com a mesma lista de achados detectados, dando lugar a um achado novo pendente com o mesmo conteúdo (regra/conta/mensagem) mas id diferente; uma ContabilObservacao criada na mesma apuração permaneceu intacta. Nada persistido em produção. Detalhe técnico completo no CLAUDE.md desta pasta.

144. Bug real: "Conta do Passivo/Ativo com saldo invertido" disparava pra conta descendente de uma redutora, não só a filha direta

Usuário reportou, com print de achados reais ("Conta do Passivo 'DANITHI LTDA'"/"'MPASARABIAHOLDINGPARTICIPAÇÕES E'" com saldo devedor): quando a conta "mãe" tem o sinal "(-)" na frente (ex. "(-) CAPITALA INTEGRALIZAR"), o saldo "invertido" dos filhos dela é o comportamento esperado da própria natureza daquela conta, não uma inconsistência — mas a regra saldo_sinal_invertido só excluía a conta que em si começava com "(-)", não suas descendentes.

Corrigido com _indices_descendentes_de_conta_redutora() (novo, regras.py) — identifica toda conta com algum ancestral (não só o pai direto) redutora, usando o mesmo algoritmo de pilha de níveis já usado em toda a aplicação pra árvore de contas (nível = codigo.count("."), ordem de leitura do PDF), não comparação de prefixo de código (o Questor reaproveita código de classificação entre contas analíticas irmãs, então string matching não seria confiável pra achar o pai). Validado com uma árvore sintética reproduzindo o exemplo do usuário (confirmando que o achado deixa de disparar com a correção e dispararia sem ela) e rodando contra as 3 apurações reais já em produção — a apuração 1751/TAROBA (a mesma do print) teve exatamente os 2 achados dos sócios suprimidos.

Não afeta achados já persistidos: os 2 achados reais da apuração 1751 só somem da tela depois de reprocessar essa apuração (o motor de regras roda de novo do zero, ver rodada 143) — nenhuma edição direta no banco foi feita. Detalhe técnico completo no CLAUDE.md desta pasta.

145. Botão de excluir observação volta — restrito a quem criou, só antes de concluir a análise

Pedido explícito do usuário: "inclua outro botão, transforme o atual em outro e inclua o da lixeira para excluir a observação. A exclusão só poderá ser realizada pelo usuário que criou ela e só pode ser realizada antes do usuário apertar o botão de concluir análise. Após concluir a análise, edição não deve ser permitida." Reverte parte de uma decisão anterior (o botão de excluir tinha sido tirado, deixando só "encerrar" — ocultar das próximas competências, sem apagar) — agora as duas ações convivem: encerrar continua reversível e liberado pra qualquer um do time; excluir é definitivo e só de quem escreveu a observação.

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.