Compare commits

...

2 Commits

Author SHA1 Message Date
b0b63d90a5 Inclusão da Análise Vertical 2026-09-11 09:36:56 -03:00
617d3afa32 Validações aplicação Contábil 2026-09-10 16:53:09 -03:00
35 changed files with 2763 additions and 200 deletions

View File

@ -24,7 +24,7 @@ Aplicações com pacote Python próprio (`CLAUDE.md` **carregado automaticamente
| Indicador de Desempenho | `portal_api/indicadores/` — `CLAUDE.md` (técnico), `README.md`, `CHANGELOG.md` |
| Simulação de Custo de Contratação | `portal_api/custo_contratacao/` — `CLAUDE.md` (técnico), `README.md`, `CHANGELOG.md` |
| Não Conformidades | `portal_api/nao_conformidades/` — `CLAUDE.md` (técnico), `README.md`, `CHANGELOG.md` |
| Dashboard Contábil | `portal_api/dashboard_contabil/` — `CLAUDE.md` (técnico), `README.md`, `CHANGELOG.md` |
| Relatório Contábil | `portal_api/dashboard_contabil/` — `CLAUDE.md` (técnico), `README.md`, `CHANGELOG.md` |
Aplicações sem pacote Python dedicado (código ainda em `portal_api/models.py`/`views.py`/`serializers.py` — os arquivos em `docs/<app>/` **não** são carregados automaticamente, ler manualmente):
@ -216,7 +216,7 @@ Um único app, `portal_api/`:
| `custo-contratacao.html` | Ferramenta de Geradoc: formulário de simulação de custo de contratação (Empregado CLT) + painel colapsável de parâmetros fiscais, gera um PDF (ver `portal_api/custo_contratacao/CLAUDE.md`). |
| `indicador-desempenho.html` | Ferramenta de Geradoc: histórico de apurações + nova apuração (upload das 2 planilhas) + cadastro de critérios/percentuais + revisão/geração dos recibos em PDF do Indicador de Desempenho do Fiscontábil (ver `portal_api/indicadores/CLAUDE.md`). |
| `nao-conformidades.html` | Aplicação de Relatórios > Qualidade: gestão contínua das ocorrências/ações do Sigsistem, com dashboard e status interno de tratativa da Qualidade (ver `portal_api/nao_conformidades/CLAUDE.md`). |
| `dashboard-contabil.html` | Aplicação de Relatórios > Contabilidade: histórico de análises + nova análise (upload do PDF de Balancete + DRE) + revisão de achados de auditoria (com observações por conta) + botão "Gerar Dashboard" (relatório HTML autocontido, ver `portal_api/dashboard_contabil/CLAUDE.md`). |
| `dashboard-contabil.html` | Aplicação de Relatórios > Contabilidade ("Relatório Contábil" na UI): histórico de análises + nova análise (upload do PDF de Balancete + DRE) + revisão de achados de auditoria (com observações por conta) + botão "Gerar Relatório" (relatório HTML autocontido, ver `portal_api/dashboard_contabil/CLAUDE.md`). |
O item "Calendário De Paula" no menu **não é uma página local** — é um `<a href="#" id="calendario-depaula-btn">` (continua favoritável, já que ainda é um `<a class="nav-item">` — ver `docs/favoritos/favoritos.md`) cujo clique é interceptado em `sidebar.js` (`PID_CALENDARIO_DEPAULA_URL`) pra abrir `https://depaula-tvcorporativa.lovable.app/calendario` num modal com `<iframe>` (`#calendario-depaula-modal`, presente em todo shell) em vez de navegar — mesmo padrão do modal "Novo Chamado" de Ramais (`.ram-chamado-*` em `ramais.css`/`ramais.js`), só que genérico o bastante (`.iframe-modal-*` em `components.css`) pra existir em todo shell, não só em `ramais.html`. Funciona porque esse host (mesmo domínio do "Novo Chamado") não bloqueia ser embutido via `X-Frame-Options`/CSP, ao contrário do Asana (ver `docs/solicitacoes/solicitacoes.md`) — só foi possível confirmar isso testando de fato, não é garantia geral por domínio. Não recriar um `calendario.html` interno sem confirmar com o usuário; o antigo foi removido de propósito.
@ -375,9 +375,11 @@ Gestão contínua das ocorrências e ações do Sistema de Gestão da Qualidade
Ver `portal_api/nao_conformidades/CLAUDE.md`.
## Dashboard Contábil (Relatórios > Contabilidade)
## Relatório Contábil (Relatórios > Contabilidade)
Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (`ITD-FISCO-7513`): 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 regras de auditoria (saldos negativos, contas transitórias/genéricas com saldo, contas que deveriam ficar zeradas, débito ≠ crédito, variações atípicas mês a mês contra o histórico já processado no Portal), com revisão de achados e observações por conta antes da conclusão. O botão "Gerar Dashboard" gera um relatório HTML autocontido (indicadores financeiros, gráfico de evolução, DRE/Balancete agrupados, observações do contador — com a marca do escritório, não a "P.I.D." do Portal), com exportação de Balancete/DRE em XLSX a partir dele. **Ainda fora de escopo**: consolidação entre várias empresas/competências ao mesmo tempo (substituiria o BI Contábil por completo).
Chamado de "Dashboard Contábil" até uma rodada anterior — renomeado pra "Relatório Contábil" a 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, título da página, cabeçalhos e o botão que gera o relatório; nomes técnicos internos — pasta do pacote, arquivos, classes de model, rotas — continuam `dashboard_contabil`/`dashboard-contabil.*`/`Contabil*`/`/contabil-*`, sem nenhuma mudança).
Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (`ITD-FISCO-7513`): 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 regras de auditoria (saldos negativos, contas transitórias/genéricas com saldo, contas que deveriam ficar zeradas, débito ≠ crédito, variações atípicas mês a mês contra o histórico já processado no Portal), com revisão de achados e observações por conta antes da conclusão — cada conta/linha também ganha um checkbox de "validado" (marcador informativo de que o contador já conferiu aquele item) e um editor de observação inline na própria tabela (não mais um popup), com um toggle "mostrar ao cliente" que nasce desmarcado por padrão. O botão "Gerar Relatório" gera um relatório HTML autocontido (indicadores financeiros, gráfico de evolução, DRE/Balancete agrupados, observações do contador — com a marca do escritório, não a "P.I.D." do Portal), com exportação de Balancete/DRE em XLSX a partir dele. **Ainda fora de escopo**: consolidação entre várias empresas/competências ao mesmo tempo (substituiria o BI Contábil por completo).
Ver `portal_api/dashboard_contabil/CLAUDE.md`.

View File

@ -40,7 +40,7 @@ Não há suíte de testes, lint ou build configurados neste projeto.
| Simulação de Custo de Contratação | Geradoc | [portal_api/custo_contratacao/](portal_api/custo_contratacao/README.md) |
| Indicador de Desempenho | Geradoc | [portal_api/indicadores/](portal_api/indicadores/README.md) |
| Não Conformidades | Relatórios > Qualidade | [portal_api/nao_conformidades/](portal_api/nao_conformidades/README.md) |
| Dashboard Contábil | Relatórios > Contabilidade | [portal_api/dashboard_contabil/](portal_api/dashboard_contabil/README.md) |
| Relatório Contábil | Relatórios > Contabilidade | [portal_api/dashboard_contabil/](portal_api/dashboard_contabil/README.md) |
Cada pasta acima tem um `README.md` (o que a aplicação faz, estado atual) e um `CHANGELOG.md` (histórico rodada a rodada, extraído de `plano.md`). As 5 aplicações com pacote Python próprio também têm um `CLAUDE.md` com o detalhamento técnico completo, carregado automaticamente pelo Claude Code; as demais têm esse detalhe no arquivo dentro da própria pasta em `docs/`.

View File

