61 KiB
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:
- Rename só de rótulo visível — "Dashboard Contábil" virou "Relatório Contábil" no menu (
catalogo.py),<title>/<h1>/cabeçalhos dedashboard-contabil.htmle 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 noCLAUDE.mddesta pasta. - Checkbox "validado" — campo novo
validado(BooleanField, defaultFalse, migração0066) emContabilConta/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. - Observação virou editor inline — o popup único
#dc-observacao-modalfoi 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á existenteoculta_no_relatorio, cujodefaulttambém mudou deFalsepraTrue(migração0066) — uma observação nova só vai pro relatório do cliente depois que o contador confirmar isso explicitamente, nunca por padrão. - 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.