` (o `` da observação expandida) — só a linha "normal" da conta/linha tem a classe.
### Filtros de template (`portal_api/templatetags/contabil_extras.py`)
Primeiro (e único) uso de template tags customizadas no projeto. `moeda`/`percentual`/`indice`/`competencia` no padrão brasileiro; `None` sempre vira "—", nunca "R$ 0,00"/"0,00%". `numero_bruto` devolve o valor cru só para o atributo `data-count` da animação, nunca para texto exibido. `mes_curto` para o cabeçalho da Análise Vertical.
**`moeda_av`/`percentual_av` são separados de propósito**: `ContabilLinhaAnaliseVertical.valores` grava valor/percentual como **texto**, então `moeda` (que faria `f"{valor:,.2f}"`) falharia contra uma string, e `percentual` multiplicaria por 100 um número que já é um percentual pronto. Mesmo cuidado no JS: `pidDcFormatPercentualAnaliseVertical()` é deliberadamente diferente de `pidDcFormatIndicadorPercentual()` — usar o errado exibiria `10000,00%`.
Um indicador personalizado chega ao contexto **já formatado como texto** (`_contabil_formata_indicador()`), porque o Django Template Language não permite escolher um filtro por nome vindo de variável.
### Visual e animações
Fontes "Manrope" (títulos/cards/abas), "Inter" (corpo, `font-variant-numeric: tabular-nums` nas colunas de valor) e "JetBrains Mono" (fórmula do verso do card). Paleta roxo/dourado dos documentos do escritório (`#3d2178`/`#281552`/`#b4872a`), com gradiente e glow radial sutil no cabeçalho.
> **Regra de ouro para toda animação de entrada aqui: nunca fixar `opacity:0`/`transform` como estilo estático fora de um `@keyframes`, só via `animation: nome duração easing both;`.** Isso garante que `@media print { * { animation: none !important; } }` sozinho devolve o elemento ao estado normal na impressão, sem reset explícito por seletor. Animação nova que não siga isso pode fazer a impressão sair em branco.
**Contagem animada dos cards**: `data-count`/`data-final`/`data-format`/`data-color-rule` alimentam `pidDcrAnimaContadores()`, que anima de 0 até o valor com `requestAnimationFrame`, formatando os quadros intermediários com `toLocaleString("pt-BR")`. O texto **final** é sempre `data-final`, a string que os filtros Django já geraram — nunca um valor recalculado em JS.
Cor por sinal só onde é seguro: `sign` (ROA/ROE/EBIT/EBITDA — positivo verde, negativo vermelho) e `liquidez` (verde se ≥ 1). **Kanitz, composição/grau de endividamento e IPL não ganham cor nenhuma** — não existe limiar validado para eles, e colorir como "bom"/"ruim" daria falsa segurança num número que o próprio card diz ser estimativa.
**Cards só animam depois da aba Resumo ser aberta** (contar um número invisível não faz sentido). Isso cria um risco de impressão: se o usuário nunca abrir a aba e mandar imprimir, a contagem começaria do zero na hora da captura. O **único** listener de `beforeprint` do documento decide entre inicializar tudo já no valor final (aba nunca aberta) ou só finalizar uma contagem em andamento — nunca as duas coisas competindo.
**Flip card**: `.dcr-card` é só a cena 3D (`perspective` + `min-height`, necessário porque as duas faces são `position:absolute` e não contribuem para a altura); `.dcr-card-inner` gira no hover do pai; cada face tem `backface-visibility:hidden` e carrega o fundo/borda/sombra/padding. O verso mostra nome + descrição (`"Sem descrição cadastrada."` se vazia) + a fórmula de exibição, centralizados. Como `@media print` zera toda animação, a impressão sempre mostra a frente.
> **O relatório é uma foto estática do momento em que foi gerado.** Editar a definição de um indicador depois não atualiza um relatório já aberto ou baixado — precisa clicar "Gerar Relatório" de novo. Isso já gerou uma dúvida ("o card diz 'Sem descrição cadastrada' mas o indicador tem descrição") que não era bug.
### Exportações
- **XLSX** (`exportacao.py`, funções puras com openpyxl, recebendo dataclasses `LinhaBalanceteXlsx`/`LinhaDreXlsx`, nunca os models): um botão por seção (Balancete/D.R.E.), `` puro sem JS. Cabeçalho mesclado (título/empresa/CNPJ/competência), linha de cabeçalho de colunas com fundo roxo, dados a partir da linha 6 com `freeze_panes`. Conta sintética e linha totalizadora ganham negrito + fundo dourado claro. A hierarquia vira `Alignment(indent=nivel)` — não dá para reproduzir toggle numa planilha, então a árvore nasce totalmente expandida. Valores gravados como `float` com `number_format = '"R$" #,##0.00'`, continuam somáveis no Excel.
- **PDF do Resumo** (`resumo_pdf.py`, pacote puro, recebe a apuração já carregada e o dict de `_contabil_dados_resumo()`): Resumo do Fechamento + Indicadores + Observações, sem Balancete/D.R.E./Análise Vertical (que já têm o caminho XLSX). Banner roxo/dourado com `logo-branco.png`. O texto rico do contador vira flowables do reportlab (`_resumo_fechamento_flowables()`, via BeautifulSoup): `_inline_markup()` reconstrói `b`/`i`/`u`/`br` aninhados na marcação que o `Paragraph` entende (`strong`→`b`, `em`→`i`), `ul`/`ol` viram `ListFlowable`, e ` ` (a única forma que o editor produz) vira um `Image` decodificado de base64 em memória, redimensionado para a largura útil. Seção pulada se o resumo estiver vazio.
**`_contabil_dados_resumo(apuracao)`** é compartilhada por `dashboard()` e `resumo_pdf()`: calcula os grupos de indicadores e devolve `observacoes_visiveis` **sem** separar por `alvo_tipo` nem calcular `ancora` (isso é específico do relatório HTML). `dashboard()` faz a separação e as âncoras por cima.
## Riscos conhecidos e armadilhas
- **Códigos de classificação fixos** — se um cliente usar plano de contas com numeração diferente, indicadores e a regra de caixa saem errados **silenciosamente**. Revisar contra mais balancetes reais de empresas diferentes antes de confiar cegamente num valor exibido ao cliente.
- **Descrições coladas em PDF de fonte atípica** — não têm correção segura; a ferramenta avisa por badge. Valores monetários nunca são afetados.
- **Duas cópias mantidas à mão**: os SVGs de ícone (Python + JS) e a lógica de árvore/destaque (tela de revisão + relatório). Mudança num lado exige o outro.
- **Duas constantes conceituais duplicadas**: `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (Python), hoje com papéis diferentes (sob demanda na revisão, padrão no relatório).
- **`total_achados_pendentes` gera N+1** na listagem de apurações.
- **Testar exclusão nesta ViewSet destrói arquivo de verdade**: `perform_destroy()` chama `instance.arquivo.delete(save=False)`, e apagar arquivo do storage **não é revertido** por `transaction.set_rollback(True)` — o padrão de teste usado no resto desta documentação protege só o banco. Validar o caminho "204" contra uma apuração real já custou o PDF anexado dela (o registro voltou pelo rollback, o arquivo não). Nenhum dado analítico depende desse arquivo (`arquivo` não é exposto em serializer nenhum e `reprocessar()` sempre grava um upload novo), mas um teste futuro precisa de apuração descartável ou storage isolado. Ver `[[feedback_rollback_nao_desfaz_arquivo]]` na memória.
- **Mudança em `.py` exige reiniciar o `runserver`** para o usuário conseguir testar; mudança só em `.html`/`.css`/`.js` não exige. Isso já mascarou um diagnóstico ("não funciona" que não era bug de código). Ver `[[feedback_py_edit_precisa_restart_runserver]]` na memória.
|