@ -9,6 +9,7 @@ from .models import (
ContabilAchado,
ContabilApuracao,
ContabilConta,
ContabilLinhaAnaliseVertical,
ContabilLinhaDre,
Departamento,
EmpresaQuestor,
@ -265,6 +266,12 @@ class ContabilLinhaDreAdmin(admin.ModelAdmin):
search_fields = ("descricao",)
@admin.register(ContabilLinhaAnaliseVertical)
class ContabilLinhaAnaliseVerticalAdmin(admin.ModelAdmin):
list_display = ("apuracao", "ordem", "descricao", "totalizador")
search_fields = ("descricao",)
@admin.register(ContabilAchado)
class ContabilAchadoAdmin(admin.ModelAdmin):
list_display = ("apuracao", "regra", "severidade", "status", "titulo")

View File

@ -62,7 +62,7 @@ MODULE_APPS = {
{
"key": "contabilidade",
"label": "Contabilidade",
"tools": [{"key": "dashboard-contabil", "label": "Dashboard Contábil"}],
"tools": [{"key": "dashboard-contabil", "label": "Relatório Contábil"}],
},
],
"relatorios-gerenciais": [

View File

@ -130,4 +130,87 @@ Pedido: a linha sintética do Balancete (grupo do plano de contas, ex. "1.02.05
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.
(b) Novo mecanismo de destaque: ao expandir uma conta/linha, só as linhas que acabaram de ficar visíveis (filhos diretos, não os netos — que continuam recolhidos pelo limiar acima) ganham um realce dourado; expandir uma delas em seguida move o realce pra seus filhos e o grupo destacado antes volta à cor padrão, nunca acumulando mais de uma "leva" ao mesmo tempo — ajuda o contador a acompanhar visualmente onde ele acabou de descer na árvore. `dcContasDestaque`/`dcDreDestaque` (`dashboard-contabil.js`, dois `Set()` resetados em `renderRevisao()`) guardam só a leva mais recente; `dcFilhosDiretos()` (função genérica, reaproveitada pelas duas árvores) calcula os filhos diretos de um item a partir da lista plana. Classe nova `.dc-conta-row--destaque` (`dashboard-contabil.css`, fundo `rgba(var(--gold-rgb), .16)`). Só na tela de revisão — o relatório "Gerar Dashboard" não ganhou esse destaque (é uma navegação estática, sem o mesmo conceito de "acabei de expandir isto agora"). Puramente visual/em memória, sem persistência nem mudança de modelo. **Nota de rodada seguinte (110)**: essa última frase não vale mais — o relatório ganhou o mesmo destaque logo depois.
### 110. Código interno da empresa nas telas do Dashboard Contábil
Pedido explícito do usuário, com captura de tela do histórico mostrando só o nome da empresa ("GUARANI MUSICAL INSTRUMENTOS MUSICAIS LTDA - EPP") — o Portal sempre referencia empresa pelo código interno (`codigo_empresa`), mesmo padrão já usado em Importação de Plano de Saúde (`${codigo_empresa} - ${nome_empresa}`). O backend já expunha `codigo_empresa` nos dois serializers usados pela tela de revisão (`ContabilApuracaoListSerializer`/`ContabilApuracaoDetailSerializer`) — faltava só o frontend exibir. Dois pontos ajustados em `dashboard-contabil.js`: coluna "Empresa" do histórico (linha da tabela) e o cabeçalho da tela de Revisão, ambos passaram de `nome_empresa` sozinho pra `codigo_empresa - nome_empresa`. **Não alterado de propósito**: o relatório "Gerar Dashboard" (`dashboard-contabil-relatorio.html`, título/`<h1>`) e a planilha XLSX exportada continuam só com o nome — são documentos entregues ao cliente com a identidade do escritório, não telas internas do Portal, então o código de referência interno provavelmente não faz sentido lá (a confirmar com o usuário se for pedido depois).
### 111. Cor do destaque de expansão ajustada por tema + destaque restaura ao recolher
Duas rodadas de ajuste fino sobre o destaque da rodada 109 (`.dc-conta-row--destaque`), só na tela de revisão:
(a) A cor dourada fixa (`rgba(var(--gold-rgb), .16)`) destoava do tema escuro e ficava quase invisível no claro. Duas variáveis novas em `dashboard-contabil.css` (`--dc-destaque-bg`/`--dc-row-tint-bg`, redefinidas em `:root[data-theme="light"]`) parametrizam a cor por tema: no escuro, o destaque usa `--card-bg-hover` (mesmo tom neutro do hover das linhas — tentativa anterior com `rgba(var(--accent-rgb), .18)` foi descartada por pedido do usuário, achou o hover mais discreto/agradável); no claro, o efeito é **invertido** — as linhas fora do destaque ganham um tingimento roxo sutil (`rgba(var(--accent-rgb), .08)`) e a leva recém-revelada fica em branco puro (`--bg-surface`), lida como "em foco" em vez de tingida. Bug real encontrado no meio do caminho: a regra genérica de tingimento (`.dc-contas-table tbody tr td`, 3 seletores de tipo) tinha especificidade CSS maior que a regra de destaque original (`.dc-conta-row--destaque td`, 1 seletor de tipo) e sempre vencia, apagando o destaque por completo — corrigido aumentando a especificidade da regra de destaque (duas classes: `.dc-contas-table .dc-conta-row--destaque td`).
(b) Ao recolher uma leva destacada, o destaque devia voltar pra leva anterior (a que estava destacada antes de expandir), não simplesmente desaparecer. `dcContasDestaqueHistorico`/`dcDreDestaqueHistorico` (dois arrays, um por árvore) empilham o destaque atual a cada expandir e desempilham a cada recolher — mesmo espírito de undo por nível, sem tentar rastrear relação de parentesco entre expansões não relacionadas.
### 112. Destaque replicado no relatório "Gerar Dashboard" (revertendo a nota da rodada 109)
Pedido explícito do usuário: aplicar as mesmas mudanças de cor/comportamento da rodada 111 também no relatório gerado pro cliente (`dashboard-contabil-relatorio.html`), que até aqui não tinha esse destaque nenhum. Só a variante clara existe aqui (o relatório não tem alternância de tema).
Implementado com uma custom property por linha (`--dcr-row-bg`, lida por um único seletor `.dcr-tabela tbody tr td { background: var(--dcr-row-bg, transparent); }`, escrita por três regras de mesma especificidade — tingimento padrão/`.dcr-linha-total`/`.dcr-destaque`, nessa ordem, a última declarada vence o empate) em vez de competir por especificidade CSS entre elas — mesmo bug de especificidade da rodada 111 apareceu aqui de um jeito mais sério: como a maioria das linhas reveladas ao expandir um grupo real também são grupos (`tipo="S"`, com filhos mais fundo ainda; confirmado consultando o banco: "BENS NUMERÁRIOS" é `tipo="S"` de verdade), excluir `.dcr-linha-total` do destaque (tentativa inicial, calcada na tela de revisão) fazia o efeito nunca aparecer na prática — o destaque final **não** exclui linhas de grupo/total (só o tingimento padrão de repouso as exclui, pra manter o dourado fixo delas fora do hover/destaque). `pidDcrArvore()` ganhou a mesma dupla empilha/desempilha da rodada 111 (`historico`/`destaque` locais à função, chamada uma vez por tabela).
Ajuste final, mesmo pedido de "ficar mais bonito" que abriu a rodada: o Balancete nascia com a tela inteira na cor de grupo/total (todo o plano de contas real é `tipo="S"` até um nível bem profundo) enquanto a D.R.E. já nascia com essa aparência "gostosa" (poucas linhas são `totalizador`). `destaque` agora nasce pré-populado com as linhas marcadas `colapsado_padrao` no servidor (o último nível já visível por padrão, ver rodada 109) — nasce com o mesmo efeito de "acabei de expandir até aqui", sem precisar de nenhum clique.
### 113. Ícone selecionável por indicador + flip card no relatório
Pedido explícito do usuário, escopado por `AskUserQuestion` só pro relatório "Gerar Dashboard" (a aba "Dashboard" da revisão não ganhou flip/ícone, pra não competir com os botões de olho/lápis que já existem em cada card ali). Reverte a "perda aceita conscientemente" da rodada 104 (ícone único genérico pra todo indicador). Detalhe completo no `CLAUDE.md` desta pasta ("Ícone selecionável + flip card no relatório"): campo novo `IndicadorContabilDefinicao.icone` (migração `0063`, default `"barras"` — nenhum indicador já cadastrado muda de aparência sem edição manual), 11 ícones curados tipo KPI, seletor visual no modal "Novo/Editar Indicador", e o card do relatório virou um flip 3D (hover revela descrição + fórmula cadastradas em "Gerenciar Indicadores"). O desenho de cada ícone existe em duas cópias mantidas manualmente em sincronia (Python em `views.py`, JS em `dashboard-contabil.js`) — o relatório é HTML puro sem acesso ao JS do app, e o modal de cadastro é só JS estático sem contexto de servidor.
### 114. Fórmula do verso separada da fórmula de cálculo + esclarecimento sobre o relatório ser uma foto estática
Dois pontos levantados pelo usuário testando a rodada 113: (a) o card de EBIT mostrava "Sem descrição cadastrada" mesmo já tendo descrição salva — não era bug, era o relatório sendo uma foto estática do momento em que foi gerado (editar o indicador depois não atualiza um relatório já aberto/baixado, precisa gerar de novo); (b) a fórmula do verso mostrava a expressão técnica de cálculo (`resultado_liquido - despesas_financeiras`, chaves internas dos componentes), inadequada pro cliente final.
Campo novo `IndicadorContabilDefinicao.formula_exibicao` (`CharField`, `blank=True`, migração `0064`) — texto livre, sem validação de sintaxe (não passa por `avalia_formula()`), editável no modal de indicador logo abaixo do campo técnico (que ganhou o rótulo "Fórmula (cálculo interno)", pra diferenciar do novo "Fórmula (como aparece ao cliente)"). `_contabil_monta_cards_indicadores()` resolve o fallback no servidor (`formula_exibicao` preenchida vence; em branco cai pra `formula` mesmo) — nenhum indicador já cadastrado muda de comportamento até alguém preencher essa preferência pela tela. Detalhe completo no `CLAUDE.md` desta pasta.
### 115. Ajustes de acabamento do flip card + destaque inicial também na tela de revisão
Três pedidos na sequência, testando as rodadas 113/114: (a) a fonte do valor na frente do card (`R$ -143.648,54`) quebrava linha — `.dcr-card__value` de `1.55rem` pra `1.3rem`; (b) o verso do flip card tinha um scroll aninhado (a descrição rolava sozinha, separada da fórmula abaixo) — removido `flex:1`/`overflow-y:auto` de `.dcr-card-face__descricao`, agora descrição e fórmula fluem juntas num único bloco contínuo (a face inteira já rolava, `.dcr-card-face--back`); (c) pedido pra levar o mesmo destaque inicial do relatório (rodada 112) também pra tela de revisão do Balancete/D.R.E, reduzindo a poluição visual de abrir a tela inteira na cor de grupo/total.
`renderRevisao()` (`dashboard-contabil.js`) agora inicializa `dcContasDestaque`/`dcDreDestaque` como cópia de `dcContasColapsadas`/`dcDreColapsadas` (`new Set(dcContasColapsadas)`) em vez de `new Set()` vazio — mesma lógica já usada no relatório: as linhas colapsadas por padrão são o último nível já visível de cada ramo, então já nascem destacadas sem precisar de clique nenhum. `dcContasDestaqueHistorico`/`dcDreDestaqueHistorico` continuam nascendo vazios (o destaque inicial não empilha nada — só a partir da primeira expansão manual).
### 116. Botão de observação virou sempre ícone + cor do destaque invertida no tema escuro
Dois pedidos testando a rodada 115: (a) mostrar o texto da observação já preenchida direto na tabela também poluía a coluna (o "+ Observação" das linhas vazias já tinha virado ícone antes, mas a linha com "Cliente não envia o estoque." continuava em texto) — agora o botão é sempre o mesmo ícone, só muda de cor (`.dc-conta-observacao-btn--preenchida`, `--text-muted` vira `--accent`) conforme há observação ou não; o texto continua acessível por tooltip/clique, só sumiu da célula. (b) A cor do destaque no tema escuro estava invertida do que o usuário queria — trocado `--dc-destaque-bg`/`--dc-row-tint-bg` de lugar: destaque agora usa `--bg-canvas` (mais escuro) e o resto `--bg-surface-raised` (mais claro, mas ainda diferente do hover). O tema claro não mudou (já tinha sido invertido por pedido anterior, contrariamente ao escuro). Detalhe completo no `CLAUDE.md` desta pasta.
### 117. Bug real — folha genuína ficava de fora do destaque inicial (D.R.E.)
Usuário reportou, testando a rodada 116, que várias linhas-folha da D.R.E. (`(-) DE VENDAS DE MERCADORIAS MERCADO INTERNO`, `(-) SIMPLES NACIONAL`, `DESCONTOS OBTIDOS` e outras linhas de resultado financeiro) não ficavam com o destaque escuro esperado, mesmo sendo visualmente "de baixo" quanto um grupo colapsado vizinho no mesmo nível. Causa: o destaque inicial (rodada 112/115) usava só o conjunto de linhas colapsadas por padrão — que só marca linha com filho escondido —, deixando de fora qualquer folha genuína (sem filho nenhum), que nunca entra nesse conjunto mas é igualmente "o fim do ramo" visualmente.
Corrigido com um critério novo, mesmo algoritmo nas duas cópias (`dcUltimaLevaVisivel()` em `dashboard-contabil.js`, `calculaDestaqueInicial()` em `dashboard-contabil-relatorio.html`): reconstrói a lista de linhas realmente visíveis e marca destaque em toda linha cuja próxima linha visível não seja mais profunda — cobre grupo colapsado e folha genuína com a mesma regra. Validado manualmente contra os níveis reais da apuração `id=4` antes de aplicar. Detalhe completo no `CLAUDE.md` desta pasta.
### 118. Acabamento do flip card (texto centralizado, fonte) + confirmação ao fechar o modal de indicador sem salvar
Dois pedidos independentes: (a) o verso do flip card, com uma captura de tela do equivalente que o usuário já usa no Power BI como referência — texto centralizado (`text-align:center` em `.dcr-card-face--back`), fontes um pouco menores, e a fórmula ganhou "JetBrains Mono" em vez de só `"Courier New", monospace`; (b) o modal "Novo/Editar Indicador" fechava direto ao clicar fora (overlay), sem perguntar nada — `pidDcFecharIndicadorModalComConfirmacao()` (nova) pergunta "Sair sem salvar as alterações?" (`pidConfirm`) antes de fechar de verdade, ligada ao overlay e ao "Cancelar"; fechar depois de salvar/excluir com sucesso continua direto, sem pergunta. Detalhe completo no `CLAUDE.md` desta pasta.
### 119. Resumo do Fechamento — texto rico do contador antes dos indicadores
Pedido explícito do usuário, com um exemplo real de carta que a De Paula já manda ao cliente hoje (considerações/saldos/variações do fechamento) como referência do que deveria caber aqui. A aba "Indicadores" do relatório "Gerar Dashboard" virou "Resumo" (mesmo nome também na aba "Dashboard" da revisão) — passa a carregar duas coisas: o texto livre do contador ("Resumo do Fechamento", no topo) e, embaixo, os grupos de cards de indicador de sempre.
Campo novo `ContabilApuracao.resumo_fechamento` (`TextField`, `blank=True`, migração `0065`) — texto rico (HTML sanitizado por nh3, mesma allowlist de `AcessoGeral.observacoes`/`AjudaAplicacao.texto`), preenchido via editor `contenteditable` com colar/arrastar imagem (mesmo padrão dos outros dois campos ricos do projeto, duplicado de propósito) na aba "Dashboard" da revisão. Nova `@action` `POST /api/contabil-apuracoes/{id}/resumo-fechamento/` (`ContabilApuracaoViewSet` não tem PATCH genérico, de propósito) substitui o campo inteiro de uma vez, validado por `ContabilResumoFechamentoSerializer`. No relatório, `{{ apuracao.resumo_fechamento|safe }}` — primeiro `|safe` de template Django do projeto (até agora todo texto rico só existia via `innerHTML` em tela SPA), seguro porque já vem sanitizado antes de salvar; seção some inteira quando o campo está vazio. Testado de ponta a ponta via shell, inclusive confirmando que um `<script>` embutido é removido pelo nh3. Detalhe completo no `CLAUDE.md` desta pasta.
### 120. Rebatizado pra "Relatório Contábil" + validação por conta/linha + observação virou editor inline (sem popup) + resumo por aba
Pedido explícito do usuário: deixar a ferramenta soar como um aliado do trabalho do contador, não mais um processo novo. Quatro mudanças na mesma rodada:
1. **Rename só de rótulo visível** — "Dashboard Contábil" virou "Relatório Contábil" no menu (`catalogo.py`), `<title>`/`<h1>`/cabeçalhos de `dashboard-contabil.html` e do relatório, e o botão "Gerar Dashboard" virou "Gerar Relatório". Nada técnico mudou (pasta, arquivos, classes, rotas continuam com o nome antigo) — detalhe completo no `CLAUDE.md` desta pasta.
2. **Checkbox "validado"** — campo novo `validado` (`BooleanField`, default `False`, migração `0066`) em `ContabilConta`/`ContabilLinhaDre`; um segundo botão (ícone de check, cor `--teal`) ao lado do de observação em cada linha do Balancete/DRE, puramente informativo (não afeta achado/observação/relatório), pro contador marcar que já conferiu aquela conta durante a revisão.
3. **Observação virou editor inline** — o popup único `#dc-observacao-modal` foi removido; clicar no ícone de observação abre uma linha extra dentro da própria tabela (textarea + checkbox "Mostrar esta observação ao cliente no relatório" + Cancelar/Salvar), evitando perder o texto por causa de um clique acidental fora de um popup. Esse checkbox controla o já existente `oculta_no_relatorio`, cujo `default` também mudou de `False` pra `True` (migração `0066`) — uma observação nova só vai pro relatório do cliente depois que o contador confirmar isso explicitamente, nunca por padrão.
4. **Resumo por aba** — Balancete e DRE ganharam cada um sua própria seção de resumo (quantidade + lista de observações) no final da tabela, além da lista combinada que já existia na aba "Dashboard" (mantida, sem mudança — servem propósitos diferentes).
Detalhe completo no `CLAUDE.md` desta pasta.
### 121. Correções de acabamento da rodada 120 + observação também no relatório do cliente
Três pedidos testando a rodada 120: (a) o checkbox "Mostrar ao cliente" do editor inline nascia marcado mesmo numa conta sem observação nenhuma (arrastava o `oculta_no_relatorio=False` antigo de contas criadas antes do default mudar) — corrigido pra só refletir o valor salvo quando já existe observação (`conta.observacao && !conta.oculta_no_relatorio`), senão nasce sempre desmarcada; (b) a caixa de texto do editor tinha `max-width:640px`, cortando antes do fim da linha — removido, agora estica a linha inteira; (c) um bug real de renderização — a célula dos 2 botões (validado + observação) tinha `display:flex` aplicado direto no `<td>`, o que tira a célula do comportamento normal de tabela (fundo/altura não acompanhavam mais a linha, dando a impressão de "quebrar antes dos botões") — corrigido movendo o `flex` pra uma `<div>` dentro do `<td>`. De quebra, também corrigido um bug real no destaque: linhas de resultado consecutivas no nível raiz da DRE (`RESULTADO ANTES DA CS E IR` etc.) estavam todas ficando escuras só por serem vizinhas no mesmo nível — `dcUltimaLevaVisivel()`/`calculaDestaqueInicial()` agora só aplicam destaque no nível raiz quando o item de fato esconde algo (grupo colapsado), validado contra os dados reais da apuração.
Pedido separado na mesma rodada: o relatório "Gerar Dashboard" ganhou o mesmo ícone de observação da tela de revisão — coluna "Observação" nova no Balancete/DRE do relatório, ícone só quando há observação marcada como visível ao cliente, clique abre um painel inline abaixo da linha (`pidDcrObs()`, nova função em `dashboard-contabil-relatorio.html`). Detalhe completo no `CLAUDE.md` desta pasta.
### 122. Botão "validado" de uma conta/linha sintética virou tri-state (nenhum/parcial/completo)
Pedido explícito do usuário: numa conta/linha com filhos (sintética), marcar o próprio check não deveria mais só alternar True/False — passou a ter 3 estados calculados a partir dos descendentes (`dcEstadoValidacaoGrupo()`, nova): amarelo enquanto nem todos os descendentes estiverem validados (mesmo já tendo marcado a própria sintética), verde quando 100% dos descendentes estiverem validados. Ciclo de clique implementado em `dcClicarValidadoConta()`/`dcClicarValidadoLinha()` (novas): 1º clique marca só a sintética (fica amarela); clicar de novo (já amarela) pergunta "Deseja validar todas as contas deste grupo?" e, confirmado, valida tudo em lote (fica verde); clicar de novo (já verde) desmarca o grupo inteiro sem perguntar. Folhas (sem filhos) continuam com o toggle simples de antes. Detalhe completo no `CLAUDE.md` desta pasta.
### 123. Demonstração Mensal (Análise Vertical) — nova aba na revisão e no relatório
Pedido explícito do usuário: a seção "Demonstração Mensal (Análise Vertical)" do mesmo PDF (páginas finais, histórico de 3 meses com valor+variação percentual por linha, mesma árvore da DRE) — até então ignorada de propósito pelo parser — passou a ser extraída, persistida e exibida. Escopo confirmado por `AskUserQuestion` antes de implementar: aba própria (não embutida na aba D.R.E.) e mesmos recursos por linha que Balancete/D.R.E. (observação inline, tri-state "validado", ocultar do relatório).
Extração validada rodando de fato contra o PDF de referência do usuário (`792 - balancete 072026.pdf`) antes de escrever o parser definitivo — mesmo cuidado de sempre (nunca desenhar regex só de texto colado). Model novo `ContabilLinhaAnaliseVertical` (mesma árvore/descrição/nível da DRE, `valores` como `JSONField` de texto — um `{valor, percentual}` por mês, alinhado por posição com `ContabilApuracao.analise_vertical_meses`) + `ContabilLinhaAnaliseVerticalViewSet` (réplica de `ContabilLinhaDreViewSet`). Nova aba na tela de revisão (`renderAnaliseVertical()`, réplica de `renderDre()` com N colunas dinâmicas de Valor/Variação) e no relatório "Gerar Dashboard" (4ª aba, entre D.R.E. e Resumo) — as duas somem por completo quando a apuração não tem essa seção (relatório antigo). Dois filtros de template novos (`moeda_av`/`percentual_av`) porque o percentual desta seção já vem "pronto" do PDF (não é uma fração como os indicadores). Testado ponta a ponta via `Client.force_login()` contra o PDF real (extração, persistência, PATCH de observação/validado, relatório gerado), sem deixar resíduo em produção. Detalhe completo no `CLAUDE.md` desta pasta.

View File

@ -1,7 +1,9 @@
# Dashboard Contábil (Relatórios > Contabilidade)
# 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`), `<title>`/`<h1>`/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).
@ -20,7 +22,7 @@ O relatório Questor de Balancete + DRE tem uma particularidade de renderizaçã
- **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).
- **Demonstração Mensal (Análise Vertical)** (páginas finais do mesmo PDF, quando presentes) — **passou a ser extraída** (rodada 123; antes o parser parava aí de propósito, já que o histórico próprio do Portal cobre variação mês a mês de forma mais confiável). É a mesma árvore da DRE (mesma descrição/ordem/negrito), só que cada linha repete N pares "Valor Variação" (um por mês mostrado, ex. "mai - 2026 jun - 2026 jul - 2026" — normalmente os 3 meses até a competência do PDF) em vez de um valor único. `_RE_PAR_VALOR_VARIACAO` casa um par de cada vez (`(valor, percentual)`, na ordem em que aparecem na linha); `_RE_MES_ANALISE_VERTICAL` captura o cabeçalho de mês uma única vez (primeira página da seção — as páginas seguintes repetem o mesmo cabeçalho, ignorado depois da primeira captura). O nível de indentação usa o mesmo divisor `/7.0` da DRE, calibrado contra o mesmo PDF de referência. Ver "Análise Vertical" mais abaixo pra models/views/frontend/relatório.
- `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).
@ -46,8 +48,9 @@ Regras 8-10 não geram achado nenhum na primeira apuração de uma empresa (sem
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.
- **`ContabilLinhaDre`**: uma linha da DRE, sem código de classificação (o relatório não traz um pra DRE, diferente do Balancete).
- **`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`.
- **`ContabilLinhaAnaliseVertical`** (rodada 123): uma linha da Demonstração Mensal (Análise Vertical) — mesma árvore/descrição/nível da DRE, mas `valores` (`JSONField`) guarda um `{"valor": "...", "percentual": "..."}` por mês em vez de um `DecimalField` único (gravado como texto, não float, pra não perder precisão), alinhado por posição com `ContabilApuracao.analise_vertical_meses` (ex.: `["mai/2026", "jun/2026", "jul/2026"]`, lista compartilhada por toda a apuração, não por linha). Mesmos `observacao`/`oculta_no_relatorio`/`validado` de `ContabilConta`/`ContabilLinhaDre` — recursos por linha idênticos (editor inline, tri-state, ocultar do relatório), decisão confirmada com o usuário via `AskUserQuestion` antes de implementar. Lista vazia (`analise_vertical_meses=[]`, nenhuma linha) quando o PDF não tinha essa seção — relatório antigo, ou empresa sem essa seção habilitada no Questor; a aba/tab correspondente some nesse caso (ver "Análise Vertical" abaixo).
- **`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
@ -63,7 +66,7 @@ Diferente de `IndicadorApuracaoViewSet`/`ImportacaoPlanoSaudeViewSet` (onde a ch
## 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 abre outro modal (`#dc-observacao-modal`), sem essa exigência (pode ficar em branco). Botão "Gerar Dashboard" (`#dc-gerar-dashboard-btn`) chama `pidGerarDashboardContabil()` — ver "Relatório 'Gerar Dashboard'" abaixo.
`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()`).
@ -81,6 +84,26 @@ Diferente de `IndicadorApuracaoViewSet`/`ImportacaoPlanoSaudeViewSet` (onde a ch
**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 `<circle r="15.9155">` (circunferência ≈ 100, então `stroke-dasharray`/`stroke-dashoffset` já funcionam direto em unidades de percentual, sem precisar de `pathLength`) — cada segmento é um `<circle>` 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 `<tr class="dc-obs-edit-row">` extra logo abaixo da própria linha, dentro da mesma tabela (`renderContas()`/`renderDre()`, `dashboard-contabil.js`), com um `<textarea>`, um checkbox "Mostrar esta observação ao cliente no relatório" e os botões Cancelar/Salvar — motivo do usuário: um clique acidental fora do popup não deve mais descartar o texto em digitação, e ver a conta ao lado da observação ajuda a não perder o contexto. Estado de qual editor está aberto (no máximo um por tabela) fica em `dcContaObsEditId`/`dcDreObsEditId` (module-level, resetados em `renderRevisao()`), seguindo o mesmo padrão de `dcContasColapsadas`/`dcAchadosContaExpandida`. Salvar chama `pidAtualizarObservacaoContaContabil(id, {observacao, oculta_no_relatorio})`/a equivalente de DRE — os dois campos juntos numa única chamada PATCH, já que "mostrar ao cliente" é decidido no mesmo instante em que a observação é escrita; `oculta_no_relatorio` agora nasce `True` por padrão no model (antes `False`) — uma observação nova só vai pro relatório do cliente depois que o contador marcar o checkbox explicitamente, nunca por padrão (migração `0066`, só o `default` mudou, sem reescrever linhas já existentes).
Segundo botão ao lado do de observação (mesma célula, `.dc-obs-cell-actions`): um ícone de check (`.dc-conta-validado-btn`, cor `--teal` quando marcado — deliberadamente não `--accent`, que já é usado pra "observação preenchida" e também é a cor de tema escolhida pelo usuário, ver "Temas de cor" no `tokens.css`) pro campo novo `validado` — puramente informativo ("já conferi esta conta/linha durante a revisão"), sem gate em nada (não bloqueia conclusão, não afeta achado/relatório). PATCH via `pidAtualizarValidadoContaContabil`/`...LinhaDreContabil`.
**Tri-state numa conta/linha sintética (com filhos)** (pedido explícito do usuário, rodada seguinte): uma folha continua um toggle simples (verde/cinza), mas uma sintética passa a ter 3 estados, calculados a cada render a partir dos **descendentes** (todos, não só os filhos diretos — `dcDescendentes(itens, nivelFn, id)`, generaliza `dcFilhosDiretos` pra não parar no primeiro nível) via `dcEstadoValidacaoGrupo(item, descendentes)`:
- `"nenhum"` (cor padrão) — nem a sintética nem nenhum descendente está validado.
- `"parcial"` (`--gold`, mesmo amarelo dos badges de severidade média) — qualquer combinação intermediária, **inclusive** só a própria sintética marcada (1º clique) sem nenhum descendente ainda.
- `"completo"` (`--teal`, mesma cor de uma folha validada) — **todo** descendente está validado, checado primeiro e independente do campo `validado` da própria sintética (o que importa é "está tudo marcado embaixo", não se o cabeçalho do grupo foi clicado).
`dcClicarValidadoConta(id)`/`dcClicarValidadoLinha(id)` (novas, chamadas pelo listener de clique de `[data-dc-conta-validado]`/`[data-dc-dre-validado]`) implementam o ciclo de 3 cliques pedido pelo usuário, recalculando o estado a cada clique (nunca guardado à parte):
1. `"nenhum"` → PATCH só na própria sintética (`validado=true`) → vira `"parcial"` (a menos que, coincidentemente, todo descendente já estivesse validado).
2. `"parcial"` → `pidConfirm("Deseja validar todas as contas deste grupo?")` → se confirmado, PATCH em lote (`Promise.all`) de todo descendente ainda não validado (mais a própria sintética, se ainda não) → vira `"completo"`. Se cancelado, nada muda.
3. `"completo"` → PATCH em lote desmarcando a própria sintética + todos os descendentes, **sem perguntar** (pedido explícito: "apertar novamente desmarca todo o grupo").
Cada PATCH é individual (`ContabilContaViewSet`/`ContabilLinhaDreViewSet` continuam sem uma action de lote) — o "lote" é só client-side via `Promise.all`, aceitável dado que um grupo real tem no máximo algumas dezenas de contas. Todo caminho termina chamando `renderContas()`/`renderDre()` inteiro (abandonando o ajuste direto no DOM que existia antes só pro caso de folha) — necessário porque o estado de uma sintética **ancestral** também pode ter mudado de cor e precisa recalcular, o que só um re-render completo garante; o efeito colateral aceito é que um editor de observação aberto numa outra linha perde o texto ainda não salvo nesse recálculo (like-for-like com o comportamento já aceito ao alternar entre linhas, ver "Editor de observação inline" acima).
**Resumo de observações no final de cada aba** (pedido explícito do usuário: "inclua no final das páginas um resumo da quantidade de observações e as observações realizadas"): Balancete e DRE ganharam cada um sua própria seção `.dc-obs-resumo` logo abaixo da tabela (`renderContasObsResumo()`/`renderDreObsResumo()`, chamadas no fim de `renderContas()`/`renderDre()` — sempre em sincronia com a tabela) com a contagem (`<span class="dc-obs-resumo__count">`) e a lista de observações já registradas **daquela aba**, cada uma com o mesmo botão de olho (mostrar/ocultar do relatório) que já existia na lista combinada da aba "Dashboard". É **adicional**, não substitui: a lista combinada de `#dc-dash-observacoes-list` (Balancete + DRE + Auditoria juntos) continua existindo do jeito que estava — decisão confirmada com o usuário, já que cada resumo serve um propósito diferente (visão específica de uma aba vs. visão consolidada antes de gerar o relatório). `pidDcObsItemHtml()` (função nova) fatora a marcação de um item de lista (`.dc-dash-obs`), reaproveitada pelos dois resumos novos — a lista combinada da aba "Dashboard" manteve sua própria montagem inline (precisa do rótulo de origem "Balancete"/"D.R.E."/"Auditoria" e de uma chave composta `tipo:id`, que os resumos por aba não precisam por já serem de um tipo só).
**Ícone de observação + painel inline também no relatório do cliente** (pedido explícito do usuário, "assim como temos na aplicação"): as tabelas de Balancete/DRE do relatório ganharam uma coluna "Observação" (igual à da tela de revisão) com um ícone que só aparece quando `item.conta.observacao`/`item.linha.observacao` está preenchida **e** `oculta_no_relatorio` é `False` (a mesma condição de `observacoes_contas`/`observacoes_dre` no contexto do view, ver "Relatório 'Gerar Dashboard'" abaixo) — uma observação marcada como não-visível ao cliente não aparece nem como ícone. Clicar abre um `<tr class="dcr-obs-inline-row">` já presente no HTML (nasce `hidden`) logo abaixo da conta/linha, com o texto puro (sem `|safe`, é `TextField` simples, não rich text). `pidDcrObs(tbodyId)` (nova função em `dashboard-contabil-relatorio.html`, chamada logo depois de `pidDcrArvore(tbodyId)` pro mesmo `tbody`) controla o abrir/fechar; a visibilidade da linha de observação é sincronizada (`sincroniza()`, chamada a **todo** clique no corpo da tabela, inclusive os de expandir/recolher grupo) a partir de dois fatores: se o usuário marcou aquele painel como aberto E se a linha-pai (o `previousElementSibling`) está visível — assim, colapsar um grupo ancestral também fecha visualmente qualquer painel de observação aberto dentro dele, sem duplicar a lógica de pilha/nível de `pidDcrArvore`. Importante: essas linhas de observação são **excluídas** da lista `linhas` que `pidDcrArvore()` percorre (`filter` por `data-dcr-obs-row`) — incluí-las quebraria a pilha de colapso, já que elas não têm `data-dcr-nivel` próprio.
## Relatório "Gerar Dashboard" (`indicadores.py` + `dashboard-contabil-relatorio.html`)
`ContabilApuracaoViewSet.dashboard()` (`GET /api/contabil-apuracoes/{id}/dashboard/`, mesma permissão de toggle único das outras actions) gera um documento HTML autocontido (não estende o shell do Portal — nunca a marca "P.I.D.", ver "Logos" no CLAUDE.md raiz) com os indicadores financeiros, a DRE/Balancete agrupados por nível (recolhível, ver "Árvore recolhível" abaixo), um gráfico de evolução do Resultado Líquido e as observações que o contador já registrou (contas, linhas de DRE, achados tratados/ignorados com `observacao_contador`). Escopo confirmado com o usuário: **sempre uma apuração por vez** (a que está sendo revisada), sem "Filial" (não existe no modelo) nem consolidação entre empresas — isso ficaria pra um BI à parte, fora de escopo aqui.
@ -229,7 +252,7 @@ Pedido explícito do usuário — reversão deliberada da decisão de escopo da
**`dashboard_contabil/indicadores.py` foi esvaziado, mas não apagado**: `CHAVES_CARDS`/`CHAVE_CARDS_RESULTADO`/`CHAVE_CARDS_LIQUIDEZ`/`MetaIndicador`/`METADADOS_CARDS` foram removidos (zero consumidor depois da migração — mantê-los seria texto duplicado e defasável em relação à `descricao`/`formula` reais do banco). `calcula_indicadores()`/`IndicadoresFinanceiros`/`_saldo()`/`_divide()`/`_busca_linha_por_trecho()`/`CODIGO_*` foram **deliberadamente mantidos**, mesmo sem nenhum chamador em produção (`ContabilApuracaoViewSet.dashboard()`/`.indicadores()` não chamam mais `_contabil_calcula_indicadores()` — a função wrapper em `views.py` continua existindo, só não é mais invocada) — é a única exceção neste projeto à convenção de apagar código sem uso, justificada pelo risco real de indicador financeiro: se um valor um dia parecer suspeito, dá pra recalcular pelo caminho antigo e comparar contra o banco sem precisar reconstruir a fórmula de cabeça. Revisar se ainda vale a pena manter numa rodada futura, depois que a migração provar estabilidade em produção por um tempo.
**Relatório "Gerar Dashboard" perdeu os 11 `.dcr-card` hardcoded** — `dashboard-contabil-relatorio.html` agora tem um único `{% for grupo in indicadores_grupos %}` genérico (mesmo conceito do `{% for indicador in indicadores_personalizados %}` que já existia pra indicador personalizado antes desta rodada, agora é o **único** caminho de renderização, sem duplicação). `_contabil_monta_cards_indicadores()`/`_contabil_agrupa_indicadores_cards()` (`views.py`) montam a lista já com tudo pronto: valor formatado, valor cru (`data-count`), formato pro contador animado (`data-format`), regra de cor e destaque dourado só pros 11 migrados (`_CONTABIL_INDICADOR_COR_REGRA`/`_CONTABIL_INDICADOR_DOURADO`/`_CONTABIL_INDICADOR_NOTA`, dicts chave-fixa em `views.py` — réplica visual exata do que esses 11 já tinham hardcoded no template antes; indicador personalizado criado depois não entra em nenhum desses dicts, nasce sem cor/destaque/nota, mesmo como já era). **Perda real, aceita conscientemente**: os 11 cards tinham um ícone SVG próprio cada (gráfico de barras pro ROA, gota pra Liquidez Seca, etc.) — o loop genérico usa um ícone único pra todo indicador agora, não haveria como manter 11 ícones distintos sem mais uma tabela chave→SVG; mencionado ao usuário, não pedido de volta ainda.
**Relatório "Gerar Dashboard" perdeu os 11 `.dcr-card` hardcoded** — `dashboard-contabil-relatorio.html` agora tem um único `{% for grupo in indicadores_grupos %}` genérico (mesmo conceito do `{% for indicador in indicadores_personalizados %}` que já existia pra indicador personalizado antes desta rodada, agora é o **único** caminho de renderização, sem duplicação). `_contabil_monta_cards_indicadores()`/`_contabil_agrupa_indicadores_cards()` (`views.py`) montam a lista já com tudo pronto: valor formatado, valor cru (`data-count`), formato pro contador animado (`data-format`), regra de cor e destaque dourado só pros 11 migrados (`_CONTABIL_INDICADOR_COR_REGRA`/`_CONTABIL_INDICADOR_DOURADO`/`_CONTABIL_INDICADOR_NOTA`, dicts chave-fixa em `views.py` — réplica visual exata do que esses 11 já tinham hardcoded no template antes; indicador personalizado criado depois não entra em nenhum desses dicts, nasce sem cor/destaque/nota, mesmo como já era). ~~**Perda real, aceita conscientemente**: os 11 cards tinham um ícone SVG próprio cada (gráfico de barras pro ROA, gota pra Liquidez Seca, etc.) — o loop genérico usa um ícone único pra todo indicador agora, não haveria como manter 11 ícones distintos sem mais uma tabela chave→SVG; mencionado ao usuário, não pedido de volta ainda.~~ Revertido numa rodada posterior — ver "Ícone selecionável + flip card no relatório" mais abaixo.
### Dois ajustes de acabamento no construtor de fórmula (mesma rodada da migração)
@ -240,3 +263,75 @@ Pedido explícito do usuário — reversão deliberada da decisão de escopo da
### Nova consulta de histórico (`_contabil_monta_historico_completo`, `views.py`)
Diferente de `_contabil_monta_historico` (capada em 2 apurações anteriores, usada só pelas regras de auditoria no `create()`), esta busca **todo** o histórico da empresa — usada só pelo gráfico de evolução e pelo cálculo de Depreciação/Amortização do relatório. Recebe `codigo_empresa`/`competencia_atual` diretamente (não um `CabecalhoExtraido`) porque roda contra uma `ContabilApuracao` já persistida, ao contrário de `_contabil_monta_historico` (que roda durante o `create()`, antes de qualquer coisa existir no banco).
### Ícone selecionável + flip card no relatório (rodada seguinte)
Pedido explícito do usuário, escopado por `AskUserQuestion` só pro relatório "Gerar Dashboard" (`dashboard-contabil-relatorio.html`) — a aba "Dashboard" da tela de revisão (`dashboard-contabil.js`/`.html`) **não** ganhou flip nem ícone nesta rodada, decisão deliberada pra não competir com os botões de olho/lápis que já existem em cada card ali.
**Ícone volta a ser configurável por indicador** — reverte a "perda aceita conscientemente" da migração anterior (ver acima). Campo novo `IndicadorContabilDefinicao.icone` (`CharField`, `choices`, default `"barras"` — o mesmo desenho hardcoded que todo indicador usava antes desta rodada, então nenhum indicador já cadastrado muda de aparência sem uma edição manual). Migração `0063_indicadorcontabildefinicao_icone`. 11 opções curadas de ícone tipo KPI (barras/tendência de alta/tendência de baixa/percentual/pizza/atividade/cifrão/alvo/camadas/cartão/selo).
**O desenho (miolo de `<svg>`) de cada ícone existe em duas cópias**, mantidas em sincronia manualmente, não geradas uma a partir da outra — `_CONTABIL_ICONES_SVG` (`portal_api/views.py`, usado por `_contabil_monta_cards_indicadores()` pra montar `card["icone_svg"]` via `mark_safe()`, já que são só literais Python fixos neste arquivo) e `PID_DC_INDICADOR_ICONES` (`static/js/dashboard-contabil.js`, desenha a grade de botões do seletor no modal "Novo/Editar Indicador"). Duplicação proposital: o relatório é HTML puro servido pelo Django (sem acesso ao JS do app) e o modal de cadastro é só JS/HTML estático (a página `dashboard-contabil.html` é uma `TemplateView` sem contexto de servidor) — não dá pra ter uma fonte única sem inventar mais uma ida ao backend só pra isso. Editar/adicionar um ícone exige mexer nos dois lugares.
**Seletor de ícone** (`#dc-indicador-icones`, `.dc-ind-icone-grid`/`.dc-ind-icone-btn` em `dashboard-contabil.css`): grade de botões, cada um já o próprio preview (mesmo SVG que vai aparecer no card), `.is-selecionado` marca o ativo. `dcIndicadorIconeSelecionado` (estado em memória) é inicializado por `pidDcAbrirIndicadorModal()` (`definicao.icone` ao editar, `"barras"` ao criar) e incluído no payload de salvar; `IndicadorContabilDefinicaoInputSerializer.icone` (`ChoiceField`, default `"barras"`) valida no backend.
**Flip card no relatório** — hover revela a `descricao`/fórmula do indicador (cadastradas em "Gerenciar Indicadores"). `.dcr-card` virou só a "cena" 3D (`perspective` + `min-height:172px`, necessário porque as duas faces do card são `position:absolute` agora e não contribuem mais pra altura do elemento); `.dcr-card-inner` é quem gira (`transform:rotateY(180deg)` no hover do `.dcr-card` pai, `transition` 480ms); cada face (`.dcr-card-face--front`/`--back`, `backface-visibility:hidden`) carrega o fundo/borda/sombra/padding que antes viviam direto em `.dcr-card`. O verso mostra nome + descrição (`"Sem descrição cadastrada."` se `descricao` estiver em branco — campo é opcional no model) + a fórmula. `@media print` já zera toda `animation`/`transition` globalmente, então o PDF/impressão sempre mostra a frente do card (não existe "hover" ativo numa impressão).
**Fórmula do verso não é mais a expressão técnica de cálculo** (ajuste na mesma rodada, depois de o usuário ver `resultado_liquido - despesas_financeiras` — as chaves internas dos componentes — no verso do EBIT e pedir um jeito de controlar o texto exibido ao cliente separado do cálculo real). Campo novo `IndicadorContabilDefinicao.formula_exibicao` (`CharField`, `blank=True`, migração `0064`) — texto **livre**, sem nenhuma validação de sintaxe (ao contrário de `formula`, nunca passa por `avalia_formula()`/`ast`), só pra exibição; editável no modal "Novo/Editar Indicador" logo abaixo do campo técnico, agora rotulado "Fórmula (cálculo interno)" pra deixar claro que só `formula_exibicao` é "Fórmula (como aparece ao cliente)". `_contabil_monta_cards_indicadores()` resolve o fallback no servidor: `definicao.formula_exibicao.strip() or definicao.formula` — indicador sem essa preferência preenchida (todo indicador criado antes desta rodada, os 11 migrados inclusive) continua mostrando a fórmula técnica até alguém preencher pela tela, nunca fica sem nenhuma fórmula visível. Nenhuma mudança em `formula` em si (continua a mesma expressão validada, usada só pro cálculo) nem em `IndicadorContabilComponente`.
Verificado rodando o relatório de verdade via `Client.force_login()` num shell (`manage.py shell`, apuração `id=4`) — sem servidor de desenvolvimento nenhum aberto: a estrutura nova (`dcr-card-inner`, ícone certo por `icone_svg`), o fallback de `formula_exibicao` pra `formula` quando em branco, e o texto customizado aparecendo no lugar quando preenchido — os dois últimos testados trocando o campo de um indicador real (EBIT) e revertendo logo em seguida, sem deixar resíduo em produção. Confirmado também que "Sem descrição cadastrada" no card de um indicador que já tem `descricao` salva não é bug: o relatório é uma foto estática do momento em que foi gerado — editar a definição depois não atualiza um relatório já aberto/baixado, precisa clicar "Gerar Dashboard" de novo.
**Dois ajustes de acabamento, mesma rodada**: (a) `.dcr-card__value` (valor da frente) de `1.55rem` pra `1.3rem` — valor negativo em moeda (`R$ -143.648,54`) quebrava linha com a fonte maior; (b) `.dcr-card-face__descricao` perdeu `flex:1; overflow-y:auto` (um scroll aninhado por cima do scroll que já existe em `.dcr-card-face--back`) — descrição e fórmula agora fluem juntas num único bloco, rolando como texto contínuo em vez de ficar a fórmula presa numa área separada cortada.
**A tela de revisão (`dashboard-contabil.js`) ganhou o mesmo destaque inicial do relatório**: `renderRevisao()` agora inicializa `dcContasDestaque`/`dcDreDestaque` como cópias de `dcContasColapsadas`/`dcDreColapsadas` (`new Set(dcContasColapsadas)`), em vez de `new Set()` vazio — mesmo raciocínio do ajuste do relatório (ver "Destaque replicado no relatório 'Gerar Dashboard'" acima): 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 nenhum clique. Reduz a poluição visual de abrir a tela inteira na cor de grupo/total (mesmo motivo que levou ao ajuste no relatório).
### Botão de observação virou sempre ícone + cor do destaque invertida (tema escuro)
Dois pedidos na sequência sobre a tela de revisão (Balancete/D.R.E.):
**Botão de observação**: mostrar o texto da observação já preenchida direto na tabela (comportamento desde sempre) também poluía a coluna, além do "+ Observação" (já resolvido numa rodada anterior, ver acima) — o usuário pediu pra nunca mostrar texto nenhum na tabela, só o ícone, mudando de cor conforme preenchida ou não. `PID_DC_OBSERVACAO_ICONE` (constante única, sem mais um branch vazio/preenchido) é sempre o conteúdo do botão agora; `.dc-conta-observacao-btn--preenchida` (classe condicional em `dashboard-contabil.js`, tanto em `renderContas()` quanto em `renderDre()`) é a única diferença — troca a cor do ícone de `--text-muted` pra `--accent`. O texto da observação continua acessível via `title`/tooltip (`"Adicionar observação"` ou o texto em si) e pelo clique, que abre o modal de edição normalmente — só sumiu da própria célula da tabela. `.dc-conta-observacao-btn` virou sempre um botão circular 26px (antes só a variante vazia era assim; a variante "preenchida" tinha texto truncado com `max-width`/`text-overflow`, removido).
**Cor do destaque no tema escuro invertida**: `--dc-destaque-bg` (linhas destacadas) e `--dc-row-tint-bg` (as demais) trocaram de papel — antes o destaque usava `--card-bg-hover` (mais claro) e o resto ficava transparente (mais escuro, revelando o fundo da tabela); o usuário pediu o oposto, destaque mais escuro e o resto mais claro. Agora `--dc-destaque-bg: var(--bg-canvas)` (o tom mais escuro do tema) e `--dc-row-tint-bg: var(--bg-surface-raised)` (mais claro que `--bg-canvas`/`--bg-surface`, mas ainda diferente de `--card-bg-hover` — usar o mesmo tom do hover deixaria o hover das linhas não-destacadas sem nenhum efeito visível, já que ficariam idênticas em repouso e ao passar o mouse). **Só o tema escuro mudou** — o tema claro já tinha sido invertido por pedido anterior do usuário (não-destaque colorido, destaque em branco) e continua como estava, sem relação com este pedido.
### Bug real: folha genuína ficava de fora do destaque inicial
Usuário reportou, testando a rodada anterior na D.R.E., que várias linhas-folha genuínas (`(-) DE VENDAS DE MERCADORIAS MERCADO INTERNO`, `(-) SIMPLES NACIONAL`, `DESCONTOS OBTIDOS`, várias linhas de resultado financeiro) não estavam com o destaque que deveriam ter, mesmo sendo visualmente "de baixo" quanto um grupo colapsado por padrão no mesmo nível. Causa: o destaque inicial usava só `dcContasColapsadas`/`dcDreColapsadas` (que só marca linha que **tem filho escondido**) — uma folha de verdade (sem filho nenhum, ex. `(-) SIMPLES NACIONAL`, código-fonte confirmado via `manage.py shell`: `ContabilLinhaDre.objects.get(id=503).nivel == 2`, sem nenhuma linha de `nivel=3` logo depois) nunca entra nesse conjunto, então nunca ganhava destaque, mesmo estando no mesmo nível visual que um grupo colapsado vizinho.
Critério corrigido (`dcUltimaLevaVisivel()` em `dashboard-contabil.js`, `calculaDestaqueInicial()` na cópia irmã em `dashboard-contabil-relatorio.html`, mesmo algoritmo nas duas): reconstrói a lista de linhas **realmente visíveis** dado o colapso padrão (mesma pilha de níveis usada pra ocultar descendente de linha colapsada) e marca como destaque toda linha cuja **próxima linha visível não seja mais profunda que ela** — cobre os dois casos com uma regra só: nada foi revelado logo abaixo dela agora, seja porque está colapsada (filhos escondidos) ou porque é uma folha sem filho nenhum. Validado manualmente contra os níveis reais da apuração `id=4` (`ContabilLinhaDre` ids 490-504) antes de aplicar — o algoritmo original (`= colapsadas`) incluía só 492/496 (grupos colapsados); o corrigido inclui também 501/503 (as duas folhas citadas pelo usuário), sem incluir 500/502 (grupos abertos com algo visível abaixo, corretamente fora do destaque).
### Flip card: texto centralizado, fonte menor, fórmula em fonte monoespaçada mais elegante
Três ajustes finos de acabamento no verso do flip card do relatório, pedido explícito do usuário com uma captura de tela do flip card equivalente que ele já usa no Power BI como referência: (a) `text-align:center` em `.dcr-card-face--back` — herdado por título/descrição/rótulo/fórmula, nenhum precisou de regra própria; (b) fontes um pouco menores (`.dcr-card-face__titulo` `0.8rem→0.74rem`, `__descricao` `0.78rem→0.7rem`, `__formula-label` `0.66rem→0.62rem`, `__formula` `0.74rem→0.7rem`); (c) fórmula ganhou "JetBrains Mono" (peso 500, adicionada ao mesmo `<link>` do Google Fonts já usado por Inter/Manrope nesta página), com `"Courier New", monospace` como fallback.
### Modal "Novo/Editar Indicador" ganhou confirmação ao fechar sem salvar
Pedido explícito do usuário: fechar o modal clicando fora (overlay) saía direto sem perguntar nada, arriscando perder o que já estava preenchido. `pidDcFecharIndicadorModalComConfirmacao()` (nova, `async`) chama `pidConfirm("Sair sem salvar as alterações?", { perigoso: true })` antes de fechar de verdade — ligada ao clique no overlay e ao botão "Cancelar", os dois únicos caminhos de fechar iniciados pelo usuário (o modal não tem um "X" próprio). `pidDcFecharIndicadorModal()` em si continua sem confirmação nenhuma, chamada direto pelos fluxos de sucesso (salvar/excluir) — não haveria o que descartar depois de uma ação já concluída. Mesmo padrão já usado no modal de "Mais informações" (`ajuda-aplicacao.js`) e no antigo "Fechar sem salvar" do Cadastro de Regras de Plano de Saúde (ver "Modal de confirmação genérico" no `CLAUDE.md` da raiz).
### Resumo do Fechamento — texto rico do contador antes dos indicadores
Pedido explícito do usuário, com um exemplo real de texto que a De Paula já manda ao cliente hoje por fora do Portal (carta com considerações/saldos/variações do fechamento) como referência do que deveria caber aqui. A aba "Indicadores" do relatório "Gerar Dashboard" (só cards de indicador até aqui) virou **"Resumo"** — nome da aba (`data-dcr-tab="indicadores"`, atributo mantido igual, só o rótulo visível mudou) e do card `dc-dash-section__titulo` na aba "Dashboard" da revisão — porque agora carrega duas coisas: o texto livre do contador (**"Resumo do Fechamento"**, sempre no topo) e, embaixo, os grupos de cards de indicador de sempre (inalterados).
**Model**: `ContabilApuracao.resumo_fechamento` (`TextField`, `blank=True`, migração `0065`) — texto rico (HTML sanitizado), mesma allowlist nh3 (`RICHTEXT_ALLOWED_TAGS`/`_ATTRS`/`_SCHEMES`) de `AcessoGeral.observacoes`/`AjudaAplicacao.texto`; `CONTABIL_RESUMO_FECHAMENTO_MAX_CHARS`/`validar_tamanho_resumo_fechamento_contabil` seguem o mesmo padrão "constante+validator por campo" já usado pelos outros dois (o projeto não tem um validator de tamanho de texto rico genérico, é copiado por campo de propósito).
**Backend**: `ContabilApuracaoViewSet` não tem PATCH genérico (`http_method_names` exclui `"patch"` de propósito, ver docstring da classe) — mesmo padrão de `indicadores_ocultos()`/`indicadores_selecionados()`, uma `@action` dedicada nova (`POST /api/contabil-apuracoes/{id}/resumo-fechamento/`, `resumo_fechamento()`) substitui o campo inteiro de uma vez, validado por `ContabilResumoFechamentoSerializer` (`validate_resumo_fechamento` chama `nh3.clean()`, mesmo `validate_texto`/`validate_observacoes` de `AjudaAplicacaoSerializer`/`AcessoGeralSerializer`), guardada por `_contabil_garante_em_revisao()` (não editável depois de "Concluída", igual ao resto da apuração). Campo exposto em leitura via `ContabilApuracaoDetailSerializer` (não no `ListSerializer` — só a tela de revisão precisa dele).
**Frontend — aba "Dashboard"** (`templates/dashboard-contabil.html`/`dashboard-contabil.js`): editor `contenteditable` com colar/arrastar imagem embutida — mesmo padrão de `.ag-richtext` (`acessos-gerais.js`)/`.ajuda-modal__editor` (`ajuda-aplicacao.js`), duplicado aqui de propósito (nenhum desses três é um componente compartilhado no projeto; `PID_DC_RESUMO_FECHAMENTO_IMAGE_MAX_BYTES = 2MB`, mesmo limite dos outros dois). Populado só em `renderRevisao()` (uma vez por apuração aberta) — **nunca** em `renderDashboardTab()` (que roda toda vez que o contador troca pra essa aba): resetar o `innerHTML` a cada troca de aba descartaria texto ainda não salvo, diferente do padrão de "recarrega sempre" que os cards de indicador/observações usam ali (esses não são editados inline na mesma tela). Sem botão "Cancelar"/toggle editar-vs-visualizar (diferente de `AjudaAplicacaoSerializer`) — é sempre editável enquanto a apuração está "Em revisão" (`contenteditable` alternado via JS conforme `status`), porque só o próprio contador vê esta tela, ao contrário do texto de "Mais informações" (lido por qualquer usuário, editado só pelo perfil "Inovação").
**Frontend — relatório** (`dashboard-contabil-relatorio.html`): `{{ apuracao.resumo_fechamento|safe }}` dentro de `.dcr-resumo-fechamento__texto`, condicionado a `{% if apuracao.resumo_fechamento %}` (nada renderiza se a apuração não tem resumo). **Primeiro `|safe` de template Django do projeto** — até agora todo texto rico só existia em tela SPA, injetado via `.innerHTML =` no JS, nunca por um template renderizado no servidor; seguro aqui pelo mesmo motivo de sempre (já vem sanitizado por nh3 antes de salvar, nunca cru do request). Seção com fundo/borda/sombra próprios (`.dcr-resumo-fechamento`, visual de card, diferente das outras `.dcr-secao` que são só agrupamento sem fundo) — se destaca do restante da aba antes dos grupos de indicador.
Testado de ponta a ponta via `Client.force_login()` num shell: `POST .../resumo-fechamento/` com um `<script>alert(1)</script>` embutido junto de HTML válido — confirmado que o nh3 removeu o `<script>` e manteve `<p>`/`<strong>`/`<ul>`/`<li>` intactos; conferido que o texto aparece no relatório antes de "Indicadores de Resultado" e que a seção some quando `resumo_fechamento` está vazio; revertido pro valor original (vazio) na apuração real (`id=4`) ao final do teste.
### Análise Vertical — nova aba na revisão e no relatório (rodada 123)
Pedido explícito do usuário: a Demonstração Mensal (Análise Vertical), até então ignorada pelo parser (ver "Extração do PDF" acima), passou a ser exibida tanto na tela de revisão quanto no relatório "Gerar Dashboard" — 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).
**Backend**: `ContabilLinhaAnaliseVerticalViewSet` (`GET`/`PATCH` em `/api/contabil-linhas-analise-vertical/{id}/`) é uma réplica exata de `ContabilLinhaDreViewSet` — mesmo `_contabil_garante_em_revisao()`, mesmo serializer com `valores` read-only. `ContabilApuracaoViewSet.create()` faz um terceiro `bulk_create` (depois de `ContabilConta`/`ContabilLinhaDre`) com as linhas de `resultado.extracao.linhas_analise_vertical`, e grava `analise_vertical_meses` já na criação da `ContabilApuracao` — vem pronto do parser, não precisa de nenhum cálculo adicional na view. `dashboard()` ganhou `linhas_analise_vertical`/`analise_vertical_meses`/`observacoes_analise_vertical` no contexto, reaproveitando `_contabil_arvore_contexto()` (mesma função da DRE, só que passando a lista/nivel_fn certos) — nenhuma função de árvore nova precisou ser escrita.
**Frontend — tela de revisão** (`dashboard-contabil.html`/`.js`): nova aba `data-dc-tab="analise-vertical"` (botão `#dc-av-tab-btn`, `hidden` quando `analise_vertical_meses` está vazio — relatório antigo sem essa seção não ganha uma aba com tabela vazia). `renderAnaliseVertical()` é uma réplica de `renderDre()` (mesmo algoritmo de pilha de colapso, mesmo tri-state de validado via `dcValidadoInfo()`/`dcClicarValidadoAv()` — as funções genéricas já existentes, `dcColapsoPadrao`/`dcUltimaLevaVisivel`/`dcFilhosDiretos`/`dcDescendentes`/`dcEstadoValidacaoGrupo`/`dcValidadoInfo`, já recebiam `itens`/`nivelFn` como parâmetro e foram 100% reaproveitadas sem alteração), só que cada `<td>` de valor vira **N pares** de colunas (Valor/Variação, uma dupla por mês) montadas a partir de `linha.valores` — o cabeçalho da tabela (`renderAnaliseVerticalHead()`) também é montado em JS porque o número de meses varia (normalmente 3, mas não é fixo). `pidDcFormatPercentualAnaliseVertical()` é um formatador **novo**, deliberadamente diferente de `pidDcFormatIndicadorPercentual()` — o percentual da Análise Vertical já vem "pronto" do PDF (ex. `"100.00"` quer dizer 100,00%), enquanto o dos cards de indicador é uma fração que precisa ser multiplicada por 100; reusar o errado exibiria `10000,00%`. Editor de observação inline + resumo de observações no fim da aba seguem exatamente o padrão de Balancete/D.R.E. (`dc-av-obs-*`).
**Se a apuração aberta não tem Análise Vertical mas a aba estava ativa** (ex.: usuário estava vendo essa aba de uma apuração anterior e abre uma sem essa seção) — `renderRevisao()` força a volta pra aba "Observações" nesse caso específico, pra não deixar um botão de aba escondido com o painel dele ainda visível.
**Frontend — relatório** (`dashboard-contabil-relatorio.html`): 4ª aba `data-dcr-tab="analise-vertical"`, entre "D.R.E." e "Resumo" — todo o bloco (botão + painel) fica dentro de `{% if analise_vertical_meses %}`, então some inteiro em relatórios de apurações antigas sem essa seção; `pidDcrArvore("dcr-av-body")`/`pidDcrObs("dcr-av-body")` (chamadas incondicionais no fim do script) já toleram um `tbody` inexistente (`if (!tbody) return;`, mesma guarda que essas duas funções já tinham). Reaproveita a mesma árvore recolhível/observação inline da DRE, sem nenhuma função nova. "Observações da Análise Vertical" tem sua própria seção no fim da aba, e a lista consolidada "Todas as Observações da Análise" (aba "Resumo") ganhou um 3º grupo (prefixo "Análise Vertical — ...", ícone de gráfico de barras) ao lado de Balancete/D.R.E./Auditoria — a mesma extensão simples de sempre, um `{% for %}` a mais.
Dois filtros de template novos em `contabil_extras.py` — `moeda_av`/`percentual_av` — porque `ContabilLinhaAnaliseVertical.valores` grava valor/percentual como **texto** (não `Decimal`, ver acima), e os filtros `moeda`/`percentual` existentes não servem: `moeda` faria `f"{valor:,.2f}"` falhar contra uma string, e `percentual` multiplicaria por 100 (pensado pra fração, não pra um percentual "já pronto" como o desta seção).
**Validado ponta a ponta via `Client.force_login()` num shell**, contra o PDF de referência real (`792 - balancete 072026.pdf`, empresa 0792/WEITNAUER, competência 07/2026): `POST /api/contabil-apuracoes/` extraiu 154 linhas de Análise Vertical (3 meses: mai/jun/jul de 2026) além das 177 contas/163 linhas de DRE de sempre; `GET .../dashboard/` gerou o relatório com a aba "Análise Vertical" presente; `PATCH` numa linha (observação + `oculta_no_relatorio=False` + `validado=True`) persistiu corretamente e a observação apareceu no relatório gerado em seguida. Toda apuração/arquivo criados durante o teste foram apagados ao final, sem deixar resíduo em produção.

View File

@ -1,14 +1,14 @@
# Dashboard Contábil (Relatórios > Contabilidade)
# Relatório Contábil (Relatórios > Contabilidade)
> Ver `CLAUDE.md` nesta mesma pasta para o detalhamento técnico (extração do PDF, regras de auditoria, models). Ver `CLAUDE.md` na raiz para arquitetura geral/transversal do Portal.
> Ver `CLAUDE.md` nesta mesma pasta para o detalhamento técnico (extração do PDF, regras de auditoria, models). Ver `CLAUDE.md` na raiz para arquitetura geral/transversal do Portal. Chamado de "Dashboard Contábil" até uma rodada anterior — renomeado a pedido do usuário (rótulo visível só, nomes técnicos internos continuam `dashboard_contabil`/`Contabil*`).
Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil: 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 automaticamente e roda um conjunto de checagens de auditoria (saldos negativos de caixa, contas transitórias/genéricas com saldo, contas que deveriam estar zeradas, diferença débito/crédito, variações atípicas mês a mês) — o contador revisa os achados, registra observações por conta e conclui a análise. O botão "Gerar Dashboard" gera um relatório HTML autocontido (indicadores financeiros, gráfico de evolução, DRE/Balancete agrupados, observações do contador) com a marca do escritório, pronto pra enviar ao administrador da empresa.
Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil: 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 automaticamente e roda um conjunto de checagens de auditoria (saldos negativos de caixa, contas transitórias/genéricas com saldo, contas que deveriam estar zeradas, diferença débito/crédito, variações atípicas mês a mês) — o contador revisa os achados, marca contas/linhas como validadas, registra observações por conta/linha (editor inline na própria tabela, com um toggle "mostrar ao cliente" que nasce desmarcado), escreve um resumo livre do fechamento (considerações/análises, texto rico com imagem) e conclui a análise. Quando o PDF traz a seção "Demonstração Mensal (Análise Vertical)" (histórico de 3 meses, valor+variação percentual por linha), ela também é extraída e ganha sua própria aba de revisão (mesmos recursos de observação/validado/ocultar do Balancete/D.R.E.). O botão "Gerar Relatório" gera um relatório HTML autocontido (resumo do fechamento, indicadores financeiros, gráfico de evolução, DRE/Balancete/Análise Vertical agrupados, observações do contador) com a marca do escritório, pronto pra enviar ao administrador da empresa.
**Ainda fora de escopo**: exportação em XLSX; consolidação entre várias empresas/competências ao mesmo tempo (é sempre uma apuração por vez).
## Onde mexer
- `portal_api/dashboard_contabil/` — `parser.py` (extração do PDF via `pdfplumber`), `regras.py` (motor de regras de auditoria), `indicadores.py` (indicadores financeiros do relatório), `pipeline.py` (orquestração), `modelos.py` (dataclasses).
- `ContabilApuracao`/`ContabilConta`/`ContabilLinhaDre`/`ContabilAchado` (`portal_api/models.py`).
- `ContabilApuracao`/`ContabilConta`/`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical`/`ContabilAchado` (`portal_api/models.py`).
- `dashboard-contabil.html` / `static/js/dashboard-contabil.js` / `static/css/dashboard-contabil.css`.
- `dashboard-contabil-relatorio.html` (relatório em si) / `portal_api/templatetags/contabil_extras.py` (filtros de formatação).

View File

@ -40,11 +40,33 @@ class LinhaDreExtraida:
totalizador: bool
@dataclass
class ValorMensalAnaliseVertical:
valor: Decimal
percentual: Decimal
@dataclass
class LinhaAnaliseVerticalExtraida:
"""Uma linha da seção "Demonstração Mensal (Análise Vertical)" — mesma
descrição/nível/negrito de `LinhaDreExtraida` (é a mesma árvore da DRE),
só que com um valor+percentual por mês em vez de um valor único (ver
`parser.py`)."""
ordem: int
descricao: str
nivel: int
totalizador: bool
valores: list[ValorMensalAnaliseVertical]
@dataclass
class ResultadoExtracao:
cabecalho: CabecalhoExtraido
contas: list[LinhaBalanceteExtraida]
linhas_dre: list[LinhaDreExtraida]
meses_analise_vertical: list[str] = field(default_factory=list)
linhas_analise_vertical: list[LinhaAnaliseVerticalExtraida] = field(default_factory=list)
@dataclass

View File

@ -24,7 +24,14 @@ from typing import IO
import pdfplumber
from .modelos import CabecalhoExtraido, LinhaBalanceteExtraida, LinhaDreExtraida, ResultadoExtracao
from .modelos import (
CabecalhoExtraido,
LinhaAnaliseVerticalExtraida,
LinhaBalanceteExtraida,
LinhaDreExtraida,
ResultadoExtracao,
ValorMensalAnaliseVertical,
)
# Vão horizontal (em pontos) acima do qual dois caracteres consecutivos são
# tratados como pertencendo a campos/palavras diferentes. Calibrado contra o
@ -42,6 +49,22 @@ _RE_CNPJ = re.compile(r"CNPJ:\s*([\d./-]+)")
_RE_PERIODO = re.compile(r"Per[ií]odo:\s*(\d{2}/\d{2}/\d{4})\s*a\s*(\d{2}/\d{2}/\d{4})")
_RE_CODIGO_NOME_EMPRESA = re.compile(r"^(?P<codigo>\d+)\s+(?P<nome>.+)$")
# Uma linha da Análise Vertical repete N pares "Valor Variação" (um por mês
# mostrado) em vez do valor único da DRE. O valor às vezes traz um espaço
# residual antes do parêntese de fechamento (ex. "(552.338,51 )") — artefato
# do mesmo gerador de relatório, `Decimal()` já tolera esse espaço; a
# variação é sempre um percentual com "%" colado ao número. Calibrado contra
# `792 - balancete 072026.pdf` (mesmo arquivo de referência da DRE/Balancete).
_TOKEN_VALOR_ANALISE_VERTICAL = r"\(?-?[\d.]+,\d{2}\s?\)?"
_TOKEN_PERCENTUAL_ANALISE_VERTICAL = r"\(?-?[\d.]+,\d{2}\s?%\s?\)?"
_RE_PAR_VALOR_VARIACAO = re.compile(
rf"({_TOKEN_VALOR_ANALISE_VERTICAL})\s+({_TOKEN_PERCENTUAL_ANALISE_VERTICAL})"
)
# Cabeçalho de mês da Análise Vertical, ex. "mai - 2026 jun - 2026 jul - 2026"
# — capturado uma única vez (primeira página da seção), as páginas seguintes
# repetem o mesmo cabeçalho a cada quebra de página.
_RE_MES_ANALISE_VERTICAL = re.compile(r"([A-Za-z]{3})\s*-\s*(\d{4})")
# Linhas totalizadoras conhecidas da DRE (usadas por regras que precisam de
# uma linha específica, ex. razão custo/receita) — além do negrito (que já
# identifica visualmente todo totalizador pro frontend), casar por texto é
@ -84,6 +107,13 @@ def _para_decimal(texto: str) -> Decimal:
return -valor if negativo else valor
def _para_percentual(texto: str) -> Decimal:
"""Mesma conversão de `_para_decimal`, só removendo o "%" antes — o sinal
negativo continua expresso só pelo parêntese (ex. "(7,33%)"), igual ao
valor monetário."""
return _para_decimal(texto.replace("%", ""))
def _para_data(texto: str) -> date:
dia, mes, ano = texto.split("/")
return date(int(ano), int(mes), int(dia))
@ -156,18 +186,25 @@ def extrai_balancete_dre(origem: str | IO[bytes]) -> ResultadoExtracao:
sobre o upload, antes de saber `codigo_empresa`/`competencia` (extraídos
daqui), então ainda não há um caminho em disco definitivo nesse momento.
As páginas finais de "Demonstração Mensal (Análise Vertical)", quando
presentes, são ignoradas de propósito — o Dashboard Contábil calcula
variação mês a mês a partir do próprio histórico de apurações já
processadas no Portal, não a partir desse relatório complementar (ver
CLAUDE.md/plano.md)."""
presentes, também são extraídas (`linhas_analise_vertical`/
`meses_analise_vertical`) — é a mesma árvore da DRE, só que com um
valor+percentual por mês em vez de um valor único; **não** substitui o
histórico próprio do Portal usado pelas regras de variação mês a mês
(`regras.py` continua comparando contra apurações anteriores já
persistidas, não contra esta seção), só alimenta a aba/relatório
"Análise Vertical" com os mesmos meses que o próprio Questor já calcula."""
cabecalho: CabecalhoExtraido | None = None
contas: list[LinhaBalanceteExtraida] = []
linhas_dre: list[LinhaDreExtraida] = []
linhas_analise_vertical: list[LinhaAnaliseVerticalExtraida] = []
meses_analise_vertical: list[str] | None = None
secao = "cabecalho"
ordem_dre = 0
ordem_av = 0
base_x0_dre: float | None = None
base_x0_av: float | None = None
with pdfplumber.open(origem) as pdf:
if not pdf.pages:
@ -185,9 +222,17 @@ def extrai_balancete_dre(origem: str | IO[bytes]) -> ResultadoExtracao:
continue
if texto.startswith(_TITULO_ANALISE_VERTICAL):
secao = "ignorar"
break
if secao != "dre" and texto.startswith(_TITULO_DRE):
if secao != "analise_vertical":
secao = "analise_vertical"
base_x0_av = None
continue
# Guarda só contra cabeçalho/balancete pra não disparar de
# novo quando o título da DRE se repete no topo de cada
# página já dentro da seção "dre" (comportamento de sempre)
# ou da Análise Vertical, que também reimprime esse título —
# sem essa restrição, `secao` voltaria de "analise_vertical"
# pra "dre" a cada quebra de página da Análise Vertical.
if secao in ("cabecalho", "balancete") and texto.startswith(_TITULO_DRE):
secao = "dre"
base_x0_dre = None
continue
@ -243,12 +288,59 @@ def extrai_balancete_dre(origem: str | IO[bytes]) -> ResultadoExtracao:
totalizador=linha["negrito"],
)
)
if secao == "ignorar":
break
elif secao == "analise_vertical":
# O cabeçalho de mês ("mai - 2026 jun - 2026 jul - 2026")
# só existe uma vez de verdade — repete a cada quebra de
# página, mas só a primeira ocorrência importa (captura
# os meses e não é tratada como linha de dado, já que
# não tem par valor+variação nenhum). Qualquer outra
# linha sem nenhum par (título repetido, cabeçalho de
# coluna "Descrição/Valor/Variação") é ignorada do mesmo
# jeito que a DRE ignora linhas sem valor monetário.
if meses_analise_vertical is None:
candidatos_mes = _RE_MES_ANALISE_VERTICAL.findall(texto)
if candidatos_mes:
meses_analise_vertical = [
f"{mes.lower()}/{ano}" for mes, ano in candidatos_mes
]
continue
pares = _RE_PAR_VALOR_VARIACAO.findall(texto)
if not pares:
continue
posicao = texto.find(pares[0][0])
if posicao == -1:
continue
descricao = texto[:posicao].strip()
if not descricao:
continue
if base_x0_av is None:
base_x0_av = linha["x0"]
nivel = max(0, round((linha["x0"] - base_x0_av) / 7.0))
ordem_av += 1
linhas_analise_vertical.append(
LinhaAnaliseVerticalExtraida(
ordem=ordem_av,
descricao=descricao,
nivel=nivel,
totalizador=linha["negrito"],
valores=[
ValorMensalAnaliseVertical(
valor=_para_decimal(valor_texto), percentual=_para_percentual(percentual_texto)
)
for valor_texto, percentual_texto in pares
],
)
)
if not contas:
raise ExtracaoInvalidaError("Nenhuma linha de balancete encontrada no PDF.")
if not linhas_dre:
raise ExtracaoInvalidaError("Nenhuma linha de DRE encontrada no PDF.")
return ResultadoExtracao(cabecalho=cabecalho, contas=contas, linhas_dre=linhas_dre)
return ResultadoExtracao(
cabecalho=cabecalho,
contas=contas,
linhas_dre=linhas_dre,
meses_analise_vertical=meses_analise_vertical or [],
linhas_analise_vertical=linhas_analise_vertical,
)

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0.7 on 2026-09-10 17:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0062_indicadorcontabildefinicao_grupo_and_more'),
]
operations = [
migrations.AddField(
model_name='indicadorcontabildefinicao',
name='icone',
field=models.CharField(choices=[('barras', 'Barras'), ('tendencia-alta', 'Tendência de alta'), ('tendencia-baixa', 'Tendência de baixa'), ('percentual', 'Percentual'), ('pizza', 'Gráfico de pizza'), ('atividade', 'Atividade'), ('moeda', 'Cifrão'), ('alvo', 'Alvo/meta'), ('camadas', 'Camadas'), ('cartao', 'Cartão'), ('selo', 'Selo/prêmio')], default='barras', max_length=20, verbose_name='Ícone'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 6.0.7 on 2026-09-10 17:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0063_indicadorcontabildefinicao_icone'),
]
operations = [
migrations.AddField(
model_name='indicadorcontabildefinicao',
name='formula_exibicao',
field=models.CharField(blank=True, max_length=300, verbose_name='Fórmula (como aparece ao cliente)'),
),
migrations.AlterField(
model_name='indicadorcontabildefinicao',
name='formula',
field=models.CharField(max_length=500, verbose_name='Fórmula (cálculo)'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 6.0.7 on 2026-09-10 18:48
import portal_api.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0064_indicadorcontabildefinicao_formula_exibicao_and_more'),
]
operations = [
migrations.AddField(
model_name='contabilapuracao',
name='resumo_fechamento',
field=models.TextField(blank=True, validators=[portal_api.models.validar_tamanho_resumo_fechamento_contabil], verbose_name='Resumo do Fechamento'),
),
]

View File

@ -0,0 +1,57 @@
# Generated by Django 6.0.7 on 2026-09-10 19:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0065_contabilapuracao_resumo_fechamento'),
]
operations = [
migrations.AlterModelOptions(
name='contabilachado',
options={'ordering': ['-severidade', 'id'], 'verbose_name': 'Achado do Relatório Contábil', 'verbose_name_plural': 'Achados do Relatório Contábil'},
),
migrations.AlterModelOptions(
name='contabilapuracao',
options={'ordering': ['-competencia', '-criado_em'], 'verbose_name': 'Apuração do Relatório Contábil', 'verbose_name_plural': 'Apurações do Relatório Contábil'},
),
migrations.AlterModelOptions(
name='contabilconta',
options={'ordering': ['ordem'], 'verbose_name': 'Conta do Balancete (Relatório Contábil)', 'verbose_name_plural': 'Contas do Balancete (Relatório Contábil)'},
),
migrations.AlterModelOptions(
name='contabillinhadre',
options={'ordering': ['ordem'], 'verbose_name': 'Linha da DRE (Relatório Contábil)', 'verbose_name_plural': 'Linhas da DRE (Relatório Contábil)'},
),
migrations.AlterModelOptions(
name='indicadorcontabilcomponente',
options={'ordering': ['id'], 'verbose_name': 'Componente de indicador (Relatório Contábil)', 'verbose_name_plural': 'Componentes de indicador (Relatório Contábil)'},
),
migrations.AlterModelOptions(
name='indicadorcontabildefinicao',
options={'ordering': ['nome'], 'verbose_name': 'Indicador do Relatório Contábil (personalizado)', 'verbose_name_plural': 'Indicadores do Relatório Contábil (personalizados)'},
),
migrations.AddField(
model_name='contabilconta',
name='validado',
field=models.BooleanField(default=False, verbose_name='Validado pelo contador'),
),
migrations.AddField(
model_name='contabillinhadre',
name='validado',
field=models.BooleanField(default=False, verbose_name='Validado pelo contador'),
),
migrations.AlterField(
model_name='contabilconta',
name='oculta_no_relatorio',
field=models.BooleanField(default=True, verbose_name='Observação oculta no relatório'),
),
migrations.AlterField(
model_name='contabillinhadre',
name='oculta_no_relatorio',
field=models.BooleanField(default=True, verbose_name='Observação oculta no relatório'),
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 6.0.7 on 2026-09-11 12:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0066_alter_contabilachado_options_and_more'),
]
operations = [
migrations.AddField(
model_name='contabilapuracao',
name='analise_vertical_meses',
field=models.JSONField(blank=True, default=list, verbose_name='Meses da Análise Vertical'),
),
migrations.CreateModel(
name='ContabilLinhaAnaliseVertical',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ordem', models.IntegerField(verbose_name='Ordem')),
('descricao', models.CharField(max_length=255, verbose_name='Descrição')),
('nivel', models.IntegerField(default=0, verbose_name='Nível de indentação')),
('totalizador', models.BooleanField(default=False, verbose_name='Linha totalizadora')),
('valores', models.JSONField(default=list, verbose_name='Valores mensais')),
('observacao', models.TextField(blank=True, verbose_name='Observação')),
('oculta_no_relatorio', models.BooleanField(default=True, verbose_name='Observação oculta no relatório')),
('validado', models.BooleanField(default=False, verbose_name='Validado pelo contador')),
('apuracao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='linhas_analise_vertical', to='portal_api.contabilapuracao')),
],
options={
'verbose_name': 'Linha da Análise Vertical (Relatório Contábil)',
'verbose_name_plural': 'Linhas da Análise Vertical (Relatório Contábil)',
'ordering': ['ordem'],
},
),
]

View File

@ -16,6 +16,7 @@ from .custo_contratacao.calculo import ParametrosFiscais
LINK_FERRAMENTA_ICONE_MAX_BYTES = 2 * 1024 * 1024
ACESSO_GERAL_OBSERVACOES_MAX_CHARS = 2_000_000
AJUDA_APLICACAO_TEXTO_MAX_CHARS = 2_000_000
CONTABIL_RESUMO_FECHAMENTO_MAX_CHARS = 2_000_000
PLANO_SAUDE_ARQUIVO_MAX_BYTES = 15 * 1024 * 1024
COMPROMISSO_HORARIO_COMERCIAL_INICIO = time(8, 0)
@ -65,6 +66,11 @@ def validar_tamanho_texto_ajuda_aplicacao(valor: str) -> None:
raise ValidationError("O texto (com imagens embutidas) ficou grande demais.")
def validar_tamanho_resumo_fechamento_contabil(valor: str) -> None:
if len(valor) > CONTABIL_RESUMO_FECHAMENTO_MAX_CHARS:
raise ValidationError("O resumo do fechamento (com imagens embutidas) ficou grande demais.")
def validar_cor_categoria_evento(valor: str) -> None:
if not re.fullmatch(r"#[0-9A-Fa-f]{6}", valor):
raise ValidationError("A cor deve estar no formato hexadecimal, ex.: #7c4dff.")
@ -1954,7 +1960,7 @@ def validar_tamanho_arquivo_contabil(arquivo: File) -> None:
class ContabilApuracao(models.Model):
"""Uma análise do Dashboard Contábil (Relatórios > Contabilidade): o
"""Uma análise do Relatório Contábil (Relatórios > Contabilidade): o
contador anexa o PDF de Balancete + DRE (leiaute Questor, mesmo relatório
hoje enviado ao cliente) e o pipeline (`portal_api.dashboard_contabil.
pipeline.processa_apuracao`) extrai as contas/linhas da DRE e roda o
@ -1997,10 +2003,27 @@ class ContabilApuracao(models.Model):
# (ver "Gerenciar Indicadores" na aba "Dashboard"). Indicador padrao=True
# não precisa (nem deveria) aparecer aqui, já entra sempre.
indicadores_selecionados = models.JSONField("Indicadores não padrão selecionados", default=list, blank=True)
# Texto rico (HTML sanitizado por nh3, mesmo allowlist de
# AcessoGeral.observacoes/AjudaAplicacao.texto — ver
# RICHTEXT_ALLOWED_TAGS/_ATTRS/_SCHEMES em serializers.py) que o contador
# escreve na aba "Dashboard" da revisão — considerações/análises livres
# sobre o fechamento, aparece no relatório "Gerar Dashboard" antes dos
# cards de indicador. Sanitizado em ContabilResumoFechamentoSerializer,
# nunca aceito cru do request.
resumo_fechamento = models.TextField(
"Resumo do Fechamento", blank=True, validators=[validar_tamanho_resumo_fechamento_contabil]
)
# Rótulos dos meses da seção "Demonstração Mensal (Análise Vertical)" do
# PDF (ex.: ["mai/2026", "jun/2026", "jul/2026"]), na mesma ordem/posição
# dos valores de cada ContabilLinhaAnaliseVertical.valores — compartilhado
# aqui (não por linha) porque é sempre o mesmo conjunto de meses pra toda
# a apuração. Lista vazia quando o PDF não tinha essa seção (relatório
# mais antigo, ou empresa sem essa seção habilitada no Questor).
analise_vertical_meses = models.JSONField("Meses da Análise Vertical", default=list, blank=True)
class Meta:
verbose_name = "Apuração do Dashboard Contábil"
verbose_name_plural = "Apurações do Dashboard Contábil"
verbose_name = "Apuração do Relatório Contábil"
verbose_name_plural = "Apurações do Relatório Contábil"
ordering = ["-competencia", "-criado_em"]
constraints = [
models.UniqueConstraint(fields=["codigo_empresa", "competencia"], name="contabil_apuracao_unica")
@ -2037,12 +2060,20 @@ class ContabilConta(models.Model):
observacao = models.TextField("Observação", blank=True)
# Não afeta a linha do Balancete em si (continua listada normalmente) —
# só remove a observação da seção "Observações" do relatório "Gerar
# Dashboard" (ver aba "Dashboard" da tela de revisão).
oculta_no_relatorio = models.BooleanField("Observação oculta no relatório", default=False)
# Dashboard" (ver aba "Dashboard" da tela de revisão). Default True
# (oculta) — pedido explícito do usuário: uma observação nova só vai pro
# relatório do cliente depois que o contador confirmar isso explicitamente
# no editor inline da tabela, nunca por padrão.
oculta_no_relatorio = models.BooleanField("Observação oculta no relatório", default=True)
# Checkbox de "já revisei esta conta" — puramente informativo (não afeta
# achados/status/relatório), pedido explícito do usuário como um segundo
# botão ao lado do de observação, pra marcar contas já conferidas durante
# a revisão.
validado = models.BooleanField("Validado pelo contador", default=False)
class Meta:
verbose_name = "Conta do Balancete (Dashboard Contábil)"
verbose_name_plural = "Contas do Balancete (Dashboard Contábil)"
verbose_name = "Conta do Balancete (Relatório Contábil)"
verbose_name_plural = "Contas do Balancete (Relatório Contábil)"
ordering = ["ordem"]
def __str__(self) -> str:
@ -2062,12 +2093,45 @@ class ContabilLinhaDre(models.Model):
totalizador = models.BooleanField("Linha totalizadora", default=False)
observacao = models.TextField("Observação", blank=True)
# Mesmo espírito de ContabilConta.oculta_no_relatorio — só afeta a
# seção "Observações" do relatório "Gerar Dashboard".
oculta_no_relatorio = models.BooleanField("Observação oculta no relatório", default=False)
# seção "Observações" do relatório "Gerar Dashboard". Default True
# (oculta), mesmo motivo documentado lá.
oculta_no_relatorio = models.BooleanField("Observação oculta no relatório", default=True)
# Mesmo espírito de ContabilConta.validado.
validado = models.BooleanField("Validado pelo contador", default=False)
class Meta:
verbose_name = "Linha da DRE (Dashboard Contábil)"
verbose_name_plural = "Linhas da DRE (Dashboard Contábil)"
verbose_name = "Linha da DRE (Relatório Contábil)"
verbose_name_plural = "Linhas da DRE (Relatório Contábil)"
ordering = ["ordem"]
def __str__(self) -> str:
return f"{self.descricao}"
class ContabilLinhaAnaliseVertical(models.Model):
"""Uma linha da seção "Demonstração Mensal (Análise Vertical)" do mesmo
PDF — mesma árvore/descrição/nível da DRE (`ContabilLinhaDre`), só que
com um valor+percentual por mês em vez de um valor único, por isso
`valores` é uma lista (não um `DecimalField` só). Cada posição da lista
corresponde à mesma posição em `ContabilApuracao.analise_vertical_meses`
(ex.: `valores[0]` é o mês `analise_vertical_meses[0]`). Gravado como
texto (não float), pra não perder precisão decimal em nenhuma conversão
— `[{"valor": "1234.56", "percentual": "12.34"}, ...]`."""
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="linhas_analise_vertical")
ordem = models.IntegerField("Ordem")
descricao = models.CharField("Descrição", max_length=255)
nivel = models.IntegerField("Nível de indentação", default=0)
totalizador = models.BooleanField("Linha totalizadora", default=False)
valores = models.JSONField("Valores mensais", default=list)
observacao = models.TextField("Observação", blank=True)
# Mesmo espírito de ContabilLinhaDre.oculta_no_relatorio/.validado.
oculta_no_relatorio = models.BooleanField("Observação oculta no relatório", default=True)
validado = models.BooleanField("Validado pelo contador", default=False)
class Meta:
verbose_name = "Linha da Análise Vertical (Relatório Contábil)"
verbose_name_plural = "Linhas da Análise Vertical (Relatório Contábil)"
ordering = ["ordem"]
def __str__(self) -> str:
@ -2122,8 +2186,8 @@ class ContabilAchado(models.Model):
oculto_no_relatorio = models.BooleanField("Observação oculta no relatório", default=False)
class Meta:
verbose_name = "Achado do Dashboard Contábil"
verbose_name_plural = "Achados do Dashboard Contábil"
verbose_name = "Achado do Relatório Contábil"
verbose_name_plural = "Achados do Relatório Contábil"
ordering = ["-severidade", "id"]
def __str__(self) -> str:
@ -2131,7 +2195,7 @@ class ContabilAchado(models.Model):
class IndicadorContabilDefinicao(models.Model):
"""Um indicador financeiro do Dashboard Contábil — todo indicador é um
"""Um indicador financeiro do Relatório Contábil — todo indicador é um
registro aqui agora, inclusive os antigos 11 "de sistema" (ROA/ROE/
Kanitz/EBIT/EBITDA/Liquidez.../Composição.../Grau.../IPL), migrados pra
cá numa rodada posterior à criação deste modelo (a pedido explícito do
@ -2158,11 +2222,58 @@ class IndicadorContabilDefinicao(models.Model):
(FORMATO_INDICE, "Índice"),
]
# Ícone do card no relatório "Gerar Dashboard" — só uma chave curta aqui;
# o desenho (SVG) de cada opção fica em duas cópias mantidas em sincronia
# "à mão", nunca geradas uma a partir da outra: `_CONTABIL_ICONES_SVG`
# (portal_api/views.py, monta o card do relatório) e
# `PID_DC_INDICADOR_ICONES` (static/js/dashboard-contabil.js, desenha o
# seletor de ícone no modal de cadastro). Essa duplicação é proposital,
# mesmo espírito de outras duplicações entre o relatório autocontido e o
# resto do Portal (ver "Origem do arquivo" em CLAUDE.md sobre a intro do
# login) — o relatório é HTML puro servido pelo Django, sem acesso ao JS
# do app, e a tela de cadastro é só JS/HTML estático (sem contexto de
# servidor), então não dá pra ter uma fonte única sem inventar mais uma
# ida ao backend só pra isso.
ICONE_BARRAS = "barras"
ICONE_TENDENCIA_ALTA = "tendencia-alta"
ICONE_TENDENCIA_BAIXA = "tendencia-baixa"
ICONE_PERCENTUAL = "percentual"
ICONE_PIZZA = "pizza"
ICONE_ATIVIDADE = "atividade"
ICONE_MOEDA = "moeda"
ICONE_ALVO = "alvo"
ICONE_CAMADAS = "camadas"
ICONE_CARTAO = "cartao"
ICONE_SELO = "selo"
ICONE_CHOICES = [
(ICONE_BARRAS, "Barras"),
(ICONE_TENDENCIA_ALTA, "Tendência de alta"),
(ICONE_TENDENCIA_BAIXA, "Tendência de baixa"),
(ICONE_PERCENTUAL, "Percentual"),
(ICONE_PIZZA, "Gráfico de pizza"),
(ICONE_ATIVIDADE, "Atividade"),
(ICONE_MOEDA, "Cifrão"),
(ICONE_ALVO, "Alvo/meta"),
(ICONE_CAMADAS, "Camadas"),
(ICONE_CARTAO, "Cartão"),
(ICONE_SELO, "Selo/prêmio"),
]
chave = models.SlugField("Chave", max_length=60, unique=True)
nome = models.CharField("Nome", max_length=120)
descricao = models.TextField("Descrição", blank=True)
formula = models.CharField("Fórmula", max_length=500)
formula = models.CharField("Fórmula (cálculo)", max_length=500)
# Texto livre, independente de `formula` — pedido explícito do usuário
# depois de ver a fórmula técnica (`resultado_liquido - despesas_financeiras`,
# variáveis internas em snake_case) aparecendo pro cliente no relatório
# "Gerar Dashboard". Em branco (padrão pra todo indicador já cadastrado
# antes deste campo existir) cai pra `formula` mesmo, ver
# `_contabil_monta_cards_indicadores()` em views.py — nunca fica
# sem nenhuma fórmula exibida, só sem a versão "bonita" até alguém
# preencher pela tela.
formula_exibicao = models.CharField("Fórmula (como aparece ao cliente)", max_length=300, blank=True)
formato = models.CharField("Formato de exibição", max_length=12, choices=FORMATO_CHOICES, default=FORMATO_INDICE)
icone = models.CharField("Ícone", max_length=20, choices=ICONE_CHOICES, default=ICONE_BARRAS)
# Livre, só pra agrupar visualmente no relatório/aba "Dashboard" (ver
# `indicadores_personalizados`/`renderDashboardIndicadores()`) — não
# exposto no formulário de criar/editar (sempre "Indicadores
@ -2184,8 +2295,8 @@ class IndicadorContabilDefinicao(models.Model):
atualizado_em = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Indicador do Dashboard Contábil (personalizado)"
verbose_name_plural = "Indicadores do Dashboard Contábil (personalizados)"
verbose_name = "Indicador do Relatório Contábil (personalizado)"
verbose_name_plural = "Indicadores do Relatório Contábil (personalizados)"
ordering = ["nome"]
def __str__(self) -> str:
@ -2252,8 +2363,8 @@ class IndicadorContabilComponente(models.Model):
# e outra.
class Meta:
verbose_name = "Componente de indicador (Dashboard Contábil)"
verbose_name_plural = "Componentes de indicador (Dashboard Contábil)"
verbose_name = "Componente de indicador (Relatório Contábil)"
verbose_name_plural = "Componentes de indicador (Relatório Contábil)"
ordering = ["id"]
constraints = [
models.UniqueConstraint(fields=["definicao", "chave"], name="contabil_componente_chave_unica")

View File

@ -19,6 +19,7 @@ from .models import (
ContabilAchado,
ContabilApuracao,
ContabilConta,
ContabilLinhaAnaliseVertical,
ContabilLinhaDre,
Departamento,
EmpresaQuestor,
@ -2247,6 +2248,7 @@ class ContabilContaSerializer(serializers.ModelSerializer):
"saldo_atual",
"observacao",
"oculta_no_relatorio",
"validado",
]
read_only_fields = ["id", "conta_numero", "codigo", "descricao", "tipo", "saldo_anterior", "debito", "credito", "saldo_atual"]
@ -2254,10 +2256,17 @@ class ContabilContaSerializer(serializers.ModelSerializer):
class ContabilLinhaDreSerializer(serializers.ModelSerializer):
class Meta:
model = ContabilLinhaDre
fields = ["id", "ordem", "descricao", "nivel", "valor", "totalizador", "observacao", "oculta_no_relatorio"]
fields = ["id", "ordem", "descricao", "nivel", "valor", "totalizador", "observacao", "oculta_no_relatorio", "validado"]
read_only_fields = ["id", "ordem", "descricao", "nivel", "valor", "totalizador"]
class ContabilLinhaAnaliseVerticalSerializer(serializers.ModelSerializer):
class Meta:
model = ContabilLinhaAnaliseVertical
fields = ["id", "ordem", "descricao", "nivel", "totalizador", "valores", "observacao", "oculta_no_relatorio", "validado"]
read_only_fields = ["id", "ordem", "descricao", "nivel", "totalizador", "valores"]
class ContabilAchadoSerializer(serializers.ModelSerializer):
conta_codigo = serializers.CharField(source="conta.codigo", read_only=True, default=None)
conta_descricao = serializers.CharField(source="conta.descricao", read_only=True, default=None)
@ -2362,6 +2371,26 @@ class ContabilIndicadoresSelecionadosSerializer(serializers.Serializer):
return [chave for chave in chaves if chave in chaves_nao_padrao]
class ContabilResumoFechamentoSerializer(serializers.Serializer):
"""Corpo de `ContabilApuracaoViewSet.resumo_fechamento()` — texto rico
(mesmo editor contenteditable + allowlist nh3 de
`AcessoGeral.observacoes`/`AjudaAplicacao.texto`) que o contador escreve
na aba "Dashboard" da revisão, aparece no relatório "Gerar Dashboard"
antes dos cards de indicador. Substitui o campo inteiro de uma vez,
mesmo espírito dos outros dois serializers acima — não há conceito de
edição incremental num texto livre."""
resumo_fechamento = serializers.CharField(allow_blank=True)
def validate_resumo_fechamento(self, value: str) -> str:
return nh3.clean(
value,
tags=RICHTEXT_ALLOWED_TAGS,
attributes=RICHTEXT_ALLOWED_ATTRS,
url_schemes=RICHTEXT_ALLOWED_SCHEMES,
)
class IndicadorContabilComponenteSerializer(serializers.ModelSerializer):
"""Só leitura — usada aninhada em `IndicadorContabilDefinicaoSerializer`
pra mostrar os componentes já salvos de um indicador personalizado.
@ -2402,7 +2431,9 @@ class IndicadorContabilDefinicaoSerializer(serializers.ModelSerializer):
"nome",
"descricao",
"formula",
"formula_exibicao",
"formato",
"icone",
"grupo",
"padrao",
"criado_por_nome",
@ -2438,7 +2469,13 @@ class IndicadorContabilDefinicaoInputSerializer(serializers.Serializer):
nome = serializers.CharField(max_length=120)
descricao = serializers.CharField(required=False, allow_blank=True, default="")
formula = serializers.CharField(max_length=500)
formula_exibicao = serializers.CharField(max_length=300, required=False, allow_blank=True, default="")
formato = serializers.ChoiceField(choices=[f[0] for f in IndicadorContabilDefinicao.FORMATO_CHOICES])
icone = serializers.ChoiceField(
choices=[i[0] for i in IndicadorContabilDefinicao.ICONE_CHOICES],
required=False,
default=IndicadorContabilDefinicao.ICONE_BARRAS,
)
# True (default) = aparece automaticamente em toda apuração, mesmo
# comportamento de sempre; False = fica salvo, só aparece nas apurações
# em que o contador selecionar (ver "Gerenciar Indicadores").
@ -2512,6 +2549,7 @@ class ContabilApuracaoDetailSerializer(serializers.ModelSerializer):
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
contas = ContabilContaSerializer(many=True, read_only=True)
linhas_dre = ContabilLinhaDreSerializer(many=True, read_only=True)
linhas_analise_vertical = ContabilLinhaAnaliseVerticalSerializer(many=True, read_only=True)
achados = ContabilAchadoSerializer(many=True, read_only=True)
class Meta:
@ -2528,7 +2566,10 @@ class ContabilApuracaoDetailSerializer(serializers.ModelSerializer):
"criado_por_nome",
"criado_em",
"concluida_em",
"resumo_fechamento",
"contas",
"linhas_dre",
"analise_vertical_meses",
"linhas_analise_vertical",
"achados",
]

View File

@ -47,6 +47,27 @@ def indice(valor: Decimal | None) -> str:
return _agrupa_milhar(f"{valor:,.2f}")
@register.filter
def moeda_av(valor: str | None) -> str:
"""Mesmo formato de `moeda` acima, só que recebendo o valor mensal da
Análise Vertical como ele é gravado (texto, não Decimal — ver
`ContabilLinhaAnaliseVertical.valores`, um JSONField)."""
if not valor:
return "—"
return moeda(Decimal(valor))
@register.filter
def percentual_av(valor: str | None) -> str:
"""Percentual da Análise Vertical já vem pronto do próprio relatório
(ex. "100.00" quer dizer 100,00%) — diferente do filtro `percentual`
acima, que recebe uma fração (ex. 0.50) e multiplica por 100; aqui só
formata no padrão brasileiro."""
if not valor:
return "—"
return f"{_agrupa_milhar(f'{Decimal(valor):,.2f}')}%"
@register.filter
def competencia(valor: date | None) -> str:
return valor.strftime("%m/%Y") if valor else "—"

View File

@ -98,6 +98,11 @@ router.register("nao-conformidades/acoes", views.NCAcaoViewSet, basename="nc-aca
router.register("contabil-apuracoes", views.ContabilApuracaoViewSet, basename="contabil-apuracao")
router.register("contabil-contas", views.ContabilContaViewSet, basename="contabil-conta")
router.register("contabil-linhas-dre", views.ContabilLinhaDreViewSet, basename="contabil-linha-dre")
router.register(
"contabil-linhas-analise-vertical",
views.ContabilLinhaAnaliseVerticalViewSet,
basename="contabil-linha-analise-vertical",
)
router.register("contabil-achados", views.ContabilAchadoViewSet, basename="contabil-achado")
router.register(
"contabil-indicadores-definicoes",

View File

@ -22,6 +22,7 @@ from django.middleware.csrf import get_token
from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.dateparse import parse_date
from django.utils.text import slugify
from django.views.decorators.csrf import ensure_csrf_cookie
@ -53,6 +54,7 @@ from .models import (
ContabilAchado,
ContabilApuracao,
ContabilConta,
ContabilLinhaAnaliseVertical,
ContabilLinhaDre,
Departamento,
Favorito,
@ -130,6 +132,8 @@ from .serializers import (
ContabilContaSerializer,
ContabilIndicadoresOcultosSerializer,
ContabilIndicadoresSelecionadosSerializer,
ContabilResumoFechamentoSerializer,
ContabilLinhaAnaliseVerticalSerializer,
ContabilLinhaDreSerializer,
DepartamentoSerializer,
FavoritoSerializer,
@ -3568,6 +3572,45 @@ _CONTABIL_INDICADOR_NOTA = {
}
_CONTABIL_ORDEM_GRUPOS_INDICADORES = ["Indicadores de Resultado", "Indicadores de Liquidez e Endividamento"]
# Desenho (miolo do <svg>) de cada opção de `IndicadorContabilDefinicao.icone`
# pro card do relatório "Gerar Dashboard" — ver comentário no model sobre a
# cópia irmã deste dict em `PID_DC_INDICADOR_ICONES`
# (static/js/dashboard-contabil.js), mantida em sincronia manualmente.
# `mark_safe` é seguro aqui: são só literais Python fixos neste arquivo,
# nunca texto vindo de usuário/banco.
_CONTABIL_ICONES_SVG = {
IndicadorContabilDefinicao.ICONE_BARRAS: mark_safe('<path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/>'),
IndicadorContabilDefinicao.ICONE_TENDENCIA_ALTA: mark_safe(
'<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>'
),
IndicadorContabilDefinicao.ICONE_TENDENCIA_BAIXA: mark_safe(
'<polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/>'
),
IndicadorContabilDefinicao.ICONE_PERCENTUAL: mark_safe(
'<line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/>'
),
IndicadorContabilDefinicao.ICONE_PIZZA: mark_safe(
'<path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/>'
),
IndicadorContabilDefinicao.ICONE_ATIVIDADE: mark_safe('<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>'),
IndicadorContabilDefinicao.ICONE_MOEDA: mark_safe(
'<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>'
),
IndicadorContabilDefinicao.ICONE_ALVO: mark_safe(
'<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>'
),
IndicadorContabilDefinicao.ICONE_CAMADAS: mark_safe(
'<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/>'
'<polyline points="2 12 12 17 22 12"/>'
),
IndicadorContabilDefinicao.ICONE_CARTAO: mark_safe(
'<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>'
),
IndicadorContabilDefinicao.ICONE_SELO: mark_safe(
'<circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/>'
),
}
def _contabil_monta_cards_indicadores(
apuracao: ContabilApuracao, valores: dict[str, Decimal | None]
@ -3594,6 +3637,13 @@ def _contabil_monta_cards_indicadores(
{
"chave": definicao.chave,
"nome": definicao.nome,
"descricao": definicao.descricao,
# Fórmula mostrada ao cliente no relatório: usa o texto livre
# `formula_exibicao` quando cadastrado; cai pra `formula`
# (a expressão técnica de cálculo, em snake_case) só como
# fallback pra indicador que ainda não teve esse campo
# preenchido — nunca fica sem nenhuma fórmula visível.
"formula": definicao.formula_exibicao.strip() or definicao.formula,
"grupo": definicao.grupo,
"valor_formatado": _contabil_formata_indicador(valor, definicao.formato),
"valor_bruto": _contabil_numero_bruto(valor),
@ -3601,6 +3651,7 @@ def _contabil_monta_cards_indicadores(
"cor_regra": _CONTABIL_INDICADOR_COR_REGRA.get(definicao.chave, ""),
"dourado": definicao.chave in _CONTABIL_INDICADOR_DOURADO,
"nota": nota,
"icone_svg": _CONTABIL_ICONES_SVG.get(definicao.icone, _CONTABIL_ICONES_SVG[IndicadorContabilDefinicao.ICONE_BARRAS]),
}
)
for indice, card in enumerate(cards):
@ -3680,7 +3731,9 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
def get_queryset(self) -> QuerySet[ContabilApuracao]:
queryset = super().get_queryset()
if self.action == "retrieve":
queryset = queryset.prefetch_related("contas", "linhas_dre", "achados__conta")
queryset = queryset.prefetch_related(
"contas", "linhas_dre", "linhas_analise_vertical", "achados__conta"
)
return queryset
def get_serializer_class(self) -> type[ModelSerializer]:
@ -3733,6 +3786,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
periodo_fim=cabecalho.periodo_fim,
arquivo=ContentFile(conteudo, name=arquivo.name),
criado_por=request.user,
analise_vertical_meses=resultado.extracao.meses_analise_vertical,
)
ContabilConta.objects.bulk_create(
[
@ -3764,6 +3818,22 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
for linha in resultado.extracao.linhas_dre
]
)
ContabilLinhaAnaliseVertical.objects.bulk_create(
[
ContabilLinhaAnaliseVertical(
apuracao=apuracao,
ordem=linha.ordem,
descricao=linha.descricao,
nivel=linha.nivel,
totalizador=linha.totalizador,
valores=[
{"valor": str(valor_mensal.valor), "percentual": str(valor_mensal.percentual)}
for valor_mensal in linha.valores
],
)
for linha in resultado.extracao.linhas_analise_vertical
]
)
contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()}
ContabilAchado.objects.bulk_create(
[
@ -3816,6 +3886,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
apuracao = self.get_object()
contas = list(apuracao.contas.order_by("ordem"))
linhas_dre = list(apuracao.linhas_dre.order_by("ordem"))
linhas_analise_vertical = list(apuracao.linhas_analise_vertical.order_by("ordem"))
achados = list(apuracao.achados.select_related("conta", "tratado_por").order_by("severidade", "id"))
dados = _contabil_coleta_dados_indicadores(apuracao)
@ -3845,8 +3916,15 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
"evolucao": evolucao,
"contas": _contabil_arvore_contexto(contas, "conta", lambda c: c.codigo.count("."), 18),
"linhas_dre": _contabil_arvore_contexto(linhas_dre, "linha", lambda l: max(0, l.nivel), 16),
"analise_vertical_meses": apuracao.analise_vertical_meses,
"linhas_analise_vertical": _contabil_arvore_contexto(
linhas_analise_vertical, "linha", lambda l: max(0, l.nivel), 16
),
"observacoes_contas": [conta for conta in contas if conta.observacao and not conta.oculta_no_relatorio],
"observacoes_dre": [linha for linha in linhas_dre if linha.observacao and not linha.oculta_no_relatorio],
"observacoes_analise_vertical": [
linha for linha in linhas_analise_vertical if linha.observacao and not linha.oculta_no_relatorio
],
"achados_com_observacao": [
achado for achado in achados if achado.observacao_contador and not achado.oculto_no_relatorio
],
@ -3928,6 +4006,24 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
apuracao.save(update_fields=["indicadores_selecionados"])
return Response({"indicadores_selecionados": apuracao.indicadores_selecionados})
@action(detail=True, methods=["post"], url_path="resumo-fechamento")
def resumo_fechamento(self, request: Request, pk: str | None = None) -> Response:
"""Substitui o texto inteiro do "Resumo do Fechamento" (aba
"Dashboard" da tela de revisão) — considerações/análises livres do
contador sobre o fechamento, aparece no relatório "Gerar Dashboard"
antes dos cards de indicador. Mesmo espírito de
`indicadores_ocultos()`/`indicadores_selecionados()` acima: substitui
o campo inteiro de uma vez, sanitizado por
`ContabilResumoFechamentoSerializer` (nh3, mesma allowlist de
`AcessoGeral.observacoes`/`AjudaAplicacao.texto`)."""
apuracao = self.get_object()
_contabil_garante_em_revisao(apuracao)
entrada = ContabilResumoFechamentoSerializer(data=request.data)
entrada.is_valid(raise_exception=True)
apuracao.resumo_fechamento = entrada.validated_data["resumo_fechamento"]
apuracao.save(update_fields=["resumo_fechamento"])
return Response({"resumo_fechamento": apuracao.resumo_fechamento})
@action(detail=True, methods=["post"], url_path="pre-visualizar-indicador")
def pre_visualizar_indicador(self, request: Request, pk: str | None = None) -> Response:
"""Calcula, contra **esta** apuração, o valor de cada componente e o
@ -4081,6 +4177,23 @@ class ContabilLinhaDreViewSet(viewsets.ModelViewSet):
serializer.save()
class ContabilLinhaAnaliseVerticalViewSet(viewsets.ModelViewSet):
"""Linhas da Análise Vertical de uma apuração — nascem todas juntas em
`ContabilApuracaoViewSet.create()`, então só GET/PATCH (observação do
contador) aqui, mesmo espírito de `ContabilLinhaDreViewSet`."""
http_method_names = ["get", "patch", "head", "options"]
queryset = ContabilLinhaAnaliseVertical.objects.all()
serializer_class = ContabilLinhaAnaliseVerticalSerializer
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")]
def perform_update(self, serializer: ContabilLinhaAnaliseVerticalSerializer) -> None:
_contabil_garante_em_revisao(serializer.instance.apuracao)
serializer.save()
class ContabilAchadoViewSet(viewsets.ModelViewSet):
"""Achados de auditoria gerados automaticamente na criação da apuração —
GET/PATCH (marcar como tratado/ignorado, sempre com justificativa) +
@ -4202,7 +4315,9 @@ class IndicadorContabilDefinicaoViewSet(viewsets.ModelViewSet):
nome=dados["nome"],
descricao=dados.get("descricao", ""),
formula=dados["formula"],
formula_exibicao=dados.get("formula_exibicao", ""),
formato=dados["formato"],
icone=dados.get("icone", IndicadorContabilDefinicao.ICONE_BARRAS),
padrao=dados.get("padrao", True),
criado_por=request.user,
)
@ -4218,9 +4333,22 @@ class IndicadorContabilDefinicaoViewSet(viewsets.ModelViewSet):
definicao.nome = dados["nome"]
definicao.descricao = dados.get("descricao", "")
definicao.formula = dados["formula"]
definicao.formula_exibicao = dados.get("formula_exibicao", "")
definicao.formato = dados["formato"]
definicao.icone = dados.get("icone", IndicadorContabilDefinicao.ICONE_BARRAS)
definicao.padrao = dados.get("padrao", True)
definicao.save(update_fields=["nome", "descricao", "formula", "formato", "padrao", "atualizado_em"])
definicao.save(
update_fields=[
"nome",
"descricao",
"formula",
"formula_exibicao",
"formato",
"icone",
"padrao",
"atualizado_em",
]
)
self._salva_componentes(definicao, dados["componentes"])
return Response(IndicadorContabilDefinicaoSerializer(definicao).data)

View File

@ -560,6 +560,16 @@
font-variant-numeric: tabular-nums;
}
/* Análise Vertical tem um número variável de colunas de Valor/Variação (uma
dupla por mês, ver ContabilApuracao.analise_vertical_meses) — diferente
da DRE (sempre uma única coluna de valor, `td:nth-child(2)` acima), então
alinha à direita toda coluna que não seja a 1ª (descrição) nem a última
(observação), em vez de fixar um índice. */
.dc-av-table td:not(:first-child):not(:last-child) {
text-align: right;
font-variant-numeric: tabular-nums;
}
.dc-contas-table td:nth-child(1) {
white-space: nowrap;
color: var(--text-muted);
@ -581,9 +591,42 @@
realce se move pros filhos dela e as linhas destacadas antes voltam à cor
padrão (dcContasDestaque/dcDreDestaque em dashboard-contabil.js sempre
representam só a última leva revelada, nunca acumulam). `.pa-table td` já
tem `transition: background`, então a troca de destaque anima sozinha. */
.dc-conta-row--destaque td {
background-color: rgba(var(--gold-rgb), 0.16);
tem `transition: background`, então a troca de destaque anima sozinha.
Cor por tema via as duas variáveis abaixo, não hardcoded na regra, pra dar
pra inverter o efeito no tema claro sem duplicar a regra em si. No escuro,
o destaque usa `--bg-canvas` (o tom mais escuro do tema — pedido explícito
do usuário depois de ver `--card-bg-hover`, mais claro, nas linhas
destacadas: queria o oposto, destaque mais escuro e o resto mais claro) e
as linhas fora do destaque usam `--bg-surface-raised` (mais claro que
`--bg-canvas`/`--bg-surface`, mas ainda distinto de `--card-bg-hover` —
senão o hover das linhas não-destacadas ficaria idêntico ao repouso, sem
nenhum feedback visual). No claro, o efeito já nasceu invertido por pedido
anterior do usuário — as linhas fora do destaque é que ganham a cor
(`--accent-rgb` bem sutil), e a leva recém-revelada fica em branco
(`--bg-surface`), lida como "limpa" em vez de tingida; não mexido nesta
rodada, só o tema escuro pediu a troca. */
:root {
--dc-destaque-bg: var(--bg-canvas);
--dc-row-tint-bg: var(--bg-surface-raised);
}
:root[data-theme="light"] {
--dc-destaque-bg: var(--bg-surface);
--dc-row-tint-bg: rgba(var(--accent-rgb), 0.08);
}
.dc-contas-table tbody tr td,
.dc-dre-table tbody tr td {
background-color: var(--dc-row-tint-bg);
}
/* Especificidade proposital mais alta que a regra acima (duas classes, não
uma) — senão a regra de tingimento genérico, com mais seletores de tipo
(tbody+tr+td), venceria por especificidade e o destaque nunca apareceria. */
.dc-contas-table .dc-conta-row--destaque td,
.dc-dre-table .dc-conta-row--destaque td {
background-color: var(--dc-destaque-bg);
}
.dc-conta-desc-cell {
@ -632,23 +675,149 @@
flex-shrink: 0;
}
/* Botão de observação — sempre o mesmo ícone (pedido explícito do usuário:
nem "+ Observação" nem o texto da observação em si devem aparecer na
tabela, poluía a coluna visualmente); só a cor muda pra indicar de
relance quais linhas já têm observação preenchida (`title`/tooltip
mostra o texto quando houver). Mesmo padrão dos outros ícones de tabela
(`.icon-btn`, `perfis-acesso.css`), mas menor, pra caber numa célula sem
aumentar a altura da linha. */
.dc-conta-observacao-btn {
font-size: 0.78rem;
color: var(--accent);
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
color: var(--text-muted);
background: none;
border: none;
cursor: pointer;
text-align: left;
padding: 0;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.dc-conta-observacao-btn:hover {
text-decoration: underline;
background: var(--card-bg-hover);
color: var(--accent);
}
.dc-conta-observacao-btn--preenchida {
color: var(--accent);
}
/* Coluna "Observação" agora tem 2 botões lado a lado (validado + observação)
— pedido explícito do usuário. Este `display:flex` fica num `<div>` *dentro*
do `<td>`, nunca no próprio `<td>` — um `<td>` com `display:flex` deixa de
se comportar como célula de tabela (altura/fundo/borda não acompanham mais
a linha direito), o que já causou um bug visual real (linha "quebrando"
antes de chegar nos botões, ver `renderContas()`/`renderDre()`). */
.dc-obs-cell-actions {
display: flex;
align-items: center;
gap: var(--space-1);
}
/* Botão de "validado" (check) — cor fixa `--teal`, deliberadamente diferente
de `--accent` (usado em `.dc-conta-observacao-btn--preenchida` e também a
cor de tema que o usuário escolhe em "Tema de cores", ver tokens.css), pra
não confundir "tem observação preenchida" com "conta já conferida". */
.dc-conta-validado-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
color: var(--text-muted);
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.dc-conta-validado-btn:hover {
background: var(--card-bg-hover);
color: var(--teal);
}
.dc-conta-validado-btn--validado {
color: var(--teal);
}
/* Estado "parcial" do tri-state de uma conta/linha sintética (com filhos) —
pedido explícito do usuário: amarelo enquanto nem todo descendente estiver
validado, mesmo depois de marcar a própria sintética. Vira
`--validado` (verde/teal) assim que 100% dos descendentes estiverem
validados — ver dcEstadoValidacaoGrupo()/dcValidadoInfo() em
dashboard-contabil.js. */
.dc-conta-validado-btn--parcial {
color: var(--gold);
}
.dc-conta-validado-btn:disabled {
cursor: default;
opacity: 0.5;
}
/* Editor inline de observação — substitui o antigo modal `#dc-observacao-modal`
(pedido explícito do usuário, pra não perder o texto por causa de um
clique acidental fora de um popup): uma linha extra logo abaixo da conta/
linha, dentro da própria tabela. Fundo distinto do tingimento padrão das
linhas (ver `--dc-row-tint-bg`/`--dc-destaque-bg` acima) — 2 classes,
maior especificidade que a regra genérica de 1 classe + 3 seletores de
tipo, então não precisa se preocupar com a ordem de declaração. */
.dc-contas-table .dc-obs-edit-row td,
.dc-dre-table .dc-obs-edit-row td {
background-color: var(--bg-surface);
}
.dc-obs-edit-row td {
padding: var(--space-4);
}
.dc-obs-edit {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.dc-obs-edit .modal-field {
margin: 0;
}
.dc-obs-edit .modal-actions {
margin-top: 0;
}
/* Resumo de observações no final do Balancete/DRE (pedido explícito do
usuário) — reaproveita `.dc-dash-obs-list`/`.dc-dash-obs*` (já definidas
mais abaixo) pros itens da lista, só a moldura da seção é nova. */
.dc-obs-resumo {
margin-top: var(--space-5);
padding-top: var(--space-5);
border-top: 1px solid var(--border-subtle);
}
.dc-obs-resumo__titulo {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 0.95rem;
margin: 0 0 var(--space-3);
}
.dc-obs-resumo__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 var(--space-2);
border-radius: var(--radius-pill);
background: rgba(var(--accent-rgb), 0.14);
color: var(--accent);
font-size: 0.75rem;
font-weight: 700;
}
.dc-dre-row--totalizador td {
@ -742,6 +911,76 @@
margin-bottom: var(--space-3);
}
/* Resumo do Fechamento — editor de texto rico (mesmo padrão de
.ag-richtext em acessos-gerais.css/.ajuda-modal__editor em
components.css: contenteditable + colar/arrastar imagem, ver
pidDcInicializaResumoFechamentoEditor() em dashboard-contabil.js).
Duplicado por tela de propósito, mesmo espírito dos outros dois — não é
um componente compartilhado no projeto. */
.dc-dash-resumo-fechamento {
margin-bottom: var(--space-5);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--border-subtle);
}
.dc-dash-resumo-fechamento__titulo {
font-size: 0.85rem;
font-weight: 700;
margin: 0 0 var(--space-1);
}
.dc-dash-resumo-fechamento__dica {
font-size: 0.78rem;
color: var(--text-muted);
margin: 0 0 var(--space-2);
}
.dc-dash-richtext {
min-height: 120px;
max-height: 340px;
overflow-y: auto;
padding: var(--space-3);
border-radius: var(--radius-sm);
border: 1px solid var(--border-subtle);
background: var(--bg-canvas);
color: var(--text-primary);
font-size: 0.88rem;
line-height: 1.5;
}
.dc-dash-richtext:focus {
outline: none;
border-color: var(--accent);
}
.dc-dash-richtext:empty::before {
content: attr(data-placeholder);
color: var(--text-muted);
}
.dc-dash-richtext img {
max-width: 100%;
border-radius: var(--radius-sm);
margin: var(--space-2) 0;
}
.dc-dash-richtext[contenteditable="false"] {
cursor: default;
opacity: 0.85;
}
.dc-dash-resumo-fechamento__acoes {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-2);
}
.dc-dash-resumo-fechamento__status {
font-size: 0.78rem;
color: var(--text-muted);
}
.dc-dash-grupo {
margin-bottom: var(--space-4);
}
@ -975,6 +1214,43 @@
margin-top: var(--space-1);
}
/* Seletor de ícone do indicador (modal "Novo/Editar Indicador") — cada
botão já é o próprio preview (mesmo ícone que aparece no card do
relatório "Gerar Dashboard", ver dashboard-contabil-relatorio.html);
`.is-selecionado` é a única indicação de estado, sem nenhum input
escondido visível — o valor real fica em memória (dcIndicadorIconeSelecionado
em dashboard-contabil.js) até o Salvar. */
.dc-ind-icone-grid {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.dc-ind-icone-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: var(--radius-md);
border: 1px solid var(--border-subtle);
background: var(--bg-surface);
color: var(--text-muted);
cursor: pointer;
transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.dc-ind-icone-btn:hover {
background: var(--card-bg-hover);
color: var(--text-primary);
}
.dc-ind-icone-btn.is-selecionado {
background: rgba(var(--accent-rgb), 0.16);
border-color: var(--accent);
color: var(--accent);
}
.dc-ind-componentes-lista {
margin-bottom: var(--space-2);
}

File diff suppressed because it is too large Load Diff

View File

@ -136,7 +136,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -137,7 +137,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -136,7 +136,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -5,10 +5,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard Contábil — {{ apuracao.nome_empresa }} — {{ apuracao.competencia|competencia }}</title>
<title>Relatório Contábil — {{ apuracao.nome_empresa }} — {{ apuracao.competencia|competencia }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
:root {
@ -218,22 +218,49 @@
gap: 16px;
margin-bottom: 22px;
}
/* Flip card — pedido explícito do usuário: passar o mouse revela a
descrição/fórmula do indicador (cadastradas em "Gerenciar
Indicadores", ver IndicadorContabilDefinicao.descricao/.formula), sem
precisar de nenhum clique. `.dcr-card` é só a "cena" 3D (perspective +
min-height, já que as duas faces são absolutamente posicionadas e não
contribuem mais pra altura do card); `.dcr-card-inner` é quem
realmente gira (`rotateY`); cada face (frente/verso) carrega o
fundo/borda/sombra que antes viviam direto em `.dcr-card`.
`backface-visibility:hidden` garante que só uma face fica visível de
cada vez. Em `@media print`, `transition`/`animation` são zeradas
globalmente (ver bloco mais abaixo) — o card sempre imprime a frente,
já que impressão não tem "hover" ativo. */
.dcr-card {
position: relative;
perspective: 1200px;
min-height: 172px;
animation: dcrFadeSlideUp 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: calc(var(--i, 0) * 55ms + 60ms);
}
.dcr-card:hover { transform: translateY(-3px); }
.dcr-card-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 480ms cubic-bezier(0.4, 0.2, 0.2, 1);
transform-style: preserve-3d;
}
.dcr-card:hover .dcr-card-inner { transform: rotateY(180deg); }
.dcr-card-face {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
background: #fff;
border: 1px solid var(--borda);
border-radius: 14px;
padding: 16px 18px;
box-shadow: var(--sombra);
transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
animation: dcrFadeSlideUp 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: calc(var(--i, 0) * 55ms + 60ms);
}
.dcr-card:hover {
transform: translateY(-3px);
box-shadow: var(--sombra-hover);
border-color: rgba(122, 91, 192, 0.35);
backface-visibility: hidden;
transition: box-shadow 200ms ease, border-color 200ms ease;
}
.dcr-card:hover .dcr-card-face { box-shadow: var(--sombra-hover); border-color: rgba(122, 91, 192, 0.35); }
.dcr-card-face--back { transform: rotateY(180deg); overflow-y: auto; text-align: center; }
.dcr-card__icon {
width: 38px;
height: 38px;
@ -248,7 +275,7 @@
.dcr-card--dourado .dcr-card__icon { background: var(--dourado-claro); color: var(--dourado-escuro); }
.dcr-card__label { font-size: 0.74rem; color: var(--texto-muted); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; }
.dcr-card__value {
font-size: 1.55rem;
font-size: 1.3rem;
font-weight: 800;
color: var(--roxo-escuro);
margin-top: 4px;
@ -259,6 +286,40 @@
.dcr-card__value.dcr-valor-negativo { color: var(--falha-texto); }
.dcr-card__nota { font-size: 0.7rem; color: var(--texto-muted); margin-top: 6px; line-height: 1.35; }
/* ---- Verso do flip card: descrição + fórmula ---- */
.dcr-card-face__titulo {
font-size: 0.74rem;
font-weight: 700;
color: var(--roxo-escuro);
margin: 0 0 6px;
}
.dcr-card-face__descricao {
font-size: 0.7rem;
color: var(--texto);
line-height: 1.4;
margin: 0 0 10px;
}
.dcr-card-face__formula-label {
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--texto-muted);
font-weight: 600;
margin: 0 0 4px;
}
.dcr-card-face__formula {
display: block;
font-family: "JetBrains Mono", "Courier New", monospace;
font-weight: 500;
font-size: 0.7rem;
color: var(--roxo-escuro);
background: var(--roxo-claro);
border-radius: 8px;
padding: 6px 8px;
word-break: break-word;
white-space: pre-wrap;
}
/* ---- Gráfico ---- */
.dcr-chart-wrap {
background: #fff;
@ -292,11 +353,51 @@
}
table.dcr-tabela td.dcr-valor { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
table.dcr-tabela tbody tr { transition: background 120ms ease; }
table.dcr-tabela tbody tr:not(.dcr-linha-total):hover { background: #faf9ff; }
table.dcr-tabela tbody tr:nth-child(even):not(.dcr-linha-total) { background: rgba(122, 91, 192, 0.025); }
tr.dcr-linha-total td { font-weight: 700; background: var(--dourado-claro); }
tr.dcr-linha-total td { font-weight: 700; }
.dcr-row-in { animation: dcrFadeIn 220ms ease; }
/* Destaque temporário nas linhas recém-reveladas ao expandir um grupo —
mesmo mecanismo de dc-conta-row--destaque em dashboard-contabil.js/css
do Portal (ver ali o porquê: dá pra acompanhar visualmente o que acabou
de aparecer ao expandir, e ao recolher de novo o destaque volta pra
leva anterior, ver pidDcrArvore() mais abaixo). Este documento não tem
alternância de tema (é sempre a marca clara do escritório), então só
existe a variante "clara": linhas fora do destaque ganham um leve
tingimento roxo (substitui o zebra por linha par de antes, que ficava
redundante); a leva recém-revelada fica em branco puro, lida como
"em foco" em vez de tingida — inclusive quando a linha revelada é um
grupo/total (`.dcr-linha-total`): no plano de contas real, a maioria
das linhas reveladas ao expandir um grupo também são grupos
(`tipo="S"`, com filhos mais fundo ainda) — excluir `.dcr-linha-total`
do destaque fazia o efeito nunca aparecer na prática (bug encontrado ao
testar: toda a leva revelada ao expandir "CAIXA E EQUIVALENTES DE
CAIXA" já é `.dcr-linha-total`).
Implementado via custom property por linha, não competindo por
especificidade CSS entre as 3 regras abaixo (tingimento/total/hover
têm a mesma especificidade — só o `td` de fato lê `--dcr-row-bg`, cujo
valor herda do `tr`; a regra de destaque vem por último de propósito,
pra vencer o empate por ordem de declaração quando uma linha é ao
mesmo tempo `.dcr-linha-total` e `.dcr-destaque`). Hover continua de
fora dos totais (nunca teve, decisão anterior à esta mudança) e vence
o destaque por ter uma regra mais específica (`:hover` conta como
classe a mais). */
.dcr-tabela tbody tr td {
background: var(--dcr-row-bg, transparent);
}
.dcr-tabela tbody tr:not(.dcr-linha-total) {
--dcr-row-bg: rgba(122, 91, 192, 0.05);
}
.dcr-tabela tbody tr.dcr-linha-total {
--dcr-row-bg: var(--dourado-claro);
}
.dcr-tabela tbody tr:not(.dcr-linha-total):hover {
--dcr-row-bg: #faf9ff;
}
.dcr-tabela tbody tr.dcr-destaque {
--dcr-row-bg: #fff;
}
.dcr-desc-cell { display: flex; align-items: center; gap: 7px; }
.dcr-toggle {
display: inline-flex;
@ -317,6 +418,45 @@
.dcr-toggle.is-expanded svg { transform: rotate(90deg); }
.dcr-toggle-spacer { display: inline-block; width: 18px; height: 18px; flex-shrink: 0; }
/* Ícone de observação na tabela do Balancete/DRE (coluna "Observação") —
só aparece na linha quando há mesmo uma observação visível ao cliente
(ver `oculta_no_relatorio` no template); clicar abre `.dcr-obs-inline-row`
logo abaixo, mesmo espírito do editor inline da tela de revisão do
Portal (`dc-conta-observacao-btn`/`dc-obs-edit-row`, dashboard-contabil.js). */
.dcr-obs-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
color: var(--roxo-medio);
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.dcr-obs-btn:hover { background: var(--roxo-claro); color: var(--roxo-escuro); }
.dcr-obs-btn.is-aberto { background: var(--roxo-claro); color: var(--roxo-escuro); }
.dcr-obs-inline-row td { background: #fff; padding: 12px 14px; }
.dcr-obs-inline {
border-left: 3px solid var(--roxo-medio);
padding: 8px 14px;
background: var(--roxo-claro);
border-radius: 0 8px 8px 0;
}
.dcr-obs-inline__rotulo {
display: block;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--roxo-escuro);
margin-bottom: 4px;
}
.dcr-obs-inline__texto { margin: 0; white-space: pre-wrap; color: var(--texto); }
/* ---- Observações ---- */
.dcr-obs-lista { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.dcr-obs-item {
@ -346,6 +486,29 @@
.dcr-obs-item__titulo { font-weight: 700; font-size: 0.85rem; color: var(--roxo-escuro); }
.dcr-obs-item__texto { font-size: 0.85rem; margin-top: 3px; white-space: pre-wrap; color: var(--texto); }
.dcr-vazio { color: var(--texto-muted); font-size: 0.85rem; font-style: italic; }
/* Resumo do Fechamento — texto rico escrito pelo contador na aba
"Dashboard" da revisão (editor contenteditable, ver
dashboard-contabil.js), já sanitizado por nh3 antes de salvar
(ContabilResumoFechamentoSerializer) — só por isso é seguro usar
`|safe` aqui (primeiro `|safe` de template Django do projeto; até
agora todo texto rico só existia em tela SPA, injetado via
`innerHTML` no JS, nunca por um template do servidor). */
.dcr-resumo-fechamento {
background: #fff;
border: 1px solid var(--borda);
border-radius: 14px;
padding: 20px 22px;
box-shadow: var(--sombra);
margin-bottom: 22px;
animation: dcrFadeSlideUp 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.dcr-resumo-fechamento h2 { margin-bottom: 14px; }
.dcr-resumo-fechamento__texto { font-size: 0.88rem; line-height: 1.6; color: var(--texto); }
.dcr-resumo-fechamento__texto p { margin: 0 0 10px; }
.dcr-resumo-fechamento__texto p:last-child { margin-bottom: 0; }
.dcr-resumo-fechamento__texto ul, .dcr-resumo-fechamento__texto ol { margin: 0 0 10px; padding-left: 22px; }
.dcr-resumo-fechamento__texto img { max-width: 100%; border-radius: 8px; margin: 10px 0; }
.dcr-footer { color: var(--texto-muted); font-size: 0.76rem; text-align: center; margin-top: 28px; }
@media (max-width: 640px) {
@ -364,7 +527,8 @@
.dcr-tab-panel:not(:last-child) { page-break-after: always; }
.dcr-tabela tbody tr[hidden] { display: table-row !important; }
.dcr-toggle { display: none; }
.dcr-card, .dcr-tabela-wrap, .dcr-obs-item { box-shadow: none !important; }
.dcr-obs-btn { display: none; }
.dcr-card, .dcr-tabela-wrap, .dcr-obs-item, .dcr-resumo-fechamento { box-shadow: none !important; }
}
</style>
</head>
@ -374,7 +538,7 @@
<div class="dcr-header__brand">
<img src="{% static 'img/logo-branco.png' %}" alt="De Paula Contadores">
<div>
<h1>Dashboard Contábil — {{ apuracao.nome_empresa }}</h1>
<h1>Relatório Contábil — {{ apuracao.nome_empresa }}</h1>
<p>
CNPJ {{ apuracao.cnpj|default:"não informado" }} · Competência {{ apuracao.competencia|competencia }}
<span class="dcr-status-pill dcr-status-pill--{{ apuracao.status }}">{{ apuracao.get_status_display }}</span>
@ -391,7 +555,10 @@
<span class="dcr-tabs__indicator" id="dcr-tabs-indicator"></span>
<button type="button" class="dcr-tab is-active" data-dcr-tab="balancete">Balancete</button>
<button type="button" class="dcr-tab" data-dcr-tab="dre">D.R.E.</button>
<button type="button" class="dcr-tab" data-dcr-tab="indicadores">Indicadores</button>
{% if analise_vertical_meses %}
<button type="button" class="dcr-tab" data-dcr-tab="analise-vertical">Análise Vertical</button>
{% endif %}
<button type="button" class="dcr-tab" data-dcr-tab="indicadores">Resumo</button>
</nav>
<div class="dcr-tab-panel" data-dcr-panel="balancete">
@ -414,6 +581,7 @@
<th class="dcr-valor">Débito</th>
<th class="dcr-valor">Crédito</th>
<th class="dcr-valor">Saldo Atual</th>
<th>Observação</th>
</tr>
</thead>
<tbody id="dcr-balancete-body">
@ -436,7 +604,24 @@
<td class="dcr-valor">{{ item.conta.debito|moeda }}</td>
<td class="dcr-valor">{{ item.conta.credito|moeda }}</td>
<td class="dcr-valor">{{ item.conta.saldo_atual|moeda }}</td>
<td>
{% if item.conta.observacao and not item.conta.oculta_no_relatorio %}
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="conta-{{ item.conta.id }}" aria-label="Ver observação">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
{% endif %}
</td>
</tr>
{% if item.conta.observacao and not item.conta.oculta_no_relatorio %}
<tr class="dcr-obs-inline-row" data-dcr-obs-row="conta-{{ item.conta.id }}" hidden>
<td colspan="7">
<div class="dcr-obs-inline">
<span class="dcr-obs-inline__rotulo">Observação do contador</span>
<p class="dcr-obs-inline__texto">{{ item.conta.observacao }}</p>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
@ -478,7 +663,7 @@
</h2>
<div class="dcr-tabela-wrap">
<table class="dcr-tabela">
<thead><tr><th>Descrição</th><th class="dcr-valor">Valor</th></tr></thead>
<thead><tr><th>Descrição</th><th class="dcr-valor">Valor</th><th>Observação</th></tr></thead>
<tbody id="dcr-dre-body">
{% for item in linhas_dre %}
<tr{% if item.linha.totalizador %} class="dcr-linha-total"{% endif %} data-dcr-nivel="{{ item.nivel }}" data-dcr-id="linha-{{ item.linha.id }}" data-dcr-tem-filhos="{% if item.tem_filhos %}1{% else %}0{% endif %}" data-dcr-colapsado-padrao="{% if item.colapsado_padrao %}1{% else %}0{% endif %}">
@ -495,7 +680,24 @@
</span>
</td>
<td class="dcr-valor">{{ item.linha.valor|moeda }}</td>
<td>
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="linha-{{ item.linha.id }}" aria-label="Ver observação">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
{% endif %}
</td>
</tr>
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
<tr class="dcr-obs-inline-row" data-dcr-obs-row="linha-{{ item.linha.id }}" hidden>
<td colspan="3">
<div class="dcr-obs-inline">
<span class="dcr-obs-inline__rotulo">Observação do contador</span>
<p class="dcr-obs-inline__texto">{{ item.linha.observacao }}</p>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
@ -525,7 +727,102 @@
</div>
</div>
{% if analise_vertical_meses %}
<div class="dcr-tab-panel" data-dcr-panel="analise-vertical" hidden>
<div class="dcr-secao">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg>
Demonstração Mensal (Análise Vertical)
</h2>
<div class="dcr-tabela-wrap">
<table class="dcr-tabela">
<thead>
<tr>
<th>Descrição</th>
{% for mes in analise_vertical_meses %}
<th class="dcr-valor">{{ mes|capfirst }} — Valor</th>
<th class="dcr-valor">{{ mes|capfirst }} — Variação</th>
{% endfor %}
<th>Observação</th>
</tr>
</thead>
<tbody id="dcr-av-body">
{% for item in linhas_analise_vertical %}
<tr{% if item.linha.totalizador %} class="dcr-linha-total"{% endif %} data-dcr-nivel="{{ item.nivel }}" data-dcr-id="av-{{ item.linha.id }}" data-dcr-tem-filhos="{% if item.tem_filhos %}1{% else %}0{% endif %}" data-dcr-colapsado-padrao="{% if item.colapsado_padrao %}1{% else %}0{% endif %}">
<td style="padding-left: {{ item.nivel_px|add:12 }}px">
<span class="dcr-desc-cell">
{% if item.tem_filhos %}
<button type="button" class="dcr-toggle{% if not item.colapsado_padrao %} is-expanded{% endif %}" data-dcr-toggle aria-label="Recolher/expandir">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
{% else %}
<span class="dcr-toggle-spacer"></span>
{% endif %}
<span>{{ item.linha.descricao }}</span>
</span>
</td>
{% for v in item.linha.valores %}
<td class="dcr-valor">{{ v.valor|moeda_av }}</td>
<td class="dcr-valor">{{ v.percentual|percentual_av }}</td>
{% endfor %}
<td>
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="av-{{ item.linha.id }}" aria-label="Ver observação">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
{% endif %}
</td>
</tr>
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
<tr class="dcr-obs-inline-row" data-dcr-obs-row="av-{{ item.linha.id }}" hidden>
<td colspan="{{ analise_vertical_meses|length|add:2 }}">
<div class="dcr-obs-inline">
<span class="dcr-obs-inline__rotulo">Observação do contador</span>
<p class="dcr-obs-inline__texto">{{ item.linha.observacao }}</p>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="dcr-secao">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/></svg>
Observações da Análise Vertical
</h2>
{% if observacoes_analise_vertical %}
<ul class="dcr-obs-lista">
{% for linha in observacoes_analise_vertical %}
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg></div>
<div>
<div class="dcr-obs-item__titulo">{{ linha.descricao }}</div>
<div class="dcr-obs-item__texto">{{ linha.observacao }}</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="dcr-vazio">Nenhuma observação registrada na Análise Vertical.</p>
{% endif %}
</div>
</div>
{% endif %}
<div class="dcr-tab-panel" data-dcr-panel="indicadores" hidden>
{% if apuracao.resumo_fechamento %}
<div class="dcr-secao dcr-resumo-fechamento">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
Resumo do Fechamento
</h2>
<div class="dcr-resumo-fechamento__texto">{{ apuracao.resumo_fechamento|safe }}</div>
</div>
{% endif %}
<div class="dcr-secao">
{% for grupo in indicadores_grupos %}
<h2>
@ -535,16 +832,26 @@
<div class="dcr-cards">
{% for indicador in grupo.cards %}
<div class="dcr-card{% if indicador.dourado %} dcr-card--dourado{% endif %}" style="--i:{{ indicador.indice_animacao }}">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg></div>
<div class="dcr-card__label">{{ indicador.nome }}</div>
<div
class="dcr-card__value"
data-final="{{ indicador.valor_formatado }}"
data-count="{{ indicador.valor_bruto }}"
data-format="{{ indicador.data_format }}"
{% if indicador.cor_regra %}data-color-rule="{{ indicador.cor_regra }}"{% endif %}
>{{ indicador.valor_formatado }}</div>
{% if indicador.nota %}<div class="dcr-card__nota">{{ indicador.nota }}</div>{% endif %}
<div class="dcr-card-inner">
<div class="dcr-card-face dcr-card-face--front">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ indicador.icone_svg }}</svg></div>
<div class="dcr-card__label">{{ indicador.nome }}</div>
<div
class="dcr-card__value"
data-final="{{ indicador.valor_formatado }}"
data-count="{{ indicador.valor_bruto }}"
data-format="{{ indicador.data_format }}"
{% if indicador.cor_regra %}data-color-rule="{{ indicador.cor_regra }}"{% endif %}
>{{ indicador.valor_formatado }}</div>
{% if indicador.nota %}<div class="dcr-card__nota">{{ indicador.nota }}</div>{% endif %}
</div>
<div class="dcr-card-face dcr-card-face--back">
<div class="dcr-card-face__titulo">{{ indicador.nome }}</div>
<p class="dcr-card-face__descricao">{{ indicador.descricao|default:"Sem descrição cadastrada." }}</p>
<div class="dcr-card-face__formula-label">Fórmula</div>
<code class="dcr-card-face__formula">{{ indicador.formula }}</code>
</div>
</div>
</div>
{% endfor %}
</div>
@ -570,7 +877,7 @@
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/></svg>
Todas as Observações da Análise
</h2>
{% if observacoes_contas or observacoes_dre or achados_com_observacao %}
{% if observacoes_contas or observacoes_dre or observacoes_analise_vertical or achados_com_observacao %}
<ul class="dcr-obs-lista">
{% for conta in observacoes_contas %}
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
@ -590,6 +897,15 @@
</div>
</li>
{% endfor %}
{% for linha in observacoes_analise_vertical %}
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg></div>
<div>
<div class="dcr-obs-item__titulo">Análise Vertical — {{ linha.descricao }}</div>
<div class="dcr-obs-item__texto">{{ linha.observacao }}</div>
</div>
</li>
{% endfor %}
{% for achado in achados_com_observacao %}
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><path d="M12 9v4M12 17h.01"/></svg></div>
@ -667,7 +983,14 @@
function pidDcrArvore(tbodyId) {
var tbody = document.getElementById(tbodyId);
if (!tbody) return;
var linhas = Array.prototype.slice.call(tbody.querySelectorAll("tr"));
// As linhas de observação inline (ver pidDcrObs abaixo) não entram
// nessa árvore — não têm nível/tem-filhos próprios, e deixá-las na
// lista quebraria a pilha de colapso (uma linha sem `data-dcr-nivel`
// vira NaN, que nunca compara >= nada). A visibilidade delas é
// sincronizada à parte, a partir do `hidden` da própria linha-pai.
var linhas = Array.prototype.slice.call(tbody.querySelectorAll("tr")).filter(function (tr) {
return !tr.hasAttribute("data-dcr-obs-row");
});
// Estado inicial: linhas marcadas pelo servidor como "colapsado por
// padrão" (a partir do nível equivalente ao grupo 4 do código de
// classificação, ver _CONTABIL_NIVEL_ABERTO_PADRAO em views.py)
@ -679,6 +1002,78 @@
}
});
// Histórico do destaque — mesmo espírito de dcContasDestaqueHistorico
// em dashboard-contabil.js: cada expandir empilha o destaque atual e
// troca pros filhos diretos recém-revelados; cada recolher desempilha
// e restaura o destaque de antes, em vez de simplesmente apagar.
var historico = [];
function filhosDiretos(id) {
var idx = linhas.findIndex(function (tr) {
return tr.getAttribute("data-dcr-id") === id;
});
if (idx === -1) return {};
var nivelPai = Number(linhas[idx].getAttribute("data-dcr-nivel"));
var filhos = {};
for (var j = idx + 1; j < linhas.length; j++) {
var nivel = Number(linhas[j].getAttribute("data-dcr-nivel"));
if (nivel <= nivelPai) break;
if (nivel === nivelPai + 1) filhos[linhas[j].getAttribute("data-dcr-id")] = true;
}
return filhos;
}
// Destaque inicial (antes de qualquer clique) — pedido explícito do
// usuário depois de notar que folha genuína (linha sem filho
// nenhum, ex. "(-) SIMPLES NACIONAL") ficava de fora do destaque
// mesmo sendo visualmente tão "de baixo" quanto um grupo colapsado
// por padrão no mesmo nível. Usar só `colapsadas` (que só marca
// linha que TEM filho escondido) deixa de fora exatamente essas
// folhas, já que elas nunca entram nesse conjunto.
//
// Critério correto, que cobre os dois casos com a mesma regra:
// entre as linhas realmente visíveis com este `colapsadas`, uma
// linha entra no destaque se a próxima linha visível **não** for
// mais profunda que ela — nada foi revelado logo abaixo dela agora,
// seja porque está colapsada (filhos escondidos) ou porque é uma
// folha sem filho nenhum.
function calculaDestaqueInicial() {
var visiveis = [];
var pilha = [];
linhas.forEach(function (tr) {
var nivel = Number(tr.getAttribute("data-dcr-nivel"));
while (pilha.length && pilha[pilha.length - 1] >= nivel) pilha.pop();
var oculta = pilha.length > 0;
var id = tr.getAttribute("data-dcr-id");
var temFilhos = tr.getAttribute("data-dcr-tem-filhos") === "1";
var temFilhoOculto = temFilhos && !!colapsadas[id];
if (temFilhoOculto) pilha.push(nivel);
if (!oculta) visiveis.push({ id: id, nivel: nivel, temFilhoOculto: temFilhoOculto });
});
var resultado = {};
visiveis.forEach(function (visivel, i) {
var proximo = visiveis[i + 1];
var eraFimDoRamo = !proximo || proximo.nivel <= visivel.nivel;
if (!eraFimDoRamo) return;
// Nível 0 só entra no destaque quando realmente esconde algo
// (grupo colapsado) — uma sequência de linhas de resultado
// irmãs no nível raiz da DRE não é o fim de um ramo aninhado,
// são linhas soltas lado a lado (mesmo ajuste de
// dcUltimaLevaVisivel() em dashboard-contabil.js).
if (visivel.nivel === 0 && !visivel.temFilhoOculto) return;
resultado[visivel.id] = true;
});
return resultado;
}
var destaque = calculaDestaqueInicial();
function aplicaDestaque() {
linhas.forEach(function (tr) {
tr.classList.toggle("dcr-destaque", !!destaque[tr.getAttribute("data-dcr-id")]);
});
}
function atualiza() {
var pilha = [];
linhas.forEach(function (tr) {
@ -703,16 +1098,69 @@
if (!btn) return;
var tr = btn.closest("tr");
var id = tr.getAttribute("data-dcr-id");
colapsadas[id] = !colapsadas[id];
var estavaColapsada = !!colapsadas[id];
colapsadas[id] = !estavaColapsada;
btn.classList.toggle("is-expanded", !colapsadas[id]);
if (estavaColapsada) {
historico.push(destaque);
destaque = filhosDiretos(id);
} else {
destaque = historico.pop() || {};
}
aplicaDestaque();
atualiza();
});
aplicaDestaque();
atualiza();
}
// ---- Balancete/D.R.E.: painel de observação inline ----
// Ícone ao lado da descrição (só aparece quando a conta/linha tem uma
// observação que o contador marcou como visível ao cliente, ver
// `oculta_no_relatorio` — as ocultas nem chegam a este HTML) — clicar
// abre a linha logo abaixo com o texto, mesmo comportamento do editor
// inline da tela de revisão do Portal. Fica escondida por padrão
// (`hidden` no HTML) e some de novo se a linha-pai for escondida por
// um ancestral colapsado (`sincroniza()`, chamada depois de qualquer
// clique no corpo da tabela, inclusive os de expandir/recolher de
// `pidDcrArvore` — os dois listeners ficam no mesmo `tbody`).
function pidDcrObs(tbodyId) {
var tbody = document.getElementById(tbodyId);
if (!tbody) return;
var abertas = {};
function sincroniza() {
tbody.querySelectorAll("[data-dcr-obs-row]").forEach(function (obsRow) {
var id = obsRow.getAttribute("data-dcr-obs-row");
var linhaPai = obsRow.previousElementSibling;
var paiOculto = !linhaPai || linhaPai.hidden;
obsRow.hidden = paiOculto || !abertas[id];
});
}
tbody.addEventListener("click", function (event) {
var btn = event.target.closest("[data-dcr-obs-toggle]");
if (btn) {
var id = btn.getAttribute("data-dcr-obs-toggle");
abertas[id] = !abertas[id];
btn.classList.toggle("is-aberto", abertas[id]);
}
// Sincroniza sempre, não só quando o clique foi no próprio botão de
// observação — um clique em "recolher/expandir" (pidDcrArvore) pode
// ter acabado de esconder a linha-pai de um painel que estava aberto.
sincroniza();
});
sincroniza();
}
pidDcrArvore("dcr-balancete-body");
pidDcrObs("dcr-balancete-body");
pidDcrArvore("dcr-dre-body");
pidDcrObs("dcr-dre-body");
pidDcrArvore("dcr-av-body");
pidDcrObs("dcr-av-body");
// ---- Cards: contagem animada + cor por sinal/liquidez ----
// O texto final exibido é sempre `data-final` (a string já formatada

View File

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>P.I.D. — Dashboard Contábil</title>
<title>P.I.D. — Relatório Contábil</title>
<link rel="icon" type="image/svg+xml" href="{% static 'img/pid-icone-escuro.svg' %}" />
<link rel="stylesheet" href="{% static 'css/tokens.css' %}" />
<link rel="stylesheet" href="{% static 'css/base.css' %}" />
@ -137,7 +137,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool is-active">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>
@ -268,7 +268,7 @@
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Alternar menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16" stroke-linecap="round"/></svg>
</button>
<h1 class="topbar__title">Dashboard Contábil</h1>
<h1 class="topbar__title">Relatório Contábil</h1>
<div class="topbar__actions">
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Ativar modo claro">
@ -319,7 +319,7 @@
<main class="page-content page-content--wide">
<div id="dc-no-access" class="no-access" hidden>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3" stroke-linecap="round" stroke-linejoin="round"/></svg>
<h2>Sem acesso ao Dashboard Contábil</h2>
<h2>Sem acesso ao Relatório Contábil</h2>
<p>Seu perfil de acesso não tem permissão para visualizar esta tela. Solicite à Inovação a liberação em Perfis de Acesso.</p>
</div>
@ -327,7 +327,7 @@
<div id="dc-list-view">
<div class="dc-header">
<div>
<h2>Dashboard Contábil</h2>
<h2>Relatório Contábil</h2>
<p class="dc-subtitle">Anexe o balancete + DRE em PDF (modelo Questor) de uma empresa — a ferramenta extrai as contas, roda um conjunto de checagens automáticas e apresenta as observações para análise antes da conclusão.</p>
</div>
<button type="button" class="btn-solid" id="dc-new-btn">
@ -395,7 +395,7 @@
</div>
<div class="dc-header-actions">
<button type="button" class="btn-outline" id="dc-review-back-btn">Voltar</button>
<button type="button" class="btn-outline" id="dc-gerar-dashboard-btn">Gerar Dashboard</button>
<button type="button" class="btn-outline" id="dc-gerar-dashboard-btn">Gerar Relatório</button>
<button type="button" class="btn-solid" id="dc-concluir-btn">Concluir Análise</button>
</div>
</div>
@ -406,6 +406,7 @@
<button type="button" class="pa-tab is-active" data-dc-tab="achados">Observações</button>
<button type="button" class="pa-tab" data-dc-tab="balancete">Balancete</button>
<button type="button" class="pa-tab" data-dc-tab="dre">DRE</button>
<button type="button" class="pa-tab" data-dc-tab="analise-vertical" id="dc-av-tab-btn" hidden>Análise Vertical</button>
<button type="button" class="pa-tab" data-dc-tab="dashboard">Dashboard</button>
</div>
@ -461,6 +462,11 @@
<tbody id="dc-contas-body"></tbody>
</table>
</div>
<section class="dc-obs-resumo">
<h3 class="dc-obs-resumo__titulo">Observações do Balancete <span class="dc-obs-resumo__count" id="dc-contas-obs-count">0</span></h3>
<div class="dc-dash-obs-list" id="dc-contas-obs-list"></div>
<p class="dc-empty" id="dc-contas-obs-empty" hidden>Nenhuma observação registrada no Balancete ainda.</p>
</section>
</div>
<div class="pa-tab-panel" data-dc-panel="dre" hidden>
@ -476,6 +482,29 @@
<tbody id="dc-dre-body"></tbody>
</table>
</div>
<section class="dc-obs-resumo">
<h3 class="dc-obs-resumo__titulo">Observações da DRE <span class="dc-obs-resumo__count" id="dc-dre-obs-count">0</span></h3>
<div class="dc-dash-obs-list" id="dc-dre-obs-list"></div>
<p class="dc-empty" id="dc-dre-obs-empty" hidden>Nenhuma observação registrada na DRE ainda.</p>
</section>
</div>
<div class="pa-tab-panel" data-dc-panel="analise-vertical" hidden>
<div class="pa-table-wrap">
<table class="pa-table dc-dre-table dc-av-table">
<thead>
<tr id="dc-av-head-row">
<th>Descrição</th>
</tr>
</thead>
<tbody id="dc-av-body"></tbody>
</table>
</div>
<section class="dc-obs-resumo">
<h3 class="dc-obs-resumo__titulo">Observações da Análise Vertical <span class="dc-obs-resumo__count" id="dc-av-obs-count">0</span></h3>
<div class="dc-dash-obs-list" id="dc-av-obs-list"></div>
<p class="dc-empty" id="dc-av-obs-empty" hidden>Nenhuma observação registrada na Análise Vertical ainda.</p>
</section>
</div>
<div class="pa-tab-panel" data-dc-panel="dashboard" hidden>
@ -485,9 +514,20 @@
<section class="dc-dash-section">
<div class="dc-dash-section__header">
<h3 class="dc-dash-section__titulo">Indicadores</h3>
<h3 class="dc-dash-section__titulo">Resumo</h3>
<button type="button" class="btn-outline" id="dc-indicadores-gerenciar-btn">Gerenciar Indicadores</button>
</div>
<div class="dc-dash-resumo-fechamento">
<h4 class="dc-dash-resumo-fechamento__titulo">Resumo do Fechamento</h4>
<p class="dc-dash-resumo-fechamento__dica">Escreva aqui as considerações e análises deste fechamento — cole (Ctrl+V) ou arraste uma imagem pra incluí-la no texto. Aparece no relatório "Gerar Dashboard", antes dos indicadores.</p>
<div id="dc-dash-resumo-fechamento-editor" class="dc-dash-richtext" contenteditable="true" data-placeholder="Ex.: A escrituração contábil da empresa foi atualizada até .../.../.... Considerações: o resultado do exercício foi..."></div>
<div class="dc-dash-resumo-fechamento__acoes">
<button type="button" class="btn-solid" id="dc-dash-resumo-fechamento-salvar-btn">Salvar</button>
<span class="dc-dash-resumo-fechamento__status" id="dc-dash-resumo-fechamento-status" hidden></span>
</div>
</div>
<p class="dc-dash-section__dica">Clique num card pra ver a fórmula usada.</p>
<div id="dc-dash-indicadores"></div>
</section>
@ -504,21 +544,6 @@
</div>
</div>
<div class="modal-overlay" id="dc-observacao-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title">Observação</h2>
<p class="modal-card__subtitle" id="dc-observacao-modal-conta"></p>
<div class="modal-field">
<textarea id="dc-observacao-modal-texto" rows="6" placeholder="Anote sua análise..."></textarea>
</div>
<p class="modal-error" id="dc-observacao-modal-error"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" id="dc-observacao-modal-cancel-btn">Cancelar</button>
<button type="button" class="btn-solid" id="dc-observacao-modal-save-btn">Salvar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="dc-achado-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title" id="dc-achado-modal-titulo"></h2>
@ -603,6 +628,11 @@
Indicador padrão (aparece automaticamente em todas as apurações — desmarque pra deixar disponível só quando selecionado em "Gerenciar Indicadores")
</label>
<div class="modal-field">
<label>Ícone (aparece no card do relatório "Gerar Dashboard")</label>
<div class="dc-ind-icone-grid" id="dc-indicador-icones"></div>
</div>
<div class="dc-ind-componentes-header">
<span>Componentes</span>
<button type="button" class="btn-outline" id="dc-indicador-add-componente-btn">+ Adicionar componente</button>
@ -610,11 +640,17 @@
<div id="dc-indicador-componentes" class="dc-ind-componentes-lista"></div>
<div class="modal-field">
<label for="dc-indicador-modal-formula">Fórmula</label>
<label for="dc-indicador-modal-formula">Fórmula (cálculo interno)</label>
<input type="text" id="dc-indicador-modal-formula" placeholder="ex.: (receita - custo) / receita" />
<p class="dc-ind-formula-hint" id="dc-indicador-modal-formula-hint">Use a chave de cada componente acima, combinadas com +, -, * , / e parênteses.</p>
</div>
<div class="modal-field">
<label for="dc-indicador-modal-formula-exibicao">Fórmula (como aparece ao cliente)</label>
<input type="text" id="dc-indicador-modal-formula-exibicao" placeholder="ex.: Lucro Líquido − Despesas Financeiras" />
<p class="dc-ind-formula-hint">Texto livre, só pra exibição no relatório "Gerar Dashboard" — não precisa (e não deve) usar as chaves técnicas dos componentes. Se deixar em branco, o relatório mostra a fórmula de cálculo interno acima.</p>
</div>
<button type="button" class="btn-outline" id="dc-indicador-calcular-btn">Calcular com esta apuração</button>
<div class="dc-ind-preview" id="dc-indicador-preview" hidden></div>

View File

@ -137,7 +137,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -137,7 +137,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -137,7 +137,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -137,7 +137,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -137,7 +137,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -136,7 +136,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -138,7 +138,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -137,7 +137,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>

View File

@ -136,7 +136,7 @@
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Dashboard Contábil
Relatório Contábil
</a>
</li>
</ul>