Reestruturação do balancete e DRE

This commit is contained in:
Gabriel 2026-09-10 11:55:56 -03:00
parent 4676412325
commit c87c3cba97
6 changed files with 132 additions and 24 deletions

View File

@ -115,3 +115,19 @@ Novo botão "Calcular com esta apuração" no modal de indicador, entre "Fórmul
Usuário reportou o modal de aviso "Chave(s) de indicador não padrão inexistente(s): diferença." ao tentar alternar um indicador no hub "Gerenciar Indicadores". Causa: um indicador não padrão de chave `diferença` tinha sido excluído em algum momento, mas `IndicadorContabilDefinicaoViewSet.perform_destroy()` só bloqueava/limpava referência pela **fórmula** (`indicador_referenciado`), nunca pelas referências em `ContabilApuracao.indicadores_selecionados`/`indicadores_ocultos` — a chave ficou órfã na apuração que a tinha selecionada. Como o frontend sempre reenvia a lista **inteira** a cada alternância de checkbox (`pidDcRenderHubIndicadores`/evento `change` em `dashboard-contabil.js`), e `ContabilIndicadoresSelecionadosSerializer`/`ContabilIndicadoresOcultosSerializer` rejeitavam a lista inteira se qualquer chave nela não existisse mais, o usuário ficava travado sem conseguir alternar **nenhum** indicador na apuração afetada, não só o excluído.
Dois ajustes, um pro sintoma já existente e outro pra causa raiz: (a) as duas validações passaram a **descartar silenciosamente** chave inexistente/órfã em vez de rejeitar a lista inteira — é só estado de exibição (quais cards aparecem/estão ocultos), não dado auditado, então autocorrigir é seguro e resolve o travamento já em produção sem precisar de um script de correção manual; (b) `perform_destroy()` agora também limpa a chave excluída de toda `ContabilApuracao` que a referenciava em `indicadores_selecionados`/`indicadores_ocultos`, pra não deixar mais nenhuma referência órfã nova daqui pra frente.
### 107. Balancete/D.R.E. nascem recolhidos a partir do "grupo 4"
Pedido: a árvore do Balancete (e, por extensão confirmada com o usuário, da D.R.E.) nascia sempre totalmente expandida, poluindo visualmente uma apuração com muitas contas analíticas. Passou a nascer recolhida a partir do nível equivalente ao "grupo 4" do código de classificação (ex. `1.01.01.001`, 4 segmentos) — essa conta aparece aberta, mas seus filhos (`1.01.01.001.001` em diante) ficam ocultos até o contador clicar pra expandir; o mesmo limiar de nível é aplicado à D.R.E., sobre `ContabilLinhaDre.nivel`.
Aplicado nos dois lugares que já compartilhavam o mesmo algoritmo de árvore recolhível: a tela de revisão (`dashboard-contabil.js`, `dcColapsoPadrao()` pré-popula `dcContasColapsadas`/`dcDreColapsadas` em `renderRevisao()`, em vez de nascerem como `Set()` vazio) e o relatório "Gerar Dashboard" (`_contabil_arvore_contexto()` em `views.py` ganhou o campo `colapsado_padrao` por item, calculado a partir da nova constante `_CONTABIL_NIVEL_ABERTO_PADRAO = 3`; `dashboard-contabil-relatorio.html` usa esse campo pra não marcar `is-expanded` no botão e `pidDcrArvore()` semeia o estado `colapsadas` a partir do atributo `data-dcr-colapsado-padrao` antes da primeira renderização). A impressão continua forçando toda linha a aparecer (`tr[hidden] { display: table-row !important }` em `@media print`), então o comportamento de "documento impresso nunca esconde conta atrás de um grupo recolhido" não muda. Nenhuma mudança de modelo/migração — é só o estado inicial da mesma árvore que já existia.
### 108. Fundo uniforme na tabela do Balancete (revisão)
Pedido: a linha sintética do Balancete (grupo do plano de contas, ex. "1.02.05 IMOBILIZADO") tinha um fundo elevado (`--bg-surface-raised`) além do negrito, criando um efeito de cores alternadas entre linha de grupo e linha analítica — o usuário pediu pra unificar, no mesmo padrão já usado na D.R.E. (que só usa negrito na linha totalizadora, sem fundo diferente). Removida a regra `.dc-conta-row--sintetica td { background: var(--bg-surface-raised); }` em `dashboard-contabil.css`, mantendo só o negrito (`.dc-conta-row--sintetica { font-weight: 600; }`) — a tabela do Balancete passa a ter uma única cor de fundo, igual à D.R.E.
### 109. Limiar de recolhimento ajustado pra nível 3 + destaque acompanha o grupo expandido
Pedido: (a) o limiar de recolhimento padrão da rodada 107 ("grupo 4", ex. `1.01.01.001` aberto) ficou permissivo demais — passou a recolher a partir do 3º segmento do código (ex. `1.01.01` aberto, `1.01.01.001` em diante só sob demanda). `PID_DC_NIVEL_ABERTO_PADRAO` (`dashboard-contabil.js`) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (`views.py`, usada pelo relatório "Gerar Dashboard") foram de `3` pra `2` — mesma dupla de constantes já documentada na rodada 107, ajustadas juntas.
(b) Novo mecanismo de destaque: ao expandir uma conta/linha, só as linhas que acabaram de ficar visíveis (filhos diretos, não os netos — que continuam recolhidos pelo limiar acima) ganham um realce dourado; expandir uma delas em seguida move o realce pra seus filhos e o grupo destacado antes volta à cor padrão, nunca acumulando mais de uma "leva" ao mesmo tempo — ajuda o contador a acompanhar visualmente onde ele acabou de descer na árvore. `dcContasDestaque`/`dcDreDestaque` (`dashboard-contabil.js`, dois `Set()` resetados em `renderRevisao()`) guardam só a leva mais recente; `dcFilhosDiretos()` (função genérica, reaproveitada pelas duas árvores) calcula os filhos diretos de um item a partir da lista plana. Classe nova `.dc-conta-row--destaque` (`dashboard-contabil.css`, fundo `rgba(var(--gold-rgb), .16)`). Só na tela de revisão — o relatório "Gerar Dashboard" não ganhou esse destaque (é uma navegação estática, sem o mesmo conceito de "acabei de expandir isto agora"). Puramente visual/em memória, sem persistência nem mudança de modelo.

