# Relatório Contábil (Relatórios > Contabilidade)
> Movido do `CLAUDE.md` da raiz — este arquivo é carregado automaticamente ao trabalhar dentro de `portal_api/dashboard_contabil/`. Ver `CLAUDE.md` na raiz para arquitetura geral/transversal do Portal (modelo de permissões, API, CSS, etc.).
**Renomeado de "Dashboard Contábil" pra "Relatório Contábil"** numa rodada específica — pedido explícito do usuário, pra soar como um aliado do trabalho do contador em vez de mais um processo/sistema novo. Rename **só de rótulo visível**: menu (`catalogo.py`), `
`/``/cabeçalhos de `dashboard-contabil.html` e `dashboard-contabil-relatorio.html`, o botão que gera o relatório ("Gerar Relatório", antes "Gerar Dashboard") e `verbose_name`/`verbose_name_plural` dos models no admin. **Nada técnico mudou**: a pasta continua `dashboard_contabil/`, os arquivos continuam `dashboard-contabil.*`, as classes continuam `Contabil*`/`IndicadorContabil*`, as rotas continuam `/api/contabil-*`/`/api/contabil-apuracoes/{id}/dashboard/`, a permissão continua `apps["dashboard-contabil"]`. Não seguir esse rename pra dentro do código — só onde o texto é literalmente visível ao usuário (ou documentação, como este arquivo).
Ferramenta que otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (processo descrito em `ITD-FISCO-7513`, "Roteiro de Conferência de Balancete"): o contador anexa o PDF de Balancete + DRE de uma empresa (mesmo relatório modelo Questor hoje enviado ao cliente), a ferramenta extrai as contas/linhas e roda um conjunto de checagens automáticas de auditoria, apresentando os achados numa tela de revisão onde o contador analisa, registra observações e conclui a análise. Permissão de **toggle único** (`apps["dashboard-contabil"]` em `permissoes["relatorios"]`, subgrupo "Contabilidade"), checada via `PermissaoApp("relatorios", "dashboard-contabil")` em todos os `ModelViewSet` relacionados. **Nasce restrita só ao perfil "Inovação"** (código 8) — mesmo padrão de "Não Conformidades" (ver override em `seed_portal.py`), já que expõe balancete/DRE completos dos clientes.
O botão "Gerar Dashboard" (relatório final em HTML para o administrador da empresa) já está implementado — ver "Relatório 'Gerar Dashboard'" abaixo, que inclui exportação de Balancete/DRE em XLSX a partir dele (`exportacao.py`, ver "Exportação em XLSX"). **Ainda fora de escopo**: consolidação entre várias empresas/competências ao mesmo tempo (o BI Contábil externo que este Dashboard substitui tem filtros "Ano-Mês"/"Empresa-Filial" que sugerem isso, mas o escopo confirmado com o usuário é sempre uma apuração por vez — ver seção própria).
## Decisões de escopo (confirmadas com o usuário)
- **Entrada: só PDF.** O Questor também exporta Balancete/DRE em XLSX estruturado (mais confiável de extrair, sem o risco de parsing de texto), levantado como alternativa — o usuário optou por manter só PDF, como pedido originalmente. Não há suporte a XLSX nesta ferramenta.
- **Histórico para variação mês a mês fica no próprio Portal**, não depende do relatório "Análise Comparativa Mensal"/"Acompanhamento Mensal" (que é só um relatório complementar, derivado do balancete, enviado ao cliente separadamente — não é upload desta ferramenta). Cada apuração processada fica salva (`ContabilApuracao`, chave natural `codigo_empresa`+`competencia`), e as regras de variação comparam contra as apurações anteriores da mesma empresa já no banco.
- **Escopo das regras**: só o que é derivável do próprio balancete/DRE anexado — nenhuma checagem do ITD que dependa de sistemas externos (Questor, extratos bancários, folha de pagamento, PID legado). A ferramenta é analítica ("Auditoria de Balancetes" em Auditorias > Fisco/Contábil, hoje só um placeholder `href="#"` no menu, referencia o antigo sistema PID legado — **não confundir com este Dashboard Contábil**, são coisas diferentes), não substitui as etapas operacionais do roteiro (zeramento de saldos etc.).
## Extração do PDF (`parser.py`)
O relatório Questor de Balancete + DRE tem uma particularidade de renderização que quebra a extração padrão do pdfplumber: **cada caractere do texto real é desenhado em posição própria** (sem kerning) e, por baixo dele, **o PDF inclui uma grade densa de caracteres de espaço cobrindo toda a largura de cada linha** — um artefato do gerador de relatório, não intencional para leitura. Isso faz `page.extract_words()`/`page.extract_text()` do pdfplumber tratarem esses espaços "de fundo" como separadores de palavra reais, quebrando números em dígitos isolados (ex.: "34.245.469,57" vira uma sequência de tokens `'3'`, `'4'`, `'.'`, `'2'`...).
`_reconstroi_linhas()` contorna isso trabalhando direto com `page.chars`: ignora todo caractere de texto igual a `" "` e reconstrói cada linha a partir da posição real (`x0`/`x1`) dos caracteres não-espaço, reinserindo um único espaço só quando o vão horizontal entre dois caracteres consecutivos ultrapassa `_GAP_ESPACO` (0.8pt) — calibrado contra `792 - balancete 072026.pdf` (arquivo de referência do usuário, salvo fora do repositório em `Projetos\Balancetes`): o vão dentro de uma palavra/número é ~0, entre duas palavras da mesma descrição é ~1.7-1.9pt, e entre campos da tabela (conta → flag S/A → código → descrição, ou entre colunas de valor) é sempre ≥5pt. Essa reconstrução foi validada rodando de fato contra o PDF real antes de escrever o parser definitivo (nunca desenhar regex só de texto colado — mesmo cuidado documentado na skill `importacao-plano-saude`).
- **Balancete**: cada linha casa com `_RE_LINHA_BALANCETE` (`^(conta)\s+(S)?\s*(código)\s+(resto)$`), e os últimos 4 tokens monetários de `resto` (via `_RE_MONETARIO`) são Saldo Anterior/Débito/Crédito/Saldo Atual, nessa ordem — o texto antes deles é a descrição. `tipo` é `"S"` (sintética) quando o flag aparece, `"A"` (analítica) quando não.
- **DRE**: cada linha é descrição + um único valor final (sem código de classificação, diferente do Balancete). `nivel` (indentação) é derivado do `x0` do primeiro caractere da linha, em relação ao menor `x0` visto na seção (a raiz, nível 0); `totalizador` é `True` quando algum caractere da linha usa fonte em negrito (`fontname` contendo `"bold"`, case-insensitive) — confirmado contra o PDF real: linhas como "RECEITA OPERACIONAL BRUTA"/"(-) CUSTOS TOTAIS"/"(=) LUCRO BRUTO" usam `Times-Bold`, as demais `Times-Roman`.
- **Extração para no início da seção "Demonstração Mensal (Análise Vertical)"** (páginas finais do mesmo PDF, quando presentes) — de propósito: o histórico próprio do Portal cobre a mesma necessidade de forma mais confiável (qualquer competência anterior já processada, não só as últimas 3 meses que aquele relatório mostra).
- `LINHA_DRE_RECEITA_LIQUIDA`/`LINHA_DRE_CUSTOS_TOTAIS` (constantes em `parser.py`) guardam o texto exato dessas duas linhas totalizadoras (**com** o prefixo `"(=) "`/`"(-) "` que o Questor imprime) — usadas por `regra_percentual_custo_receita_atipico` pra achar a linha certa por texto, mais robusto a pequenas variações de nível/indentação entre empresas do que confiar só no negrito.
- `extrai_balancete_dre(origem)` aceita tanto um caminho em disco quanto um arquivo já aberto em memória (`io.BytesIO`) — a view chama isto **antes** de salvar qualquer coisa no banco, já que `codigo_empresa`/`competencia` (a chave natural da apuração) só são conhecidos depois de ler o PDF, não informados pelo usuário no upload (diferente de `IndicadorApuracao`, que recebe a competência como campo do formulário).
## Regras de auditoria v1 (`regras.py`)
Cada `regra_*` é uma função pura: `(ResultadoExtracao da apuração atual, list[SnapshotHistorico]) -> list[AchadoDetectado]`. `historico` vem ordenado da apuração mais recente pra mais antiga (só as últimas 2 já persistidas da mesma empresa, resolvidas por `_contabil_monta_historico()` em `views.py`).
1. **`balanceamento_ativo_passivo`** (alta) — soma do grupo Ativo (`codigo="1"`) deve fechar com a do Passivo (`codigo="2"`, já vem negativo no relatório).
2. **`debito_credito_divergente`** (alta) — soma de Débito das contas-raiz (`codigo` sem ponto, ou seja só "1" e "2") deve bater com a soma de Crédito. **Não é uma checagem trivial de "todo balancete sempre bate"**: como a DRE (Resultado) não tem colunas de débito/crédito próprias neste relatório (só um valor líquido por linha), a identidade só fecha porque a movimentação de Resultado também transita pelas contas de Patrimônio Líquido do Passivo (ex.: "LUCROS/PREJUÍZOS DO EXERCÍCIO") — confirmado empiricamente contra `792 - balancete 072026.pdf` (débito total = crédito total = R$ 416.271.243,32 nas contas-raiz).
3. **`saldo_negativo_caixa`** (alta) — conta com `codigo` começando em `1.01.01.001` (grupo Caixa) e `saldo_atual < 0`.
4. **`conta_transitoria_com_saldo`** (média) — descrição contém "TRANSIT" (cobre "TRANSITÓRIA"/"TRANSITORIA") com `saldo_atual != 0`.
5. **`conta_deveria_zerar`** (média) — `TRECHOS_CONTA_DEVERIA_ZERAR` (lista curta e deliberadamente restrita — só "ADIANTAMENTOS DE SALÁRIOS", que o ITD confirma dever ficar zerada todo mês; **não** inclui "Adiantamento de Férias"/"13º Salário", que legitimamente carregam saldo entre meses).
6. **`saldo_sinal_invertido`** (média) — conta analítica (`tipo="A"`) do Ativo (`1.`) com saldo credor, ou do Passivo (`2.`) com saldo devedor, exceto contas redutoras (descrição começando com `"(-)"`, que são esperadas ter o sinal oposto ao grupo).
7. **`descricao_generica`** (baixa) — descrição exatamente `"DIVERSOS"` com saldo relevante (o ITD cita esse caso especificamente: "o contador deverá realocar estes lançamentos a conta pertinente").
8. **`variacao_atipica_saldo`** (média) — compara `saldo_atual` de cada conta analítica contra a apuração anterior da mesma empresa (por `codigo`), sinaliza quando a variação passa de `VARIACAO_LIMIAR_PERCENTUAL` (50%) **e** `VARIACAO_VALOR_MINIMO` (R$1.000, evita ruído em contas de valor irrisório). Só roda a partir da 2ª apuração de uma empresa.
9. **`variacao_atipica_dre`** (média) — mesma ideia, mas isolando o mês: como a DRE do relatório é **acumulada desde janeiro**, `_isola_mes_dre()` subtrai o acumulado do mês anterior (`valor_ytd_atual - valor_ytd_anterior`), exceto em janeiro (onde o acumulado já é o próprio mês). Só roda quando há **2** apurações anteriores consecutivas (`_mes_consecutivo()`) — precisa isolar tanto o mês atual quanto o anterior pra comparar mês-contra-mês de verdade, não acumulado-contra-acumulado.
10. **`percentual_custo_receita_atipico`** (média) — compara o percentual Custos/Receita Líquida do mês isolado atual contra o do mês isolado anterior (mesma técnica da regra 9) — o ITD é explícito que não existe parâmetro geral entre empresas pra essa relação, só comparação histórica da própria empresa.
Regras 8-10 não geram achado nenhum na primeira apuração de uma empresa (sem histórico ainda) — comportamento esperado, não bug.
## Models (`portal_api/models.py`)
Padrão cabeçalho → linhas de detalhe → achados (mesma filosofia de `IndicadorApuracao`/`IndicadorApuracaoColaborador`):
- **`ContabilApuracao`**: `codigo_empresa`/`nome_empresa`/`cnpj`/`competencia` (extraídos do PDF, não informados no upload) + `periodo_inicio`/`periodo_fim` + `arquivo` + `status` (`revisao`/`concluida`). `unique_together` em `codigo_empresa`+`competencia` — reprocessar a mesma competência de uma empresa exige excluir a apuração antiga primeiro (sem "reabrir"/reprocessar nesta v1, diferente de `ImportacaoPlanoSaude`).
- **`ContabilConta`**: uma linha do Balancete. `observacao` (`TextField`, editável via PATCH em **qualquer** conta, tenha ela gerado achado ou não) — é o espaço de "análise" pedido pelo usuário, independente da auditoria automática. `oculta_no_relatorio` (default `True`, invertido numa rodada — ver "Editor de observação inline" abaixo) e `validado` (`BooleanField`, default `False` — checkbox informativo de "já conferi esta conta", sem efeito em achado/observação/relatório).
- **`ContabilLinhaDre`**: uma linha da DRE, sem código de classificação (o relatório não traz um pra DRE, diferente do Balancete). Mesmos `oculta_no_relatorio`/`validado` de `ContabilConta`.
- **`ContabilAchado`**: achado de auditoria, nasce automático em `create()`, nunca é apagado — só muda de `status` (`pendente`/`tratado`/`ignorado`), sempre com `observacao_contador` obrigatória ao mudar de pendente (mesmo espírito de "histórico completo preservado" de `ImportacaoPlanoSaudeAuditoria`). `conta` é nullable — achados 1 e 2 (balanceamento/débito-crédito) são gerais, sem uma conta específica.
## `ContabilApuracaoViewSet.create()` — ordem de operações não-trivial
Diferente de `IndicadorApuracaoViewSet`/`ImportacaoPlanoSaudeViewSet` (onde a chave natural do registro, ex. `competencia`, vem do formulário do usuário), aqui `codigo_empresa`/`competencia` só são conhecidos **depois** de extrair o PDF. A ordem em `create()`:
1. Lê o arquivo inteiro pra memória (`arquivo.read()`) — nada em disco ainda.
2. `dashboard_contabil_pipeline.processa_apuracao(io.BytesIO(conteudo), _contabil_monta_historico)` — extrai o cabeçalho/contas/DRE e, com o `codigo_empresa`/`competencia` já em mãos, chama `_contabil_monta_historico()` (função injetada, consulta o ORM) pra buscar até 2 apurações anteriores da mesma empresa, então roda as regras. Captura `ContabilExtracaoInvalidaError` → 400 genérico.
3. Confere se já existe uma apuração pra essa empresa+competência (`.exists()`) → 400 com mensagem específica, **antes** de qualquer escrita (evita depender só do `IntegrityError` do banco, que devolveria um 500 cru).
4. Só agora, dentro de `transaction.atomic()`, cria `ContabilApuracao` (grava o arquivo via `ContentFile(conteudo, ...)`) + `bulk_create` de contas/linhas DRE/achados. `except Exception` fora do `with` apaga o arquivo gravado se algo falhar no meio (upload de `FileField` não é transacional).
`pipeline.processa_apuracao(origem, busca_historico)` recebe `busca_historico` como uma função (não uma lista já pronta) exatamente por essa dependência: a chave de busca do histórico só existe depois da extração, então não dá pra pré-buscar antes de chamar o pipeline como as outras duas ferramentas fazem.
## Frontend
`templates/dashboard-contabil.html` (`page-content--wide`) segue o padrão de 3 sub-views de `indicador-desempenho.html`: `#dc-list-view` (histórico + botão "Nova Análise") / `#dc-form-view` (upload de um único PDF — sem campo de competência, é extraído do arquivo) / `#dc-review-view` (abas Achados/Balancete/DRE, via `.pa-tabs`/`.pa-tab-panel` de `perfis-acesso.css`). Achados têm filtro por severidade e por status (pendentes/todos); tratar/ignorar um achado abre um modal próprio (`#dc-achado-modal`) que exige observação não-vazia. Observação de conta/linha da DRE **não** abre mais modal nenhum — editor inline na própria tabela, ver "Editor de observação inline" abaixo. Botão "Gerar Relatório" (`#dc-gerar-dashboard-btn`, id não mudou) chama `pidGerarDashboardContabil()` — ver "Relatório 'Gerar Dashboard'" abaixo.
**Balancete e DRE usam a mesma árvore recolhível** (`dashboard-contabil.js`): o Balancete já construía uma árvore expansível a partir do nível de indentação derivado do código de classificação (`dcContaNivel()`, contando segmentos separados por `.`) — a DRE não tem código de classificação (ver "Extração do PDF" acima), mas já carregava `nivel` pronto do backend (`ContabilLinhaDre.nivel`, derivado do `x0` de cada linha no PDF), então `renderDre()` reaproveita exatamente o mesmo algoritmo de `renderContas()` (pilha de níveis recolhidos, "tem filhos" = a próxima linha tem nível maior) só que sobre `linha.nivel` direto, sem precisar de um `dcContaNivel` equivalente. Reaproveita as mesmas classes CSS do toggle (`.dc-conta-toggle`/`.dc-conta-toggle-spacer`/`.dc-conta-desc-cell`, `dashboard-contabil.css`) — o nome genérico ("conta") já cobre as duas árvores, não precisou de classe nova. Estado de colapso é independente por aba (`dcContasColapsadas`/`dcDreColapsadas`, dois `Set()`).
**Nasce recolhida a partir do 3º segmento do código** (pedido explícito do usuário, pra reduzir a poluição visual de uma apuração com muitas contas analíticas — limiar ajustado numa rodada seguinte, ver abaixo): em vez de `renderRevisao()` reiniciar os dois `Set()` vazios (tudo expandido), `dcColapsoPadrao(itens, nivelFn)` os pré-popula com os ids de todo item que tem filhos **e** está no nível `PID_DC_NIVEL_ABERTO_PADRAO` (`2`) ou além — ex. a conta `1.01.01` (3 segmentos, nível 2) aparece aberta, mas seus filhos (`1.01.01.001`, nível 3) ficam ocultos até o contador clicar pra expandir; se expandido, um filho de nível 3 que também tenha netos nasce recolhido de novo pelo mesmo critério, então "nível 3 em diante" precisa sempre de um clique a mais, não só a primeira camada. Mesmo limiar aplicado à DRE, sobre `Math.max(0, linha.nivel)` — decisão confirmada com o usuário (a princípio o pedido citava só o Balancete, mas como a DRE reaproveita o mesmo algoritmo, o mesmo comportamento faz sentido nela também). O relatório "Gerar Dashboard" replica esse mesmo estado inicial (ver abaixo) — não é só a tela de revisão. `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (`views.py`) são a mesma constante conceitual duplicada nos dois lados (um é SPA, o outro HTML renderizado uma vez) — mudar o limiar exige ajustar os dois.
**Destaque acompanha o último grupo expandido** (pedido explícito do usuário, mesma rodada do ajuste de limiar acima): ao expandir uma conta/linha, as linhas que acabaram de ficar visíveis (só os filhos **diretos**, não os netos — que continuam recolhidos pelo limiar acima) ganham um realce dourado (`.dc-conta-row--destaque`); se o contador expandir uma dessas linhas em seguida, o realce se move pros filhos dela, e o grupo destacado antes volta pra cor padrão — nunca acumula mais de uma "leva" destacada ao mesmo tempo. `dcContasDestaque`/`dcDreDestaque` (dois `Set()`, um por árvore, resetados em `renderRevisao()`) guardam só a leva mais recente; `dcFilhosDiretos(itens, nivelFn, id)` (função genérica, reaproveitada pelas duas árvores) varre a lista plana a partir do índice do item expandido e para no primeiro item de nível igual ou menor (fim do grupo), coletando só os de nível exatamente `pai+1`. O handler de clique do toggle decide o destino do destaque **antes** de checar se a ação foi expandir ou recolher (`estavaColapsada = colapsadas.has(id)` guardado antes de mutar o `Set`): expandir substitui `dcContasDestaque`/`dcDreDestaque` pelos filhos diretos recém-revelados; recolher só limpa (`new Set()`), já que nada novo foi exposto. Puramente visual, sem persistência — reseta a cada apuração aberta, sem afetar nenhum dado gravado.
**Card de achado expande a conta usada no apontamento** (`renderAchados()`, pedido explícito do usuário): `achado.conta` (id, já vem no payload de `/api/contabil-apuracoes/{id}/`, junto de `conta_codigo`/`conta_descricao`) é resolvido pra objeto completo procurando em `apuracaoAtual.contas` (`.find((c) => c.id === achado.conta)`) — sem chamada de API extra, já que a apuração inteira (contas + linhas de DRE + achados) já vem de uma vez só nesse endpoint. Só achados vinculados a uma conta específica ganham o botão "Ver conta usada no apontamento" (`.dc-achado-card__toggle-conta`) — as duas regras gerais (`balanceamento_ativo_passivo`/`debito_credito_divergente`, `conta` nulo no model) não têm uma conta única por trás, então não mostram o toggle. Expandido, mostra código/descrição/saldo anterior/débito/crédito/saldo atual da conta (`.dc-achado-card__conta`, um `` em grid). Estado de expansão (`dcAchadosContaExpandida`, um `Set()` de ids de achado) segue o mesmo padrão de `dcContasColapsadas`/`dcDreColapsadas` — reiniciado em `renderRevisao()`.
**Lista de Observações ordenada por severidade** (pedido explícito do usuário): `achadosFiltrados()` filtra e depois ordena (`PID_DC_SEVERIDADE_ORDEM = {alta: 0, media: 1, baixa: 2}`) — Alta sempre primeiro, Baixa por último, preservando a ordem original (ordem em que as regras rodaram) dentro de uma mesma severidade, já que `Array.prototype.sort` é estável. Puramente ordenação de exibição no frontend, nada mudou no backend/model.
**Resumo clicável (donut por severidade + cards por grupo temático) no topo da aba Observações** (`.dc-achados-resumo`, `dashboard-contabil.html`/`.css`/`.js`, pedido explícito do usuário, inspirado numa tela de auditoria de outro sistema — ver rodadas 97/98 do `CHANGELOG.md`): acima dos chips de filtro, um donut em SVG puro (sem Chart.js — essa dependência só existe no relatório estático "Gerar Dashboard", não faz sentido carregar aqui numa tela interativa pequena) com a quantidade de observações por **severidade** (Alta/Média/Baixa, mesmas cores dos badges — `--danger`/`--gold`/`rgb(var(--slate-rgb))`) e o total no centro, mais uma grade de cards. `PID_DC_REGRAS` (constante no topo do arquivo) enumera as **10 regras de `regras.py`** por chave (`achado.regra`, campo que já existia no model `ContabilAchado`).
**Cards agrupados por tema, não um card por regra** (rodada seguinte, pedido explícito do usuário: a versão original — um card por regra, 10 ao todo, cada um com uma mini-tabela de conta+status por achado — ficou "muito poluída visualmente"). `PID_DC_GRUPOS` (constante logo abaixo de `PID_DC_REGRAS`) agrupa as 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) — agrupamento alinhado com o usuário antes de implementar (via pergunta com preview), não inferido sozinho. Cada card mostra só o total do grupo no cabeçalho e, por baixo, uma linha por regra (label + contagem, sem mini-tabela de conta/status) — regra sem nenhum achado nesta apuração continua listada com contagem `0`, só sem estar clicável (mesmo espírito de sempre mostrar as 10 regras, mesmo as que "passaram"). O detalhe por conta/status de cada achado (antes replicado dentro de cada card) não foi removido, só saiu do resumo — continua disponível na lista completa logo abaixo, ao clicar numa linha de regra pra filtrar. `renderAchadosResumo()` (chamada no início de `renderAchados()`, então atualiza sozinha a cada mudança de status/filtro) conta sempre sobre `apuracaoAtual.achados` **completo**, nunca sobre `achadosFiltrados()` — é uma visão geral estável, não deve mudar quando o usuário filtra a lista detalhada logo abaixo. As cores dos 3 grupos (`PID_DC_CATEGORIA_CORES`, mesma constante de antes, agora indexada por grupo em vez de por regra) reaproveitam os tokens `--accent-rgb`/`--danger-rgb`/`--gold-rgb` de `tokens.css` (já theme-aware, acompanham tema claro/escuro e a cor de tema escolhida pelo usuário), sem nenhum hex novo hardcoded. Decisão explícita de escopo (alinhada por pergunta ao usuário antes de implementar): **não** foi replicado o checklist pass/fail de checagens do sistema de referência (várias delas — folha, vencimento de fornecedor/cliente/imposto, saldo bancário — dependem de dado fora do Balancete/DRE anexado, fora do escopo já documentado desta ferramenta, ver "Decisões de escopo" acima) nem essa visualização foi levada pro relatório "Gerar Dashboard" (só a tela de revisão do Portal).
**Donut e linhas de regra são clicáveis, filtram a lista detalhada abaixo** (pedido explícito do usuário, rodada 98; a granularidade do clique por regra individual foi preservada na reorganização em grupos da rodada seguinte): cada fatia do donut (ou item da legenda) chama `pidDcSelecionarSeveridade(severidade)` — a mesma função que os chips "Alta"/"Média"/"Baixa" já usavam (extraída pra função reaproveitável, sem duplicar a lógica de atualizar `filtroSeveridade`+classe `.is-active`+`renderAchados()`); clicar numa linha de regra com contagem > 0 alterna `filtroRegra` (`achado.regra` exata ou `null`) — clicar de novo na mesma linha limpa o filtro; o card do grupo em si (cabeçalho) não é clicável, só as linhas de regra dentro dele. `achadosFiltrados()` ganhou uma terceira condição (`filtroRegra`) que se combina por E lógico com severidade/status já existentes — os três filtros funcionam juntos, não um substitui o outro. Como não existe um chip próprio pro filtro por categoria, uma faixa nova (`#dc-regra-filtro-ativo`, escondida quando `filtroRegra` é `null`) aparece entre os chips e a lista mostrando o nome da regra ativa + um botão "Limpar" — sem essa faixa não haveria como o usuário perceber por que a lista ficou filtrada nem como sair do filtro sem adivinhar que precisa clicar de novo na linha. Clicar em qualquer um dos dois (donut/linha de regra) também dá um `scrollIntoView` suave até `#dc-achados-list` (`pidDcScrollParaLista()`), já que o resumo pode empurrar a lista pra fora da tela em telas menores. O donut usa a técnica clássica de pizza/donut em SVG com `` (circunferência ≈ 100, então `stroke-dasharray`/`stroke-dashoffset` já funcionam direto em unidades de percentual, sem precisar de `pathLength`) — cada segmento é um `` próprio com seu `stroke-dashoffset` acumulado (offset inicial `25` desloca o início de "3 horas" pra "12 horas"), clicável individualmente porque `pointer-events` de SVG só considera pixel realmente pintado pelo `stroke`, sem precisar de hit-test manual por ângulo.
**Editor de observação inline + checkbox "validado" (rodada de "aliado do contador")**: pedido explícito do usuário pra reduzir a fricção da revisão — o antigo `#dc-observacao-modal` (popup único reaproveitado por conta/linha) foi removido; clicar no ícone de observação agora abre uma `` extra logo abaixo da própria linha, dentro da mesma tabela (`renderContas()`/`renderDre()`, `dashboard-contabil.js`), com um `