135 KiB
Changelog — Relatório Contábil
Histórico específico desta aplicação, extraído de
plano.md. As entradas anteriores à rodada 120 chamam a aplicação de "Dashboard Contábil", nome visível até aquele rename — os nomes técnicos (dashboard_contabil/dashboard-contabil.*/Contabil*//api/contabil-*) nunca mudaram.Formato: uma entrada por rodada,
### Rodada N — Título; quando a rodada não tem número registrado,### Títulosó. A numeração de rodada não é global — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo deplano.md.
Rodada 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.
Rodada 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").
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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).
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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).
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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).
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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".
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 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):
- Somar os filhos revelados ao
Setde 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. - 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.
Rodada 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.
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.
Rodada 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.
Rodada 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.
Rodada 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.
Rodada 146 — Botão "Ver na tabela" nos cards de achado — navega até a linha referida e destaca
Pedido explícito do usuário, a partir de um caso real de "Variação atípica na DRE": várias linhas da Análise Vertical repetem a mesma descrição em centros de custo diferentes ("MATERIAIS E SERVIÇOS APLICADOS NA OBRA" aparecia em mais de um grupo), tornando inviável localizar manualmente qual linha da tabela um achado descreve só pelo texto do card. Pedido generalizado pra "todas as observações identificadas pela aplicação quais referem-se a contas específicas", não só essa regra.
Todo achado com conta (a maioria das regras) ou linha_analise_vertical (só regra_variacao_atipica_dre, ver abaixo) ganhou um botão "Ver na tabela" ao lado do "Revisar" — clicar troca pra aba certa (Balancete ou Análise Vertical), abre só os ancestrais da linha alvo (não a árvore inteira, mesma pilha de níveis usada em toda a tela) e aplica um pulso visual temporário (.dc-conta-row--foco, ~2,4s) na linha, além de rolar até ela. Achados sem alvo específico (ex. desbalanceamento Ativo x Passivo) não ganham o botão.
Por que só conta/linha_analise_vertical, nunca linha_dre: regra_variacao_atipica_dre lê a seção "Demonstração Mensal (Análise Vertical)" do PDF (atual.linhas_analise_vertical), não a DRE simples — os percentuais que o achado descreve só existem naquela tabela (a aba DRE só tem uma coluna de valor, sem percentual), então o destino correto é a aba Análise Vertical, mesmo o achado se chamando "Variação atípica na DRE" (nome herdado do título da seção no relatório do Questor). ContabilAchado ganhou linha_analise_vertical (FK opcional pra ContabilLinhaAnaliseVertical, SET_NULL) e AchadoDetectado ganhou ordem_linha_analise_vertical — a view casa por ordem (já existia como campo persistido, único dentro da apuração) contra apuracao.linhas_analise_vertical.all() depois de criar/ressincronizar as linhas, tanto em create() quanto em _contabil_recria_achados() (reprocessamento). ContabilAchadoSerializer expõe só o id da linha — o frontend já tem a linha completa cacheada em apuracaoAtual.linhas_analise_vertical.
Validado rodando regra_variacao_atipica_dre sobre os dados reais de uma apuração de produção (57 achados) e conferindo que todo ordem_linha_analise_vertical retornado casa com uma ContabilLinhaAnaliseVertical de verdade via o mesmo dicionário ordem → linha usado pela view (transação com rollback, nada persistido). Migração 0076_contabilachado_linha_analise_vertical. Detalhe técnico completo no CLAUDE.md desta pasta.
Rodada 146.1 — Correção na mesma rodada: o botão não aparecia em nenhum achado já existente
Usuário reportou que o botão não apareceu, mesmo reiniciando o servidor e forçando a atualização da página. Não era cache nem JS antigo: o vínculo linha_analise_vertical só era gravado na criação do achado, e todos os achados em produção foram criados antes do campo existir. Diagnóstico no banco real: das 4 apurações, 3 tinham exclusivamente achados de variacao_atipica_dre (a que estava na tela do usuário tinha 13 de 13), então nenhum card daquela tela tinha como mostrar o botão. Reprocessar devolveria o vínculo, mas reprocessar exige reanexar o PDF — inviável pedir isso por causa de um botão.
Corrigido com uma migração de dados (0077_backfill_achado_linha_analise_vertical) que vincula os 78 achados antigos à linha certa, casando pela assinatura contida na própria mensagem do achado (descrição + percentual dos dois últimos meses) contra ContabilLinhaAnaliseVertical.valores[-2:]. Não re-executa regras.py de propósito — migração que importa código de app deixa de ser auto-contida se a regra mudar depois.
O primeiro algoritmo casava só por descrição, avançando um ponteiro na ordem de leitura, e a conferência contra produção reprovou: 4 dos 78 achados vincularam a linha errada, justamente nas descrições repetidas entre centros de custo (MATERIAIS E SERVIÇOSAPLICADOS NAOBRA, 4 ocorrências) — ou seja, exatamente o caso que motivou o pedido do usuário teria ficado errado em silêncio. A regra dispara na ocorrência cujos percentuais estouram o limiar, que não é necessariamente a primeira. Com a assinatura completa: 78 de 78 casados, e a conferência final (comparar os percentuais da linha vinculada com os citados no texto de cada achado) deu 0 divergências. Migração já aplicada em produção; os 13 achados da apuração do print passaram a trazer o botão.
Rodada 147 — Excluir análise bloqueado depois de concluída
Pedido explícito do usuário (com print do histórico, numa linha "Concluída"): "quando estiver Concluída, a exclusão deve ficar bloqueada." Fecha a última ação destrutiva que ainda escapava da trava de "concluiu, não se mexe mais" — reprocessar, editar observação e tratar achado já eram bloqueados, mas excluir a análise inteira continuava liberado, o que é justamente o mais destrutivo dos três.
ContabilApuracaoViewSet.perform_destroy() recusa com 400 quando status == "concluida" (mensagem própria, não o _contabil_garante_em_revisao, cuja frase fala em editar observações/achados). Na lista, o ícone de lixeira some na linha concluída — mesmo padrão que o botão de reprocessar ao lado já usava, pra não misturar "some" e "aparece desabilitado" na mesma linha de ações. O clique em excluir também ganhou try/catch + pidAlert: o botão não aparece numa análise concluída, mas outra pessoa pode ter concluído depois da lista ter sido carregada, e aí o motivo precisa chegar na tela em vez de o clique não fazer nada.
Validado via Client.force_login() contra dados reais: DELETE numa apuração concluída → 400 e o registro continua no banco; DELETE numa em revisão → 204, seguiu funcionando normalmente.
Incidente durante essa validação (registrado de propósito, ver também a rodada 148 abaixo): o teste rodou dentro de transaction.atomic() com rollback forçado, o padrão usado o tempo todo aqui, mas perform_destroy() chama instance.arquivo.delete(save=False) — e apagar arquivo do storage não é revertido por rollback de transação. O registro da apuração 26 (PURE TECH ENERGY) voltou pelo rollback, o PDF anexado dela não. Nenhum dado analítico se perdeu (contas, DRE, Análise Vertical, os 57 achados, observações e validações estavam todos no banco) e nenhuma funcionalidade depende desse arquivo — arquivo não é exposto em nenhum serializer e reprocessar() sempre grava um upload novo, nunca lê o antigo —, mas o anexo original precisa ser reposto reanexando o mesmo PDF pelo botão "Reprocessar". Lição pra qualquer teste futuro de exclusão nesta base: rollback só protege o banco; efeito colateral em disco exige apuração descartável ou storage isolado.
Rodada 148 — Tabelas abrem com tudo expandido; o "+" virou "−"
Pedido explícito do usuário: "por padrão, trazer as tabelas com a expansão de todas as contas até o último nível. O comportamento do botão de + deve virar um − e recolher os níveis para que retorne no atual padrão que abre a tabela. O botão de reverter deve reverter ao novo padrão (todas expandidas)." Inverte o padrão de abertura que valia desde as primeiras rodadas nas 3 tabelas da revisão (Balancete, DRE e Análise Vertical).
Na prática o estado inicial passou a ser exatamente o que o antigo "+" produzia — por isso a mudança é pequena: renderRevisao() passa new Set() nos três conjuntos de colapso e o botão da primeira coluna aplica dcColapsoPadrao() (a visão compacta, que era o padrão de abertura). O atributo e a classe foram renomeados (data-dc-expandir-tudo/dc-expandir-tudo-btn → data-dc-recolher-grupos/dc-recolher-grupos-btn), já que "expandir tudo" passaria a descrever o oposto do que o botão faz; o ícone perdeu o traço vertical do "+" e os textos de tooltip/acessibilidade foram reescritos.
Virou toggle logo em seguida, ainda na mesma rodada: o usuário testou e pediu que "após apertar uma vez, o botão de − vira um + e retorna para o padrão anterior" — na primeira versão ele só recolhia, e voltar exigia achar o botão de restaurar na outra ponta do cabeçalho. O handler passou a alternar (colapsadas.size ? new Set() : dcColapsoPadrao(...)) e dcAtualizaBotaoArvore() troca ícone/tooltip/rótulo de acessibilidade no fim de cada render. A face do botão é derivada de quantos grupos estão recolhidos, não de um flag próprio: recolher uma única conta pelo toggle da própria linha já faz o botão virar "+", que devolve a tabela inteira. Simulado contra o Balancete real da apuração 27 (564 contas): abre em "−" com 564 visíveis → clique vira "+" com 29 → clique volta pra "−" com 564, estável em ciclo. Efeito colateral: o botão de restaurar passou a fazer o mesmo que a face "+" — mantido de propósito, é a "borracha" já conhecida de outras telas do Portal, mas dá pra remover se o usuário preferir.
O destaque das linhas não precisou de ajuste nenhum: dcUltimaLevaVisivel() com o conjunto vazio já marca as folhas, que é o comportamento que o "+" tinha. Simulado contra as 4 apurações reais de produção (replicando dcColapsoPadrao/dcUltimaLevaVisivel em Python, sem tocar no banco): no padrão novo todas as linhas ficam visíveis e o destaque bate exatamente com as folhas (Balancete da apuração 27: 564 visíveis, 443 destacadas = as 443 folhas), e o "−" reduz pra visão compacta (as mesmas 564 caem pra 29). Efeito esperado e aceito: as tabelas abrem bem mais longas do que antes.
O relatório "Gerar Dashboard" não foi tocado — continua nascendo recolhido (colapsado_padrao calculado server-side em _contabil_arvore_contexto()). É o documento que vai pro cliente, tem só o botão de restaurar (nunca teve "+"/"−") e mudar o que o cliente vê não foi pedido; virou o único lugar onde dcColapsoPadrao/_CONTABIL_NIVEL_ABERTO_PADRAO ainda é padrão de abertura. Se o usuário quiser o mesmo lá, é um ajuste separado.
Mudança só em .js/.html/.css — não exige reiniciar o runserver.
Rodada 149 — CLAUDE.md reescrito como estado atual, sem cronologia
Rodada de revisão da documentação do Portal, não de código. Nenhuma mudança em .py/.js/.html/.css.
O CLAUDE.md desta pasta tinha virado um segundo changelog: 65% dele (129 KB de ~196 KB) eram seções datadas por rodada ("Análise Vertical (rodada 123)", "Reprocessar (rodada 124)", "Ver na tabela (rodada 146)"), mais cinco ocorrências de "(rodada seguinte)" que só faziam sentido lendo o arquivo na ordem. Somando com este changelog (110 KB), eram ~300 KB para uma aplicação, com a parte que responde "como isso funciona hoje" soterrada no primeiro terço.
- Reescrito como documento de estado atual, organizado por subsistema (extração → regras → models → criação → reprocessamento → observações → indicadores → API → frontend da revisão → relatório do cliente → riscos), sem nenhuma referência a rodada e sem narrativas "antes era X, agora é Y". Onde o motivo de uma decisão importa para não revertê-la por engano (por que a chave da observação inclui a descrição, por que o destaque descarta a leva padrão, por que achado é recriado e conta não, por que o relatório é GET), o motivo ficou — como motivo, não como cronologia.
- Nenhum fato técnico foi descartado: 196 KB → 79 KB é consolidação de repetição (a mesma mecânica de colapso/destaque era recontada em quatro rodadas diferentes) mais a remoção do histórico, que já estava aqui.
- Ganhou uma tabela de API com os 22 endpoints da ferramenta, que não existia em lugar nenhum: a tabela do
CLAUDE.mdda raiz tinha (e agora tem de propósito) zero linhacontabil-*. - Uma seção nova de "Riscos conhecidos e armadilhas" reúne o que antes estava espalhado: códigos de classificação fixos, as duas cópias mantidas à mão (ícones SVG, lógica de árvore), o N+1 de
total_achados_pendentese o fato de que testar exclusão nesta ViewSet destrói o PDF de verdade. - O título deste changelog passou de "Dashboard Contábil" para "Relatório Contábil" (o rename de rótulo aconteceu na rodada 120 e nunca tinha chegado aqui), e as 58 entradas passaram de
### N. Títulopara### Rodada N — Título, o formato usado pelos outros 11 changelogs do projeto.
Rodada 150 — Coluna "Observação" fixa na borda direita do relatório do cliente
Bug reportado pelo usuário: em balancetes largos (empresa com muitas contas/valores grandes), o botão "Restaurar formatação padrão" no cabeçalho "Observação" (e o ícone de observação de cada linha) ficava fora da área visível de .dcr-tabela-wrap, só alcançável arrastando a tabela inteira pro lado — o comentário em dashboard-contabil-relatorio.html que introduziu o overflow-x:auto do wrap (ver CLAUDE.md desta pasta) assumia que Balancete/D.R.E. sempre coubessem sem precisar de scroll, o que não é verdade pra toda empresa/competência.
Corrigido com position: sticky; right: 0 na última coluna (th/td.dcr-col-observacao, nova classe) das três tabelas (Balancete, D.R.E., Análise Vertical) — a coluna some da rolagem: fica sempre visível na borda direita, aconteça o que acontecer com a largura das demais. Não se aplica à linha de observação expandida (<td colspan>), que não recebe a classe nova. Sem mudança de comportamento pra quem não precisa rolar (tabela estreita continua igual); a rolagem em si continua existindo como rede de segurança pras demais colunas.
Rodada 151 — Linha da DRE removida do PDF sobrevivia ao reprocessamento (chave natural ambígua)
Bug reportado pelo usuário com print da tela: retirou "DESPESAS COM PESSOAL" de dentro de "DESPESAS DE VENDAS" na DRE, reprocessou, e a linha continuou lá — com um valor (R$ 106.667,83) maior que o do próprio grupo pai já atualizado (R$ 60.973,39), o que é impossível numa DRE consistente.
Causa: as três funções de sincronização montavam o mapa das linhas já salvas por dict comprehension ({chave: linha}). Como a chave natural da DRE/Análise Vertical era (descricao, nivel), e o mesmo rótulo aparece em ramos diferentes no mesmo nível ("DESPESAS COM PESSOAL" existe também sob "DESPESAS ADMINISTRATIVAS"), o dict guardava só a última ocorrência. A outra virava um fantasma: fora do dict, nunca era atualizada; fora do laço final de exclusão (que varre o dict, não a tabela), nunca era excluída. Ficava congelada com o valor da importação original, imune a todo reprocessamento seguinte e sem nem o badge de alterada_reprocessamento. A mesma ambiguidade fazia uma observação escrita numa das linhas homônimas aparecer na outra — o mesmo defeito que a rodada da migração 0073 corrigiu no Balancete acrescentando a descrição à chave.
Reproduzido antes de mexer no código, simulando _contabil_sincroniza_linhas_dre() fora do Django com uma DRE de duas ocorrências: saída idêntica ao print do usuário, com excluidas: nenhuma. Depois da correção, a mesma simulação exclui a linha do ramo certo e preserva a do outro ramo com o valor novo.
Duas correções, nas três tabelas:
- Chave por caminho na árvore (módulo novo
dashboard_contabil/chaves.py, Python puro): a chave da DRE/Análise Vertical passou de"descricao|nivel"para"grupo > subgrupo > descricao|nivel", montada percorrendo as linhas com a pilha de ancestrais (mesma técnica deregras._indices_descendentes_de_conta_redutora(), não comparação de prefixo). A chave do Balancete continua(codigo, descricao)— ali o código de classificação já carrega a posição na árvore. O módulo é a fonte única das três definições que não podem divergir: sincronização,ContabilObservacao.alvo_chavee os mapas de âncora do relatório. Efeito colateral da chave deixar de ser calculável por linha isolada:_contabil_chave_alvo()passou a receber a apuração,_contabil_arvore_contexto()recebe a lista de chaves alinhada em vez de umachave_fn, e no JS a chave é calculada por tabela (dcAplicaChavesObs(), no começo derenderDre()/renderAnaliseVertical()) e guardada emlinha.chave_obs. - Fila por chave, não registro único (
_contabil_agrupa_por_chave/_contabil_proxima_da_fila/_contabil_exclui_sobras): mesmo com a chave nova, duas linhas irmãs genuinamente idênticas no mesmo ramo ainda colidem. Agora cada ocorrência do PDF novo consome uma da fila na ordem de leitura, e o que sobra é excluído — o número de linhas salvas passa a bater sempre com o do PDF, seja qual for a chave.
Migração 0078: alvo_chave de max_length=320 para 1000 (o caminho inteiro é bem mais longo que a descrição isolada, e truncar reintroduziria a ambiguidade) + reescrita das chaves das observações de DRE/Análise Vertical já gravadas, reconstruindo o caminho a partir das linhas da apuração de origem (ou, se ela tiver sido excluída, da apuração mais recente da mesma empresa). Quando a chave antiga era ambígua, a primeira ocorrência vence — não há como saber a qual das linhas homônimas a observação se referia, e é a mesma que o índice do frontend antigo casava. Observação cuja linha não for encontrada fica com a chave antiga: continua listada no resumo da aba e no relatório pelo alvo_rotulo, sem casar com nenhuma linha, exatamente como já acontece com uma conta que saiu do plano.
Migração 0079: ordering dos três models de linha passou de ["ordem"] para ["ordem", "id"] (só AlterModelOptions, sem tocar em dado). O caminho de uma linha depende da posição dela na ordem de leitura, e ordem sozinha deixa a ordenação indefinida quando duas linhas empatam — situação que o próprio bug criava, já que a linha fantasma ficava com a ordem antiga enquanto as demais eram renumeradas. Os order_by("ordem") explícitos de views.py acompanharam.
Nada muda no que o cliente lê: alvo_chave só aparece em texto no relatório HTML e no PDF do Resumo para observações de conta, cuja chave não mudou.
Validado com manage.py check + makemigrations --check (estado de migração consistente com os models), teste da regra de caminho (rótulo repetido em dois ramos, nível pulado, nível negativo do parser, irmãs idênticas) e teste do mapeamento da migração, todos fora do banco. Falta rodar migrate e reprocessar a apuração afetada.
Na mesma rodada, a pedido do usuário: conta/linha removida passou a aparecer na aba Observações. Até aqui a exclusão era silenciosa — o item simplesmente sumia da tabela, e comparar duas versões da mesma apuração para descobrir o que saiu era trabalho manual. regras.achados_itens_removidos() monta um apontamento de severidade média por item excluído ("Conta removida no reprocessamento" / "Linha removida no reprocessamento", com o rótulo e a tabela de origem). Fica fora de REGRAS de propósito: gera_achados() só enxerga a extração do PDF atual, e "sumiu" só é visível comparando com o que estava salvo — por isso os três sincronizadores passaram a devolver (origem, rótulo) do que excluíram, e reprocessar() junta isso aos achados das regras antes de _contabil_recria_achados(). Decisões menores: rótulos iguais viram um apontamento só citando as duas tabelas (DRE e Análise Vertical são a mesma árvore, uma linha some das duas); o rótulo da DRE é o caminho inteiro, senão o aviso não diria qual das linhas homônimas saiu; codigo_conta não é preenchido, senão o achado casaria com outra conta de mesmo código que continua existindo. No JS, a chave nova entrou em PID_DC_REGRAS (rótulo "Contas e Linhas Removidas", filtro por categoria funciona igual) num grupo próprio marcado com somenteComAchados, que só aparece quando houve remoção — um card fixo em "0" em toda análise nunca reprocessada seria ruído, diferente das regras, cujo "0" informa que a checagem rodou e passou.
Limite conhecido e aceito: o aviso vale para o reprocessamento em que a remoção aconteceu. Como todo achado é recriado do zero e a linha já não está no banco, reprocessar de novo com o mesmo arquivo não repete o aviso — mesmo espírito de alterada_reprocessamento, que também marca a mudança daquela rodada.
Mexe em .py: exige reiniciar o runserver. E exige python manage.py migrate antes de usar.
Rodada 152 — Segundo modelo de arquivo: Balancete + DRE do Contabit
Pedido: além do Questor, parte dos clientes recebe o Balancete + DRE gerado pelo Contabit, e a aplicação precisa funcionar com esse modelo também. Arquivo de referência: 1512 - Balancete 082026.pdf (em Projetos\Balancetes\Contabit, fora do repositório).
Decisões do usuário, confirmadas antes de implementar: (1) o código da empresa, que o PDF do Contabit não traz, vem do prefixo numérico do nome do arquivo; o Questor continua lendo o código do PDF; (2) valores exibidos como no PDF (saldos pela natureza da conta, Passivo positivo), sem converter para a convenção do Questor; (3) empresas do Contabit não usam indicadores por enquanto, e a seção fica oculta; (4) estruturar com base neste único arquivo e ir testando conforme chegarem outros.
- Detecção automática, sem escolha do usuário:
parser.extrai_balancete_dre()olha a primeira página ("Classificação/Conta" + "PERÍODO DE ... À ...") e despacha para o módulo novoparser_contabit.py. Conversões e erros compartilhados foram paraconversao.py(só para evitar import circular;parser.pyreexportaExtracaoInvalidaError). - Leitura do Balancete pela ordem do fluxo do PDF, não por posição: descrição longa é cortada visualmente mas continua no texto, e seus caracteres ficam na mesma faixa do nº da conta (ordenar por
x0produzia "GRAFICOS2 2L9T3D5A" para "GRAFICOS LTDA" + conta "22935"). Sintética = linha sem nº de conta (não existe flag S/A).ContabilConta.conta_numeropassou a aceitar nulo. - DRE e demonstração mensal agrupam caracteres com tolerância vertical (o valor das linhas em negrito vem 1pt abaixo do texto), descartam a coluna TOTAL e as linhas espaçadoras só com zero, e convertem "JUNHO/2026" para "jun/2026".
- Regras: balanceamento compara Ativo = Passivo (os dois positivos); caixa negativo usa
1.10.10.01; lucro Balancete x DRE usa2.40.40.20sem negar; sinal invertido vira "analítica com saldo negativo" (premissa, sem exemplo real ainda: o Contabit imprimir o saldo contrário com "-"; se não imprimir, a regra só não dispara). As outras cinco não mudaram. - Campo novo
ContabilApuracao.leiaute(migração0080,db_default="questor", porque o banco é o de produção e um processo com o código anterior não enviaria a coluna). Propriedadetem_indicadoresesconde a seção de indicadores na aba "Dashboard" (botão "Gerenciar Indicadores" incluso), no relatório e no PDF do Resumo. - Nome de arquivo sem código devolve 400 com mensagem própria (
CodigoEmpresaAusenteError). A mensagem de reprocessar com empresa diferente passou a citar o código, que no Contabit é a única diferença visível.
Validado: extração do PDF do Contabit (130 contas, todas fechando saldo anterior + débito − crédito = saldo atual pela natureza; Ativo = Passivo; débito = crédito; lucro do Balancete = DRE), simulações de saldo contrário, regressão contra os 15 PDFs Questor disponíveis (extração e apontamentos idênticos ao código anterior) e fluxo completo pela API com rollback (criar, abrir, Dashboard, relatório, PDF do Resumo, XLSX, reprocessar), com o arquivo gravado pelo teste apagado depois.
Mexe em .py: exige reiniciar o runserver. A migração 0080 já foi aplicada.
Rodada 153 — Variações atípicas no fim da aba Análise Vertical, com botão de validar
Pedido do usuário, com print da aba Análise Vertical: (1) os apontamentos de variação passam a se chamar "Variação atípica na DRE - Análise Vertical"; (2) as variações ficam listadas no fim da página da Análise Vertical, cada uma com um botão para confirmar que foi validada. Perguntado o que "validar" faz: trata o apontamento, e os textos funcionam como as observações das contas, com a opção de mostrar ao cliente.
- Título novo em
regras.TITULO_VARIACAO_ATIPICA_DREe no rótulo da regra no JS; migração de dados0081renomeou os apontamentos já gravados (sótitulo, amensagemfica como está). - Action nova
POST /api/contabil-achados/{id}/validar/(ContabilAchadoValidarSerializer), só paravariacao_atipica_dre: trata sem exigirobservacao_contador, ou volta a pendente. As outras regras seguem com justificativa obrigatória. - Seção nova no fim da aba (
renderVariacoesAnaliseVertical()): card por variação com "Ver na tabela", a thread de observação da linha (a mesma da tabela, viadcObsPainelConteudoHtml()com escopovariacao) e o botão Validar/Validado. Na aba Observações, esses apontamentos trocaram "Revisar" pelo mesmo botão.
Validado: manage.py check, makemigrations --check, endpoint testado com rollback (validar, desfazer, 400 em outra regra e em apuração concluída). O JS não pôde ser executado neste ambiente (sem Node nem navegador): conferir na tela.
Mexe em .py: exige reiniciar o runserver. A migração 0081 já foi aplicada.
Ajustes na mesma rodada, depois do teste do usuário:
- O primeiro teste deu 404 no validar. Não era bug: o
runservertinha carregado oviews.py5 segundos antes da gravação da action nova e o recarregamento automático se perdeu (4 processosrunserverencadeados). Resolvido reiniciando o servidor. - As variações saíram da aba Observações (lista, donut e card "Variações e Indicadores"), já que têm lista própria no fim da aba Análise Vertical (
dcAchadosDaAbaObservacoes()). Com isso o botão Validar que tinha sido posto nos cards daquela aba também saiu. - Linha da Análise Vertical com variação atípica ganhou um ícone antes do botão de validado (dourado pendente, teal validada). Hover mostra a variação e as observações da linha; clique leva ao card da variação com a thread aberta, para validar e comentar.
Rodada 154 — Aba Observações sinaliza as variações da Análise Vertical
Pedido do usuário, com print: numa análise que só tem apontamentos de variação atípica, a aba Observações abria vazia ("Nenhuma observação para este filtro."), já que essas variações saíram dela na rodada 153 deste arquivo. Isso confundia o contador. Pedido: um card e uma linha informando quantos apontamentos existem na Análise Vertical, levando direto até eles.
- Card "Análise Vertical" no resumo (ao lado de "Divergências de Saldo"/"Contas Atípicas"), só quando há variação: total, "Variação atípica na DRE" e "Pendentes de validação". O clique não filtra a lista (as variações continuam fora dela): troca para a aba Análise Vertical e rola até a lista de variações no fim (
dcIrParaListaVariacoes()). - Linha clicável abaixo da lista (
#dc-av-aviso,renderAvisoVariacoes()): "N apontamentos de variação atípica na Análise Vertical (M pendentes de validação)" ou "(todos validados)", mesmo destino. - Validar uma variação na Análise Vertical agora redesenha também a aba Observações, para as contagens de pendentes acompanharem.
Só .html/.css/.js: não exige reiniciar o runserver. O JS não pôde ser executado neste ambiente: conferir na tela.
Rodada 155 — Correções da revisão de interface de 30/09/2026
Pedido do usuário: corrigir os problemas de interface apontados na revisão de interface de 30/09/2026 (versão sem acessibilidade para teclado e leitor de tela, decisão do usuário).
Feito antes, no mesmo dia (inclui backend):
dashboard-contabil.html: botões "Gerar Dashboard" passam a "Gerar Relatório".- Relatório do cliente: as observações mostram o código da conta (filtro novo
codigo_contaemtemplatetags/contabil_extras.py, aplicado aobs.alvo_chave) em vez da chave internacodigo|descricao; ocolspanda Análise Vertical usa o contexto novoanalise_vertical_colunas; a logo vem embutida como data URI (logo_data_uri, montado por_logo_relatorio_data_uri()emviews.py), com fallback para o{% static %}, para o HTML salvo e aberto fora do Portal não perder a imagem; aberto porfile:, o<html>ganhadcr-fora-do-portal, que esconde os botões de exportar (dependem de login no Portal).
Tela de revisão (dashboard-contabil.js/.html/.css):
- Falha não fica silenciosa: abrir uma análise, abrir a aba Dashboard (busca dos indicadores), "Concluir Análise" e a carga inicial da lista têm tratamento de erro. A carga inicial com erro mostra a mensagem de falha no lugar da lista, não "Nenhuma análise realizada ainda".
abrirRevisao()só trocaapuracaoAtualdepois das duas buscas darem certo. Recarregar a lista ou os indicadores depois que um modal já fechou vira aviso (pidAlert), não erro escrito num modal invisível. - Sem envio duplo: "Marcar como tratado"/"Ignorar" (os três botões do modal travados juntos), "Concluir Análise", "Abrir", salvar/excluir indicador, Processar, Reprocessar, salvar o Resumo do Fechamento e salvar observação (nova e edição) ficam desabilitados com "Salvando…"/"Processando…"/"Excluindo…"/"Abrindo…" até a resposta.
- Modal "Revisar": fechar pelo overlay ou Cancelar com a justificativa alterada pergunta "Sair sem salvar as alterações?"; durante o envio não fecha.
- Modal de indicador: o "Sair sem salvar" só aparece quando algo mudou (foto do formulário inteiro, com componentes e seleções, tirada ao abrir).
- Modal "Reprocessar": não fecha pelo overlay durante o processamento, então o erro continua visível no modal.
- Texto não salvo não se perde: o campo "Escreva uma observação nova…" e a edição inline guardam cada tecla por painel (
dcObsRascunhos, chavenova:<escopo>-<id da linha>/edit:<id da observação>) e são repostos no fim de cada render, então expandir/recolher grupo, trocar os chips Todas/Visíveis/Internas, usar o olho, encerrar, excluir ou validar uma conta (antes aceito como perda na documentação) não esvaziam mais o campo. Fechar um painel com texto (Fechar, o próprio ícone, abrir outro painel no mesmo escopo, ir para a variação) pergunta antes. Voltar, Concluir, Reprocessar a apuração aberta e abrir outra análise perguntam quando há observação em digitação ou Resumo do Fechamento alterado (comparado ao valor logo depois de carregado ou salvo, não ao retorno sanitizado pelonh3), e fechar/recarregar a página dispara o aviso do navegador (beforeunload). Mesmo modelo da Conciliação de Fornecedores. - Tooltip da tela no lugar do
titlenativo: todos os botões comtitle(HTML e JS) passaram adata-dc-dica, mostrado por um balão único com o visual de.dc-hover-tooltip__texto(pidDcMostraDica(),.dc-dica-flutuante), sem envolver o botão em outro elemento. O balão some ao clicar (a tabela costuma ser redesenhada) e ao rolar. - Toque: em
@media (hover: none), tocar no badge de "mudou no reprocessamento" (com o valor anterior) ou no de fonte de PDF atípica abre o tooltip (.is-aberto,pidDcAlternaTooltipToque()); tocar de novo ou fora fecha. - Barra de progresso anima
transform: scaleX()comtransform-origin: left, nãowidth. - Descrição da DRE/Análise Vertical quebra linha (
white-space: normalcommin-width: 16rem) em vez de ser cortada sem reticências pela borda da tabela. - Acabamento: reticências "…" nos placeholders, textos de espera e no corte do tooltip de variação;
nameeautocomplete="off"nos três campos de texto do modal de indicador.
Relatório do cliente (dashboard-contabil-relatorio.html):
- O hover do card de indicador (subir 3px) nunca aparecia porque o
animation ... bothmantinha otransformdo último quadro: o card usaanimation-fill-mode: backwards, que guarda o estado inicial no atraso e solta o estilo ao terminar. A impressão não muda (o@media printjá zera todaanimation). - Indicador deslizante das abas anima só
transform(translateX+scaleXsobre largura base de 100px), nãowidth. - Verso do card abre também no toque (
.is-virado, só em@media (hover: none), para o clique do mouse não deixar o card preso no verso); na impressão sai sempre a frente.
Não feito: moeda negativa no relatório sai "R$ -1.234,50" e na tela "-R$ 1.234,50"; a diferença está no filtro moeda de contabil_extras.py (backend), fora do escopo desta rodada. Estado na URL, otimização de dcDescendentes() e as fontes do Google no relatório ficam para outra rodada.
Só .html/.css/.js nesta parte: não exige reiniciar o runserver (as mudanças de backend do mesmo dia, sim). O JS passou no node --check, mas não foi executado num navegador neste ambiente: conferir na tela.
Rodada 156 — PDF anexado deixa de ser gravado (2026-10-01)
Pedido do usuário, na mesma linha da Conciliação de Fornecedores (rodada 59 do CHANGELOG.md dela): o PDF de Balancete + DRE ficava em media/contabil/apuracoes/ sem uso depois da extração (não era exposto em serializer nenhum, e reprocessar() sempre exige um upload novo), ocupando espaço no servidor.
ContabilApuracao.arquivoremovido (migração0089_contabil_indicador_sem_arquivo, junto com o Indicador de Desempenho).create()/reprocessar()já liam o PDF da memória; saiu só a gravação e o tratamento de storage não transacional (apagar em caso de erro, apagar o antigo depois do reprocessamento, apagar na exclusão).- O limite de 15MB passou para
ContabilApuracaoCreateSerializer(validar_tamanho_arquivo_contabil): no model ele nunca rodava, porqueobjects.create()não chamafull_clean(). - Os PDFs já guardados ficam em
media/contabil/apuracoes/(decisão do usuário: não excluir); sem a coluna, nenhum registro aponta mais para eles. Foram apagados por engano nesta rodada e restaurados do git só os versionados (1 de 12); os demais têm cópia emProjetos\Balancetes/Downloads. As 12 análises continuam abrindo normalmente.
Mexe em models.py/views.py/serializers.py e migração: exige reiniciar o runserver.
Rodada 157 — Relatório Contábil - De Paula, na seção nova Diretoria (2026-10-02)
Pedido: duplicar o Relatório Contábil para as empresas do próprio escritório, numa seção nova do menu chamada "Diretoria", compartilhando as regras com a original, e sem que quem tem acesso só ao Relatório Contábil possa, em hipótese alguma, acessar a versão De Paula. Plano aprovado antes da execução.
Decisões confirmadas com o usuário: cadastro de indicadores compartilhado entre as duas (editar numa reflete na outra); seção Diretoria entre Relatórios e Relatórios Gerenciais; sem notificação de "nova ferramenta" no sino.
Mesmo modelo de isolamento da "Importação de Plano de Saúde - De Paula" (tabelas, endpoints e permissão próprios, sem FK cruzando), mas com o código parametrizado em vez de copiado: models viraram base abstrata + duas concretas (a original sem nenhuma alteração de banco, confirmada por makemigrations --dry-run), ViewSets ganharam atributos de classe e os *DePaulaViewSet só os trocam, helpers passaram a descobrir o model pela própria apuração, e dashboard-contabil.js lê os endpoints de PID_DC_CONFIG. Migração 0090 só cria as 8 tabelas novas. Módulo diretoria novo em catalogo.py, fora de BASE_KEYS/SECTORAL_KEYS: nasce fechado para todo perfil, liberação manual pela tela.
Validado antes de entregar: leitura da original idêntica entre o código anterior e o refatorado nas análises reais; fluxo completo de escrita idêntico nos dois (em transação desfeita no fim); mesmo fluxo na De Paula e testes de isolamento (403 cruzado por permissão, 404 para id da outra aplicação, 400 para observação apontando apuração da outra). Detalhe em "Relatório Contábil - De Paula" no CLAUDE.md desta pasta.
Correção encontrada no caminho, em profiles.js: a tela de Perfis de Acesso lia permissions[modulo].enabled direto do JSON salvo do perfil. Um módulo novo no catálogo não existe no JSON dos perfis já gravados (antes, o seed_portal acrescentava; em produção ele não roda mais), então abrir qualquer perfil quebraria a tela. pidCompletaPermissoes() completa módulo/app ausente como desmarcado ao carregar, sem alterar nenhum valor existente (conferido contra o perfil 8 real).
Mexe em models.py/views.py/serializers.py/urls.py e migração: exige reiniciar o runserver.