View File

@ -65,7 +65,11 @@ Diferente de `IndicadorApuracaoViewSet`/`ImportacaoPlanoSaudeViewSet` (onde a ch
`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.
**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()` reiniciados juntos em `renderRevisao()`).
**Balancete e DRE usam a mesma árvore recolhível** (`dashboard-contabil.js`): o Balancete já construía uma árvore expansível a partir do nível de indentação derivado do código de classificação (`dcContaNivel()`, contando segmentos separados por `.`) — a DRE não tem código de classificação (ver "Extração do PDF" acima), mas já carregava `nivel` pronto do backend (`ContabilLinhaDre.nivel`, derivado do `x0` de cada linha no PDF), então `renderDre()` reaproveita exatamente o mesmo algoritmo de `renderContas()` (pilha de níveis recolhidos, "tem filhos" = a próxima linha tem nível maior) só que sobre `linha.nivel` direto, sem precisar de um `dcContaNivel` equivalente. Reaproveita as mesmas classes CSS do toggle (`.dc-conta-toggle`/`.dc-conta-toggle-spacer`/`.dc-conta-desc-cell`, `dashboard-contabil.css`) — o nome genérico ("conta") já cobre as duas árvores, não precisou de classe nova. Estado de colapso é independente por aba (`dcContasColapsadas`/`dcDreColapsadas`, dois `Set()`).
**Nasce recolhida a partir do 3º segmento do código** (pedido explícito do usuário, pra reduzir a poluição visual de uma apuração com muitas contas analíticas — limiar ajustado numa rodada seguinte, ver abaixo): em vez de `renderRevisao()` reiniciar os dois `Set()` vazios (tudo expandido), `dcColapsoPadrao(itens, nivelFn)` os pré-popula com os ids de todo item que tem filhos **e** está no nível `PID_DC_NIVEL_ABERTO_PADRAO` (`2`) ou além — ex. a conta `1.01.01` (3 segmentos, nível 2) aparece aberta, mas seus filhos (`1.01.01.001`, nível 3) ficam ocultos até o contador clicar pra expandir; se expandido, um filho de nível 3 que também tenha netos nasce recolhido de novo pelo mesmo critério, então "nível 3 em diante" precisa sempre de um clique a mais, não só a primeira camada. Mesmo limiar aplicado à DRE, sobre `Math.max(0, linha.nivel)` — decisão confirmada com o usuário (a princípio o pedido citava só o Balancete, mas como a DRE reaproveita o mesmo algoritmo, o mesmo comportamento faz sentido nela também). O relatório "Gerar Dashboard" replica esse mesmo estado inicial (ver abaixo) — não é só a tela de revisão. `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (`views.py`) são a mesma constante conceitual duplicada nos dois lados (um é SPA, o outro HTML renderizado uma vez) — mudar o limiar exige ajustar os dois.
**Destaque acompanha o último grupo expandido** (pedido explícito do usuário, mesma rodada do ajuste de limiar acima): ao expandir uma conta/linha, as linhas que acabaram de ficar visíveis (só os filhos **diretos**, não os netos — que continuam recolhidos pelo limiar acima) ganham um realce dourado (`.dc-conta-row--destaque`); se o contador expandir uma dessas linhas em seguida, o realce se move pros filhos dela, e o grupo destacado antes volta pra cor padrão — nunca acumula mais de uma "leva" destacada ao mesmo tempo. `dcContasDestaque`/`dcDreDestaque` (dois `Set()`, um por árvore, resetados em `renderRevisao()`) guardam só a leva mais recente; `dcFilhosDiretos(itens, nivelFn, id)` (função genérica, reaproveitada pelas duas árvores) varre a lista plana a partir do índice do item expandido e para no primeiro item de nível igual ou menor (fim do grupo), coletando só os de nível exatamente `pai+1`. O handler de clique do toggle decide o destino do destaque **antes** de checar se a ação foi expandir ou recolher (`estavaColapsada = colapsadas.has(id)` guardado antes de mutar o `Set`): expandir substitui `dcContasDestaque`/`dcDreDestaque` pelos filhos diretos recém-revelados; recolher só limpa (`new Set()`), já que nada novo foi exposto. Puramente visual, sem persistência — reseta a cada apuração aberta, sem afetar nenhum dado gravado.
**Card de achado expande a conta usada no apontamento** (`renderAchados()`, pedido explícito do usuário): `achado.conta` (id, já vem no payload de `/api/contabil-apuracoes/{id}/`, junto de `conta_codigo`/`conta_descricao`) é resolvido pra objeto completo procurando em `apuracaoAtual.contas` (`.find((c) => c.id === achado.conta)`) — sem chamada de API extra, já que a apuração inteira (contas + linhas de DRE + achados) já vem de uma vez só nesse endpoint. Só achados vinculados a uma conta específica ganham o botão "Ver conta usada no apontamento" (`.dc-achado-card__toggle-conta`) — as duas regras gerais (`balanceamento_ativo_passivo`/`debito_credito_divergente`, `conta` nulo no model) não têm uma conta única por trás, então não mostram o toggle. Expandido, mostra código/descrição/saldo anterior/débito/crédito/saldo atual da conta (`.dc-achado-card__conta`, um `<dl>` em grid). Estado de expansão (`dcAchadosContaExpandida`, um `Set()` de ids de achado) segue o mesmo padrão de `dcContasColapsadas`/`dcDreColapsadas` — reiniciado em `renderRevisao()`.
@ -112,7 +116,9 @@ Documento HTML autocontido — `<style>` inline com a mesma paleta já usada nos
**Observações espalhadas nas 3 abas, cada uma com o recorte certo** (pedido explícito do usuário — antes ficavam todas juntas numa única seção fora das abas): a aba **Balancete** termina com "Observações do Balancete" (só `observacoes_contas`); a aba **D.R.E.** termina com "Observações da D.R.E." (só `observacoes_dre`); a aba **Indicadores** termina com "Todas as Observações da Análise" (`observacoes_contas` + `observacoes_dre` + `achados_com_observacao` juntos, cada item com um prefixo indicando a origem — "Balancete — ...", "D.R.E. — ...", "Auditoria — ..." — já que aqui não há mais uma aba própria pra inferir o contexto). **Nunca "Achado"/"Achado de Auditoria" em texto visível** — pedido explícito do usuário, mesmo motivo pelo qual a aba de revisão já se chama "Observações" (`data-dc-tab="achados"` com o texto "Observações", `dashboard-contabil.html`) e não "Achados"; `achado`/`ContabilAchado`/`achados_com_observacao` continuam normais como nome de variável/model/classe, só não podem aparecer como palavra na tela. As três seções reaproveitam o mesmo markup (`.dcr-obs-lista`/`.dcr-obs-item`), só filtrando quais das três listas do contexto (`observacoes_contas`/`observacoes_dre`/`achados_com_observacao`, já vindas prontas de `views.py`) cada uma itera — nenhuma mudança no backend foi necessária, é só reorganização do template. Estado vazio próprio por seção ("Nenhuma observação registrada no Balancete."/"...na D.R.E."/"...nesta análise.").
**Balancete e D.R.E. têm árvore recolhível igual à tela de revisão** (pedido explícito do usuário — a primeira versão do relatório vinha totalmente expandida, sem toggle). Diferença de arquitetura em relação a `renderContas()`/`renderDre()` em `dashboard-contabil.js`: lá é uma SPA que re-renderiza a tabela inteira a cada clique; aqui é HTML estático gerado uma vez, então o nível de cada linha e se ela "tem filhos" (`nivel`/`tem_filhos`) são calculados **no servidor** (`_contabil_arvore_contexto()` em `views.py`, mesmo algoritmo — "tem filhos" = a próxima linha tem nível maior) e ficam como atributos `data-dcr-nivel`/`data-dcr-tem-filhos`/`data-dcr-id` em cada `<tr>` já renderizada. `pidDcrArvore(tbodyId)` (JS inline no template) só alterna o atributo `hidden` das `<tr>` existentes com a mesma lógica de pilha de níveis recolhidos, sem reconstruir HTML nenhum. Tudo nasce expandido (nenhum recolhido por padrão). Na impressão, `.dcr-tabela tbody tr[hidden] { display: table-row !important; }` força toda linha a aparecer mesmo que o usuário tenha recolhido algum grupo na tela — documento impresso não deve esconder conta nenhuma atrás de um grupo recolhido.
**Balancete e D.R.E. têm árvore recolhível igual à tela de revisão** (pedido explícito do usuário — a primeira versão do relatório vinha totalmente expandida, sem toggle). Diferença de arquitetura em relação a `renderContas()`/`renderDre()` em `dashboard-contabil.js`: lá é uma SPA que re-renderiza a tabela inteira a cada clique; aqui é HTML estático gerado uma vez, então o nível de cada linha e se ela "tem filhos" (`nivel`/`tem_filhos`) são calculados **no servidor** (`_contabil_arvore_contexto()` em `views.py`, mesmo algoritmo — "tem filhos" = a próxima linha tem nível maior) e ficam como atributos `data-dcr-nivel`/`data-dcr-tem-filhos`/`data-dcr-id` em cada `<tr>` já renderizada. `pidDcrArvore(tbodyId)` (JS inline no template) só alterna o atributo `hidden` das `<tr>` existentes com a mesma lógica de pilha de níveis recolhidos, sem reconstruir HTML nenhum. Na impressão, `.dcr-tabela tbody tr[hidden] { display: table-row !important; }` força toda linha a aparecer mesmo que o usuário tenha recolhido algum grupo na tela — documento impresso não deve esconder conta nenhuma atrás de um grupo recolhido.
**Nasce recolhido a partir do "grupo 4", mesmo limiar da tela de revisão** (rodada seguinte, mesmo pedido — ver "Nasce recolhida a partir do 'grupo 4'" acima): `_contabil_arvore_contexto()` ganhou um terceiro campo por item, `colapsado_padrao` (`tem_filhos and nivel >= _CONTABIL_NIVEL_ABERTO_PADRAO`, constante módulo-level `= 3`), viram `data-dcr-colapsado-padrao="1"/"0"` em cada `<tr>`; o botão de toggle só ganha a classe `is-expanded` inicial quando `not item.colapsado_padrao`. `pidDcrArvore()` lê esse atributo **antes** do primeiro clique e semeia o objeto `colapsadas` (antes só populado por interação do usuário) com os ids marcados, chamando `atualiza()` uma vez na inicialização — o resto do algoritmo (pilha de níveis, alternar `hidden`) não mudou. Mesma constante conceitual dos dois lados (`_CONTABIL_NIVEL_ABERTO_PADRAO` em `views.py` / `PID_DC_NIVEL_ABERTO_PADRAO` em `dashboard-contabil.js`), duplicada porque um é Python renderizado uma vez e o outro é JS de uma SPA — se o limiar mudar, ajustar os dois.
Gráfico de evolução do Resultado Líquido via Chart.js (`<script src="https://cdn.jsdelivr.net/npm/chart.js@4">`, snippet oficial recomendado pela biblioteca) — série injetada com `{{ evolucao|json_script:"dcr-evolucao-data" }}` (usa `DjangoJSONEncoder`, que já serializa `Decimal`/`date` sem precisar de nenhum encoder customizado) e lida em JS puro. **Inicializado sob demanda**, não no carregamento da página: o `<canvas>` nasce dentro da aba "Indicadores", escondida por padrão (Balancete é a aba inicial), e Chart.js não desenha corretamente num canvas com largura/altura zero — `inicializaGrafico()` só roda no primeiro clique nessa aba (ou no evento `beforeprint`, pra garantir que o gráfico exista na versão impressa mesmo que a aba nunca tenha sido aberta na tela).

View File

@ -3626,6 +3626,14 @@ def _contabil_agrupa_indicadores_cards(cards: list[dict[str, Any]]) -> list[dict
return [{"nome": nome, "cards": grupos[nome]} for nome in sorted(grupos, key=ordenacao)]
# A partir deste nível (equivalente ao 3º segmento de código de classificação,
# ex. "1.01.01") o Balancete/D.R.E. nascem recolhidos, tanto na tela de
# revisão (dashboard-contabil.js) quanto no relatório "Gerar Dashboard" —
# pedido explícito do usuário pra reduzir a poluição visual da árvore
# completa; o contador expande sob demanda pra ver o nível analítico abaixo.
_CONTABIL_NIVEL_ABERTO_PADRAO = 2
def _contabil_arvore_contexto(
itens: list[Any], chave: str, nivel_fn: Callable[[Any], int], px_por_nivel: int
) -> list[dict[str, Any]]:
@ -3635,17 +3643,22 @@ def _contabil_arvore_contexto(
o próximo item tem nível maior), mas calculado aqui no servidor porque o
relatório é HTML estático renderizado uma vez, não uma tela que
re-renderiza a cada clique — o JS do relatório só alterna `hidden` nas
linhas já prontas (ver `dashboard-contabil-relatorio.html`)."""
linhas já prontas (ver `dashboard-contabil-relatorio.html`). `colapsado_padrao`
marca os itens que devem nascer recolhidos (ver `_CONTABIL_NIVEL_ABERTO_PADRAO`)."""
niveis = [nivel_fn(item) for item in itens]
return [
contexto = []
for i, item in enumerate(itens):
tem_filhos = i + 1 < len(itens) and niveis[i + 1] > niveis[i]
contexto.append(
{
chave: item,
"nivel": niveis[i],
"nivel_px": niveis[i] * px_por_nivel,
"tem_filhos": i + 1 < len(itens) and niveis[i + 1] > niveis[i],
"tem_filhos": tem_filhos,
"colapsado_padrao": tem_filhos and niveis[i] >= _CONTABIL_NIVEL_ABERTO_PADRAO,
}
for i, item in enumerate(itens)
]
)
return contexto
class ContabilApuracaoViewSet(viewsets.ModelViewSet):

View File

@ -566,15 +566,24 @@
font-size: 0.82rem;
}
/* Linha sintética (grupo do plano de contas, ex. "1.01 CIRCULANTE") — em
/* Linha sintética (grupo do plano de contas, ex. "1.01 CIRCULANTE") — só
negrito pra se destacar das analíticas que ela agrupa, mesmo padrão visual
do PDF original (coluna "S/A"). */
do PDF original (coluna "S/A"); sem fundo elevado, pra manter a tabela com
uma cor de fundo única (mesmo visual já usado na DRE, que só usa negrito
pra linha totalizadora — pedido explícito do usuário pra unificar). */
.dc-conta-row--sintetica {
font-weight: 600;
}
.dc-conta-row--sintetica td {
background: var(--bg-surface-raised);
/* Destaque temporário nas contas recém-reveladas ao expandir um grupo —
pedido explícito do usuário: ao expandir uma conta, as contas que aparecem
embaixo dela ganham esse realce; ao expandir uma delas em seguida, o
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);
}
.dc-conta-desc-cell {

View File

@ -424,8 +424,10 @@ document.addEventListener("DOMContentLoaded", async () => {
document.getElementById("dc-concluida-aviso").hidden = !concluida;
document.getElementById("dc-concluir-btn").hidden = concluida;
dcContasColapsadas = new Set();
dcDreColapsadas = new Set();
dcContasColapsadas = dcColapsoPadrao(apuracao.contas, dcContaNivel);
dcDreColapsadas = dcColapsoPadrao(apuracao.linhas_dre, (linha) => Math.max(0, linha.nivel));
dcContasDestaque = new Set();
dcDreDestaque = new Set();
dcAchadosContaExpandida = new Set();
dcIndicadoresAtual = null;
renderAchados();
@ -676,6 +678,48 @@ document.addEventListener("DOMContentLoaded", async () => {
return conta.codigo.split(".").length - 1;
}
// A partir deste nível (equivalente ao 3º segmento do código de
// classificação, ex. "1.01.01") o Balancete/DRE nascem recolhidos —
// pedido explícito do usuário pra reduzir a poluição visual da árvore
// completa; o contador expande sob demanda pra ver o nível analítico
// abaixo. Mesmo limiar usado no relatório "Gerar Dashboard"
// (_CONTABIL_NIVEL_ABERTO_PADRAO em views.py).
const PID_DC_NIVEL_ABERTO_PADRAO = 2;
function dcColapsoPadrao(itens, nivelFn) {
const niveis = itens.map(nivelFn);
const colapsadas = new Set();
itens.forEach((item, i) => {
const temFilhos = i + 1 < itens.length && niveis[i + 1] > niveis[i];
if (temFilhos && niveis[i] >= PID_DC_NIVEL_ABERTO_PADRAO) colapsadas.add(item.id);
});
return colapsadas;
}
// Ids da última "leva" de linhas recém-reveladas por um clique de
// expandir — pedido explícito do usuário: destacar as contas que
// aparecem ao expandir um grupo, e mover o destaque (não acumular) pra a
// leva mais recente conforme o contador vai expandindo mais fundo. Cada
// clique de expandir SUBSTITUI esse conjunto; um clique de recolher só
// limpa. Um por árvore, já que Balancete e DRE são independentes.
let dcContasDestaque = new Set();
let dcDreDestaque = new Set();
// Filhos DIRETOS de um item (mesmo próximo nível, não os netos) — usado
// pra saber quais linhas acabaram de ficar visíveis ao expandir `id`.
function dcFilhosDiretos(itens, nivelFn, id) {
const niveis = itens.map(nivelFn);
const idx = itens.findIndex((item) => item.id === id);
const filhos = new Set();
if (idx === -1) return filhos;
const nivelPai = niveis[idx];
for (let j = idx + 1; j < itens.length; j++) {
if (niveis[j] <= nivelPai) break;
if (niveis[j] === nivelPai + 1) filhos.add(itens[j].id);
}
return filhos;
}
function renderContas() {
const body = document.getElementById("dc-contas-body");
body.innerHTML = "";
@ -708,6 +752,7 @@ document.addEventListener("DOMContentLoaded", async () => {
const tr = document.createElement("tr");
if (conta.tipo === "S") tr.classList.add("dc-conta-row--sintetica");
if (dcContasDestaque.has(conta.id)) tr.classList.add("dc-conta-row--destaque");
tr.innerHTML = `
<td>${pidDcEscapeHtml(conta.codigo)}</td>
<td style="padding-left: calc(var(--space-4) + ${nivel * 18}px)">
@ -731,8 +776,12 @@ document.addEventListener("DOMContentLoaded", async () => {
const toggleBtn = event.target.closest("[data-dc-conta-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-conta-toggle"));
if (dcContasColapsadas.has(id)) dcContasColapsadas.delete(id);
const estavaColapsada = dcContasColapsadas.has(id);
if (estavaColapsada) dcContasColapsadas.delete(id);
else dcContasColapsadas.add(id);
// Expandir troca o destaque pros filhos recém-revelados; recolher só
// limpa (nada novo foi revelado) — ver dcContasDestaque acima.
dcContasDestaque = estavaColapsada ? dcFilhosDiretos(apuracaoAtual.contas, dcContaNivel, id) : new Set();
renderContas();
return;
}
@ -773,6 +822,7 @@ document.addEventListener("DOMContentLoaded", async () => {
const tr = document.createElement("tr");
tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row";
if (dcDreDestaque.has(linha.id)) tr.classList.add("dc-conta-row--destaque");
const indentacao = nivel * 16;
tr.innerHTML = `
<td style="padding-left: calc(var(--space-4) + ${indentacao}px)">
@ -793,8 +843,11 @@ document.addEventListener("DOMContentLoaded", async () => {
const toggleBtn = event.target.closest("[data-dc-dre-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-dre-toggle"));
if (dcDreColapsadas.has(id)) dcDreColapsadas.delete(id);
const estavaColapsada = dcDreColapsadas.has(id);
if (estavaColapsada) dcDreColapsadas.delete(id);
else dcDreColapsadas.add(id);
const nivelFn = (linha) => Math.max(0, linha.nivel);
dcDreDestaque = estavaColapsada ? dcFilhosDiretos(apuracaoAtual.linhas_dre, nivelFn, id) : new Set();
renderDre();
return;
}

View File

@ -418,12 +418,12 @@
</thead>
<tbody id="dcr-balancete-body">
{% for item in contas %}
<tr{% if item.conta.tipo == "S" %} class="dcr-linha-total"{% endif %} data-dcr-nivel="{{ item.nivel }}" data-dcr-id="conta-{{ item.conta.id }}" data-dcr-tem-filhos="{% if item.tem_filhos %}1{% else %}0{% endif %}">
<tr{% if item.conta.tipo == "S" %} class="dcr-linha-total"{% endif %} data-dcr-nivel="{{ item.nivel }}" data-dcr-id="conta-{{ item.conta.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>{{ item.conta.codigo }}</td>
<td style="padding-left: {{ item.nivel_px }}px">
<span class="dcr-desc-cell">
{% if item.tem_filhos %}
<button type="button" class="dcr-toggle is-expanded" data-dcr-toggle aria-label="Recolher/expandir">
<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 %}
@ -481,11 +481,11 @@
<thead><tr><th>Descrição</th><th class="dcr-valor">Valor</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 %}">
<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 %}">
<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 is-expanded" data-dcr-toggle aria-label="Recolher/expandir">
<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 %}
@ -668,7 +668,16 @@
var tbody = document.getElementById(tbodyId);
if (!tbody) return;
var linhas = Array.prototype.slice.call(tbody.querySelectorAll("tr"));
// 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)
// nascem recolhidas, sem precisar de nenhum clique.
var colapsadas = {};
linhas.forEach(function (tr) {
if (tr.getAttribute("data-dcr-colapsado-padrao") === "1") {
colapsadas[tr.getAttribute("data-dcr-id")] = true;
}
});
function atualiza() {
var pilha = [];
@ -698,6 +707,8 @@
btn.classList.toggle("is-expanded", !colapsadas[id]);
atualiza();
});
atualiza();
}
pidDcrArvore("dcr-balancete-body");