Inclusão da conta contábil e validação das observações por classificação - Relatório Contábil

This commit is contained in:
Gabriel 2026-09-14 17:39:08 -03:00
parent affeabc061
commit 78c62d2edb
8 changed files with 164 additions and 39 deletions

View File

@ -290,3 +290,13 @@ Pedido explícito do usuário: o botão "Limpar formatação" (rodada 131), até
**Bug real, reportado pelo usuário testando a mudança acima**: na aba Análise Vertical, a coluna "Observação" (e o ícone dentro dela) aparecia cortada — não dava nem pra ler "Observação" por completo, nem pra ver o botão. Causa: `.dcr-page` (o container do relatório) tem `max-width:1140px`, e a tabela de Análise Vertical pode ter bem mais colunas que Balancete/D.R.E. (Descrição + 2 por mês + Observação, normalmente 8 no total pra 3 meses) — com `.dcr-tabela-wrap { overflow: hidden }`, o navegador espremia cada coluna até quebrar o texto do cabeçalho em 2-3 linhas e cortar a última coluna pra fora da área visível. Corrigido trocando pra `overflow-x: auto` (mantendo `overflow-y: hidden`, mesmo comportamento vertical de sempre) e `white-space: nowrap` em `table.dcr-tabela th` — agora a tabela cresce além do container quando precisa (ativando rolagem horizontal) em vez de espremer/cortar colunas. Balancete/D.R.E., que já cabiam sem aperto, não mudaram de aparência. **Bug real, reportado pelo usuário testando a mudança acima**: na aba Análise Vertical, a coluna "Observação" (e o ícone dentro dela) aparecia cortada — não dava nem pra ler "Observação" por completo, nem pra ver o botão. Causa: `.dcr-page` (o container do relatório) tem `max-width:1140px`, e a tabela de Análise Vertical pode ter bem mais colunas que Balancete/D.R.E. (Descrição + 2 por mês + Observação, normalmente 8 no total pra 3 meses) — com `.dcr-tabela-wrap { overflow: hidden }`, o navegador espremia cada coluna até quebrar o texto do cabeçalho em 2-3 linhas e cortar a última coluna pra fora da área visível. Corrigido trocando pra `overflow-x: auto` (mantendo `overflow-y: hidden`, mesmo comportamento vertical de sempre) e `white-space: nowrap` em `table.dcr-tabela th` — agora a tabela cresce além do container quando precisa (ativando rolagem horizontal) em vez de espremer/cortar colunas. Balancete/D.R.E., que já cabiam sem aperto, não mudaram de aparência.
**Pedido explícito do usuário, ainda na mesma rodada**: a rolagem horizontal resolvia o corte, mas o usuário preferiu que o texto diminuísse o bastante pra caber tudo sem precisar arrastar a tabela, pelo menos no caso comum (3 meses). Nova classe `dcr-tabela--compacta`, só na tabela de Análise Vertical (Balancete/D.R.E. mantidos do tamanho original — já cabiam sem aperto): padding menor (`9px 14px` → `5px 7px`), fonte menor (corpo `0.85rem` → `0.74rem`, cabeçalho `0.72rem` → `0.6rem`, `letter-spacing` reduzido) e os ícones de dentro da tabela (toggle de expandir, "Limpar formatação", observação) encolhidos proporcionalmente. Novo filtro de template `mes_curto` (`contabil_extras.py`) corta o ano do mês pra 2 dígitos (`"mai/2026"` → `"mai/26"`) só no cabeçalho dessa tabela — o texto repetido "— Valor"/"— Variação" em cada uma das colunas por mês era o maior consumidor de largura. `overflow-x: auto` do ajuste anterior continua como rede de segurança (uma apuração com mais de 3 meses ainda pode precisar rolar), mas o caso comum (3 meses) passa a caber inteiro sem rolagem nenhuma. Detalhe completo no `CLAUDE.md` desta pasta. **Pedido explícito do usuário, ainda na mesma rodada**: a rolagem horizontal resolvia o corte, mas o usuário preferiu que o texto diminuísse o bastante pra caber tudo sem precisar arrastar a tabela, pelo menos no caso comum (3 meses). Nova classe `dcr-tabela--compacta`, só na tabela de Análise Vertical (Balancete/D.R.E. mantidos do tamanho original — já cabiam sem aperto): padding menor (`9px 14px` → `5px 7px`), fonte menor (corpo `0.85rem` → `0.74rem`, cabeçalho `0.72rem` → `0.6rem`, `letter-spacing` reduzido) e os ícones de dentro da tabela (toggle de expandir, "Limpar formatação", observação) encolhidos proporcionalmente. Novo filtro de template `mes_curto` (`contabil_extras.py`) corta o ano do mês pra 2 dígitos (`"mai/2026"` → `"mai/26"`) só no cabeçalho dessa tabela — o texto repetido "— Valor"/"— Variação" em cada uma das colunas por mês era o maior consumidor de largura. `overflow-x: auto` do ajuste anterior continua como rede de segurança (uma apuração com mais de 3 meses ainda pode precisar rolar), mas o caso comum (3 meses) passa a caber inteiro sem rolagem nenhuma. Detalhe completo no `CLAUDE.md` desta pasta.
### 135. Bug real: observação "vazando" entre contas com a mesma classificação + ícone da conta "mãe" destacado
Usuário reportou, com print de um balancete real: 6 bancos diferentes (Banco do Brasil, Inter, Itaú, Mercado Pago, PagSeguro, Sicredi) todos sob o mesmo código de classificação "1.01.01.002.001" ("Depósitos Bancários à Vista") — uma observação escrita num deles aparecia em todos os outros. Causa raiz: `ContabilObservacao.chave_conta()` (chave natural do histórico de observações, ver rodada 126) usava só o `codigo` de classificação, que o Questor reaproveita entre várias contas analíticas de mesma natureza — não é único dentro de uma apuração. Corrigido trocando a chave pra `"codigo|descricao"` (models.py, `dashboard-contabil.js`) — mesmo espírito de `chave_linha()` (DRE/Análise Vertical, que já usa `"descricao|nivel"`). `_contabil_sincroniza_contas()` (views.py, reprocessamento) também passou a casar contas por `(codigo, descricao)` em vez de só `codigo`, mesmo trade-off que a sincronização de DRE/Análise Vertical já aceitava (conta renomeada = conta "nova", a antiga é excluída). Migração de dados `0073` recalculou o `alvo_chave` das observações já em produção (2 de conta) a partir do próprio `alvo_rotulo` (a descrição já congelada em cada registro no momento em que foi escrita) — não precisou reconstruir nada a partir da apuração de origem.
Segundo pedido, mesma rodada: quando uma conta/linha "filha" tem observação e o grupo está recolhido, o contador não tinha como saber sem expandir. O ícone de observação da sintética "mãe" agora ganha um destaque (`.dc-conta-observacao-btn--descendente`, cor `--gold`) sempre que algum descendente (não só filho direto) tem observação vigente e a própria sintética não tem observação própria (`dcTemObservacaoDescendente()`, nova, reaproveita `dcDescendentes()` já usada pelo tri-state do botão "validado") — nas três árvores (Balancete/D.R.E./Análise Vertical). Detalhe completo no `CLAUDE.md` desta pasta.
### 136. Coluna "Conta" da revisão mostrava a Classificação, não o número da conta
Usuário comparou com o PDF original (que tem as duas colunas, "Conta" e "S Classificação") e reportou que a aba Balancete da tela de revisão só mostrava a Classificação (`codigo`) numa coluna rotulada "Conta" — o número interno da conta no Questor (`conta_numero`, já extraído e salvo desde sempre, só nunca exibido) não aparecia em lugar nenhum. Confirmado com o usuário que o pedido era só pra tela de revisão (não pro relatório "Gerar Dashboard" que vai pro cliente). `dashboard-contabil.html` ganhou uma coluna nova ("Conta", `conta_numero`) antes da já existente, renomeada pra "Classificação" (`codigo`); `renderContas()` (`dashboard-contabil.js`) passou a emitir as duas células. Ajustes de acompanhamento: colspan do editor inline de observação (7 → 8) e os seletores CSS que dependiam de posição de coluna (`.dc-contas-table td:nth-child(...)`, alinhamento numérico das colunas de valor e o estilo apagado/`nowrap` da(s) coluna(s) de identificação da conta) deslocados em uma posição.

View File

@ -395,7 +395,9 @@ Três decisões de escopo confirmadas por `AskUserQuestion` **antes** de impleme
**Model `ContabilObservacao`** (`portal_api/models.py`, migração `0071`): escopo `codigo_empresa` + `alvo_tipo` (`conta`/`dre`/`analise_vertical`) + `alvo_chave`, mais `alvo_rotulo` (descrição no momento em que foi escrita, só pra exibir se aquela conta sumir do plano), `apuracao_origem` (`SET_NULL`) + `competencia_origem` (cópia, pra vigência continuar resolvendo se a apuração for excluída), `texto`, `mostrar_ao_cliente` (substitui `oculta_no_relatorio`, com o sinal invertido pra bater com o rótulo que o contador vê), `criado_por`/`criado_em` e o trio `encerrada_em_competencia`/`encerrada_por`/`encerrada_em`. **Model `ContabilObservacao`** (`portal_api/models.py`, migração `0071`): escopo `codigo_empresa` + `alvo_tipo` (`conta`/`dre`/`analise_vertical`) + `alvo_chave`, mais `alvo_rotulo` (descrição no momento em que foi escrita, só pra exibir se aquela conta sumir do plano), `apuracao_origem` (`SET_NULL`) + `competencia_origem` (cópia, pra vigência continuar resolvendo se a apuração for excluída), `texto`, `mostrar_ao_cliente` (substitui `oculta_no_relatorio`, com o sinal invertido pra bater com o rótulo que o contador vê), `criado_por`/`criado_em` e o trio `encerrada_em_competencia`/`encerrada_por`/`encerrada_em`.
**Chave natural, nunca FK pra linha**: `codigo` no Balancete, `"descricao|nivel"` na DRE/Análise Vertical (`chave_conta()`/`chave_linha()` no model, `_contabil_chave_alvo()` na view) — exatamente as chaves que `_contabil_sincroniza_*()` já usa no reprocessamento e que `regras.py` usa no histórico de variação. É o que faz a observação seguir a mesma conta de uma competência pra outra, e de brinde tira qualquer risco do reprocessamento sobre ela (antes era preciso garantir explicitamente que `observacao` não fosse tocada; agora ela nem mora lá). **Chave natural, nunca FK pra linha**: `"codigo|descricao"` no Balancete, `"descricao|nivel"` na DRE/Análise Vertical (`chave_conta()`/`chave_linha()` no model, `_contabil_chave_alvo()` na view) — exatamente as chaves que `_contabil_sincroniza_*()` já usa no reprocessamento e que `regras.py` usa no histórico de variação. É o que faz a observação seguir a mesma conta de uma competência pra outra, e de brinde tira qualquer risco do reprocessamento sobre ela (antes era preciso garantir explicitamente que `observacao` não fosse tocada; agora ela nem mora lá).
**Bug real (rodada seguinte) — observação "vazando" pra contas irmãs com a mesma classificação**: a chave do Balancete nasceu só `codigo` (sem `descricao`) — funcionava contra os balancetes de referência usados até então, mas o Questor reaproveita o mesmo código de classificação pra várias contas analíticas de mesma natureza (confirmado pelo usuário: 6 bancos diferentes — Banco do Brasil, Inter, Itaú, Mercado Pago, PagSeguro, Sicredi — todos sob o mesmo código de "Depósitos Bancários à Vista"). Como a chave não distinguia entre eles, uma observação escrita num banco aparecia em todos os outros com o mesmo código. Corrigido trocando a chave pra `"codigo|descricao"` (`ContabilObservacao.chave_conta(codigo, descricao)`, `dcChaveObsConta()` em `dashboard-contabil.js` — mesmo formato dos dois lados) e `_contabil_sincroniza_contas()` (views.py) pra casar contas por `(codigo, descricao)` em vez de só `codigo` no reprocessamento (mesmo trade-off que `_contabil_sincroniza_linhas_dre()` já aceitava: uma conta renomeada, mesmo código, vira uma conta "nova" — a antiga é excluída e outra é criada, em vez de atualizada no lugar). Migração de dados `0073` recalcula o `alvo_chave` de toda `ContabilObservacao` já gravada (`alvo_tipo="conta"`) a partir do próprio `alvo_rotulo` (a descrição da conta congelada no momento em que a observação foi escrita, já armazenada em cada registro) — não precisou reconstruir nada a partir da apuração de origem.
**Vigência** (`vigentes_para()`/`vigente_em()`): aparece em toda apuração da mesma empresa com `competencia_origem <= C` e (`encerrada_em_competencia` nulo ou `>= C`). Daí saem os três caminhos pedidos: manter é não fazer nada; encerrar grava a competência aberta (a observação **continua visível nela** e some da seguinte em diante, o histórico nunca é reescrito); incluir uma nova cria outro registro, então uma conta passa a ter uma thread, não um texto único. **Vigência** (`vigentes_para()`/`vigente_em()`): aparece em toda apuração da mesma empresa com `competencia_origem <= C` e (`encerrada_em_competencia` nulo ou `>= C`). Daí saem os três caminhos pedidos: manter é não fazer nada; encerrar grava a competência aberta (a observação **continua visível nela** e some da seguinte em diante, o histórico nunca é reescrito); incluir uma nova cria outro registro, então uma conta passa a ter uma thread, não um texto único.
@ -426,6 +428,10 @@ Validado de duas formas: (1) ponta a ponta com `Client.force_login()` em transa
**Frontend** (`dashboard-contabil.js`): as observações são carregadas à parte da apuração (`dcCarregarObservacoes()`, chamada ao abrir/criar uma análise) e indexadas por chave natural (`dcObsIndice`), porque não pertencem ao payload da apuração. O editor inline virou uma thread (`dcObsPainelHtml()`/`dcObsItemHtml()`): histórico em cima (autor, data, competência de origem, selos "Histórico"/"Encerrada"/"Editada"/"Aparece ao cliente"/"Interna" e ações de olho, ver histórico de edições (só quando `editada`), editar, encerrar/reativar), campo de observação nova embaixo. **Sem botão de excluir** (removido numa rodada seguinte, pedido explícito do usuário) — a exclusão de uma observação vigente fica limitada ao fluxo de "ocultar das próximas competências" (`encerrar`), nunca um apagar definitivo pela tela; o `DELETE` do backend (`ContabilObservacaoViewSet`, ver "Imutabilidade"/"Endpoints" acima) continua existindo e reachável via API, só não tem mais consumidor no frontend. Um handler único (`dcTrataCliqueObservacao()`) atende as três tabelas e as quatro listas de resumo, e cada mutação refaz o fetch e re-renderiza tudo (`dcRenderObservacoesTudo()`) — a mesma observação pode estar visível em mais de um lugar ao mesmo tempo. O botão da coluna "Observação" ganhou um contador (`.dc-obs-contador`), já que uma conta pode ter várias. Os chips "Todas / Visíveis ao cliente / Internas" (`dcObsFiltro`) existem nas quatro listas e compartilham a mesma variável: filtrar numa aba filtra em todas. **Frontend** (`dashboard-contabil.js`): as observações são carregadas à parte da apuração (`dcCarregarObservacoes()`, chamada ao abrir/criar uma análise) e indexadas por chave natural (`dcObsIndice`), porque não pertencem ao payload da apuração. O editor inline virou uma thread (`dcObsPainelHtml()`/`dcObsItemHtml()`): histórico em cima (autor, data, competência de origem, selos "Histórico"/"Encerrada"/"Editada"/"Aparece ao cliente"/"Interna" e ações de olho, ver histórico de edições (só quando `editada`), editar, encerrar/reativar), campo de observação nova embaixo. **Sem botão de excluir** (removido numa rodada seguinte, pedido explícito do usuário) — a exclusão de uma observação vigente fica limitada ao fluxo de "ocultar das próximas competências" (`encerrar`), nunca um apagar definitivo pela tela; o `DELETE` do backend (`ContabilObservacaoViewSet`, ver "Imutabilidade"/"Endpoints" acima) continua existindo e reachável via API, só não tem mais consumidor no frontend. Um handler único (`dcTrataCliqueObservacao()`) atende as três tabelas e as quatro listas de resumo, e cada mutação refaz o fetch e re-renderiza tudo (`dcRenderObservacoesTudo()`) — a mesma observação pode estar visível em mais de um lugar ao mesmo tempo. O botão da coluna "Observação" ganhou um contador (`.dc-obs-contador`), já que uma conta pode ter várias. Os chips "Todas / Visíveis ao cliente / Internas" (`dcObsFiltro`) existem nas quatro listas e compartilham a mesma variável: filtrar numa aba filtra em todas.
**Ícone de observação da conta/linha "mãe" também se destaca quando um descendente recolhido tem observação** (mesma rodada do bug acima, pedido explícito do usuário: "caso esteja recolhida, o usuário consegue visualizar se há ou não observações realizadas"): `dcTemObservacaoDescendente(tipo, itens, nivelFn, chaveFn, id)` (nova, reaproveita `dcDescendentes()` — todos os descendentes, não só os filhos diretos) roda pra toda conta/linha sintética (`temFilhos[i]`) nas três árvores (`renderContas()`/`renderDre()`/`renderAnaliseVertical()`) e é passada como quarto argumento de `dcObsBotaoHtml()`. O ícone da própria sintética só ganha o destaque quando ela mesma **não** tem observação própria (senão prevalece o `--preenchida` de sempre) — nova classe `.dc-conta-observacao-btn--descendente` (`dashboard-contabil.css`), cor `--gold` (mesmo tom do estado "parcial" do botão "validado", pra reaproveitar um significado visual já existente de "tem algo pendente de atenção neste grupo" sem inventar uma cor nova). Clicar no ícone continua abrindo o painel da própria conta/linha (que nasce vazio nesse caso) — é só um sinal visual de "tem observação em algum lugar dentro deste grupo recolhido", não um atalho pra ela.
**Coluna "Conta" (rodada seguinte)**: a aba Balancete da tela de revisão mostrava só a Classificação (`codigo`) numa coluna rotulada "Conta" — o `conta_numero` (numeração interna do Questor, já extraído pelo parser e salvo em `ContabilConta` desde sempre, ver "Models" acima) nunca tinha coluna própria, diferente do PDF original (que traz as duas: "Conta" e "S Classificação"). `dashboard-contabil.html` ganhou uma coluna nova antes da existente — hoje a tabela do Balancete tem 8 colunas: Conta (`conta_numero`) | Classificação (`codigo`) | Descrição | Saldo Anterior | Débito | Crédito | Saldo Atual | Observação. Escopo confirmado com o usuário: só a tela de revisão, não o relatório "Gerar Dashboard" (que continua só com Classificação — não precisa do número interno do Questor pro administrador da empresa). Como a posição das colunas mudou, os seletores CSS que dependiam de índice (`dashboard-contabil.css`, `.dc-contas-table td:nth-child(...)` — alinhamento numérico das colunas de valor, e o estilo apagado/`nowrap` das colunas de identificação da conta) e o colspan do editor inline de observação (`dcObsPainelHtml("conta", ...)` em `dashboard-contabil.js`, `7` → `8`) precisaram ser ajustados junto. Balancete é a única tabela com esse par Conta/Classificação — DRE e Análise Vertical não têm código de classificação nenhum (ver "Extração do PDF" acima), então não são afetadas.
**Histórico de edições de texto** (`ContabilObservacaoEdicao`, migração `0072`, mesma rodada da remoção do botão de excluir): pedido explícito do usuário — sem a opção de excluir, uma edição de texto precisava deixar rastro visível, pra não virar uma forma indireta de "apagar" uma observação importante reescrevendo por cima. Um registro por `PATCH` que muda `texto` de fato (`texto_novo != observacao.texto` antes de salvar, em `ContabilObservacaoViewSet.partial_update()`) — nunca por `mostrar_ao_cliente`/`encerrar`/`reativar`, que não tocam o conteúdo, e nunca quando o texto enviado é igual ao já salvo. `ContabilObservacaoSerializer` ganhou `editada` (`bool(obj.edicoes.all())`) e `edicoes` (lista aninhada, mais recente primeiro no frontend) — os dois só existem nesse serializer (usado pela tela), o relatório HTML pro cliente (`dashboard()`) não os usa, então o selo/histórico nunca aparece lá. `ContabilApuracaoViewSet.observacoes()` ganhou `.prefetch_related("edicoes__editado_por")` pra não gerar uma query por observação. Frontend: selo `.dc-obs-selo--editada` ("Editada") ao lado dos demais, e um botão de relógio (`data-dc-obs-historico`, `PID_DC_ICON_HISTORICO`) que abre `#dc-obs-historico-modal` (`pidDcAbrirHistoricoObservacao()`) — lista texto anterior (riscado) → texto novo, autor e data de cada edição; usa `obs.edicoes` já carregado junto da observação, sem chamada de API própria. Validado ponta a ponta via `Client.force_login()` dentro de uma transação com rollback forçado: criação sem edição (`editada=False`), primeira edição real de texto cria o registro e vira `editada=True`, reenviar o mesmo texto não duplica, alternar `mostrar_ao_cliente` não gera edição, e uma segunda edição real acumula um segundo registro — nada gravado em produção. **Histórico de edições de texto** (`ContabilObservacaoEdicao`, migração `0072`, mesma rodada da remoção do botão de excluir): pedido explícito do usuário — sem a opção de excluir, uma edição de texto precisava deixar rastro visível, pra não virar uma forma indireta de "apagar" uma observação importante reescrevendo por cima. Um registro por `PATCH` que muda `texto` de fato (`texto_novo != observacao.texto` antes de salvar, em `ContabilObservacaoViewSet.partial_update()`) — nunca por `mostrar_ao_cliente`/`encerrar`/`reativar`, que não tocam o conteúdo, e nunca quando o texto enviado é igual ao já salvo. `ContabilObservacaoSerializer` ganhou `editada` (`bool(obj.edicoes.all())`) e `edicoes` (lista aninhada, mais recente primeiro no frontend) — os dois só existem nesse serializer (usado pela tela), o relatório HTML pro cliente (`dashboard()`) não os usa, então o selo/histórico nunca aparece lá. `ContabilApuracaoViewSet.observacoes()` ganhou `.prefetch_related("edicoes__editado_por")` pra não gerar uma query por observação. Frontend: selo `.dc-obs-selo--editada` ("Editada") ao lado dos demais, e um botão de relógio (`data-dc-obs-historico`, `PID_DC_ICON_HISTORICO`) que abre `#dc-obs-historico-modal` (`pidDcAbrirHistoricoObservacao()`) — lista texto anterior (riscado) → texto novo, autor e data de cada edição; usa `obs.edicoes` já carregado junto da observação, sem chamada de API própria. Validado ponta a ponta via `Client.force_login()` dentro de uma transação com rollback forçado: criação sem edição (`editada=False`), primeira edição real de texto cria o registro e vira `editada=True`, reenviar o mesmo texto não duplica, alternar `mostrar_ao_cliente` não gera edição, e uma segunda edição real acumula um segundo registro — nada gravado em produção.
**Migração de dados**: a `0071` cria o model, copia cada observação preenchida das três tabelas (autor e data vêm da apuração, a melhor aproximação disponível — o modelo antigo não guardava nada disso por observação; `oculta_no_relatorio` vira `mostrar_ao_cliente` invertido) e só então remove os seis campos antigos. Em produção eram 3 observações (2 de conta, 1 de DRE), todas migradas e conferidas depois de aplicar. **Migração de dados**: a `0071` cria o model, copia cada observação preenchida das três tabelas (autor e data vêm da apuração, a melhor aproximação disponível — o modelo antigo não guardava nada disso por observação; `oculta_no_relatorio` vira `mostrar_ao_cliente` invertido) e só então remove os seis campos antigos. Em produção eram 3 observações (2 de conta, 1 de DRE), todas migradas e conferidas depois de aplicar.

View File

@ -0,0 +1,45 @@
# Generated manually on 2026-09-14
#
# Bug real: `ContabilObservacao.chave_conta()` casava uma conta do Balancete
# só pelo `codigo` de classificação — mas o Questor reaproveita o mesmo
# código pra várias contas analíticas de mesma natureza (ex.: bancos
# diferentes, todos sob o código de "Depósitos Bancários à Vista"),
# confirmado contra um balancete real com 6 bancos distintos sob o mesmo
# código. Efeito: uma observação escrita numa conta aparecia em TODAS as
# outras que compartilhavam a classificação. A chave passou a ser o par
# `(codigo, descricao)` (ver models.py) — esta migração recalcula
# `alvo_chave` de toda `ContabilObservacao` já gravada (`alvo_tipo="conta"`)
# a partir do próprio `alvo_rotulo` (a descrição da conta no momento em que a
# observação foi escrita, já armazenada em cada registro), sem precisar
# reconstruir nada a partir da apuração de origem.
from django.db import migrations
def recalcula_alvo_chave(apps, schema_editor):
ContabilObservacao = apps.get_model("portal_api", "ContabilObservacao")
for observacao in ContabilObservacao.objects.filter(alvo_tipo="conta"):
nova_chave = f"{observacao.alvo_chave}|{observacao.alvo_rotulo}"
if nova_chave != observacao.alvo_chave:
observacao.alvo_chave = nova_chave
observacao.save(update_fields=["alvo_chave"])
def reverte_alvo_chave(apps, schema_editor):
ContabilObservacao = apps.get_model("portal_api", "ContabilObservacao")
for observacao in ContabilObservacao.objects.filter(alvo_tipo="conta"):
codigo = observacao.alvo_chave.split("|", 1)[0]
if codigo != observacao.alvo_chave:
observacao.alvo_chave = codigo
observacao.save(update_fields=["alvo_chave"])
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0072_contabilobservacaoedicao'),
]
operations = [
migrations.RunPython(recalcula_alvo_chave, reverte_alvo_chave),
]

View File

@ -2246,9 +2246,10 @@ class ContabilObservacao(models.Model):
O alvo é guardado por **chave natural**, nunca por FK a uma linha de uma O alvo é guardado por **chave natural**, nunca por FK a uma linha de uma
apuração específica (uma linha é recriada/ressincronizada a cada apuração específica (uma linha é recriada/ressincronizada a cada
apuração/reprocessamento): `codigo` de classificação no Balancete e apuração/reprocessamento): `"codigo|descricao"` no Balancete (ver
`"descricao|nivel"` na DRE/Análise Vertical — as mesmas chaves já usadas `chave_conta()` — `codigo` de classificação sozinho não é único, ver
por `_contabil_sincroniza_*()` em views.py e pelo histórico de variação abaixo) e `"descricao|nivel"` na DRE/Análise Vertical — as mesmas chaves
já usadas por `_contabil_sincroniza_*()` em views.py e pelo histórico de variação
em `regras.py`. Efeito colateral bem-vindo: reprocessar uma apuração não em `regras.py`. Efeito colateral bem-vindo: reprocessar uma apuração não
toca em observação nenhuma, já que elas não moram mais na linha que é toca em observação nenhuma, já que elas não moram mais na linha que é
resincronizada. resincronizada.
@ -2326,10 +2327,18 @@ class ContabilObservacao(models.Model):
return f"{self.codigo_empresa} {self.alvo_chave} ({self.competencia_origem:%m/%Y})" return f"{self.codigo_empresa} {self.alvo_chave} ({self.competencia_origem:%m/%Y})"
@staticmethod @staticmethod
def chave_conta(codigo: str) -> str: def chave_conta(codigo: str, descricao: str) -> str:
"""Chave natural de uma conta do Balancete — mesmo critério de """Chave natural de uma conta do Balancete — o par `(codigo, descricao)`,
`_contabil_sincroniza_contas()` (casa por `codigo` de classificação).""" mesmo critério de `_contabil_sincroniza_contas()`. `codigo` sozinho
return codigo **não** é único: o Questor reaproveita a mesma classificação pra
várias contas analíticas de mesma natureza (ex. bancos diferentes,
todos sob o código de "Depósitos Bancários à Vista" — confirmado
contra um balancete real com 6 bancos distintos sob o mesmo código),
então usar só `codigo` fazia uma observação escrita numa conta
"vazar" pra todas as outras que compartilham a classificação.
`descricao` desambigua, mesmo espírito de `chave_linha()` pra
DRE/Análise Vertical."""
return f"{codigo}|{descricao}"
@staticmethod @staticmethod
def chave_linha(descricao: str, nivel: int) -> str: def chave_linha(descricao: str, nivel: int) -> str:

View File

@ -3773,11 +3773,12 @@ def _contabil_arvore_contexto(
def _contabil_chave_alvo(alvo_tipo: str, alvo: Any) -> str: def _contabil_chave_alvo(alvo_tipo: str, alvo: Any) -> str:
"""Chave natural de uma conta/linha pro histórico de observações — as """Chave natural de uma conta/linha pro histórico de observações — as
mesmas usadas por `_contabil_sincroniza_*()` no reprocessamento (`codigo` mesmas usadas por `_contabil_sincroniza_*()` no reprocessamento
no Balancete, `(descricao, nivel)` na DRE/Análise Vertical), pra uma (`(codigo, descricao)` no Balancete, `(descricao, nivel)` na DRE/Análise
observação seguir a mesma conta de uma competência pra outra.""" Vertical), pra uma observação seguir a mesma conta de uma competência pra
outra."""
if alvo_tipo == ContabilObservacao.ALVO_CONTA: if alvo_tipo == ContabilObservacao.ALVO_CONTA:
return ContabilObservacao.chave_conta(alvo.codigo) return ContabilObservacao.chave_conta(alvo.codigo, alvo.descricao)
return ContabilObservacao.chave_linha(alvo.descricao, alvo.nivel) return ContabilObservacao.chave_linha(alvo.descricao, alvo.nivel)
@ -3801,28 +3802,33 @@ def _contabil_sincroniza_contas(
apuracao: ContabilApuracao, contas_extraidas: list[dashboard_contabil_modelos.LinhaBalanceteExtraida] apuracao: ContabilApuracao, contas_extraidas: list[dashboard_contabil_modelos.LinhaBalanceteExtraida]
) -> None: ) -> None:
"""Resincroniza `ContabilConta` a partir de um reprocessamento """Resincroniza `ContabilConta` a partir de um reprocessamento
(`ContabilApuracaoViewSet.reprocessar()`) — casa pelo `codigo` de (`ContabilApuracaoViewSet.reprocessar()`) — casa pelo par
classificação (chave natural já usada pelo histórico de variação em `(codigo, descricao)` (chave natural já usada pelo histórico de
`regras.py`) e atualiza os registros **no lugar** (mesmo `id`), pra observações, ver `ContabilObservacao.chave_conta()`; `codigo` sozinho
não é único, várias contas analíticas podem compartilhar a mesma
classificação) e atualiza os registros **no lugar** (mesmo `id`), pra
achados que referenciam essas contas nunca perderem a FK. Conta sem achados que referenciam essas contas nunca perderem a FK. Conta sem
mudança real mantém `validado`/`alterada_reprocessamento` como estavam; mudança real mantém `validado`/`alterada_reprocessamento` como estavam;
conta com algum campo divergente da versão anterior volta pra conta com algum campo divergente da versão anterior volta pra
`validado=False` e `alterada_reprocessamento=True`, guardando o `validado=False` e `alterada_reprocessamento=True`, guardando o
`saldo_atual` de antes em `valor_anterior_reprocessamento` (só pro `saldo_atual` de antes em `valor_anterior_reprocessamento` (só pro
tooltip do badge no frontend). Observação nunca é afetada por aqui: ela tooltip do badge no frontend). Uma conta renomeada (mesmo código, outra
não mora mais na linha, e sim em `ContabilObservacao` (histórico por descrição) é tratada como uma conta diferente — a antiga é excluída e uma
nova é criada, mesmo trade-off que `_contabil_sincroniza_linhas_dre()`
já aceita pra `(descricao, nivel)`. Observação nunca é afetada por aqui:
ela não mora mais na linha, e sim em `ContabilObservacao` (histórico por
empresa+conta).""" empresa+conta)."""
antigas = {conta.codigo: conta for conta in apuracao.contas.all()} antigas = {(conta.codigo, conta.descricao): conta for conta in apuracao.contas.all()}
vistos: set[str] = set() vistos: set[tuple[str, str]] = set()
for indice, extraida in enumerate(contas_extraidas): for indice, extraida in enumerate(contas_extraidas):
tipo = extraida.tipo or "A" tipo = extraida.tipo or "A"
antiga = antigas.get(extraida.codigo) chave = (extraida.codigo, extraida.descricao)
antiga = antigas.get(chave)
if antiga is not None: if antiga is not None:
vistos.add(extraida.codigo) vistos.add(chave)
alterou = ( alterou = (
antiga.descricao != extraida.descricao antiga.tipo != tipo
or antiga.tipo != tipo
or antiga.saldo_anterior != extraida.saldo_anterior or antiga.saldo_anterior != extraida.saldo_anterior
or antiga.debito != extraida.debito or antiga.debito != extraida.debito
or antiga.credito != extraida.credito or antiga.credito != extraida.credito
@ -3831,7 +3837,6 @@ def _contabil_sincroniza_contas(
antiga.valor_anterior_reprocessamento = antiga.saldo_atual if alterou else None antiga.valor_anterior_reprocessamento = antiga.saldo_atual if alterou else None
antiga.ordem = indice antiga.ordem = indice
antiga.conta_numero = extraida.conta_numero antiga.conta_numero = extraida.conta_numero
antiga.descricao = extraida.descricao
antiga.tipo = tipo antiga.tipo = tipo
antiga.saldo_anterior = extraida.saldo_anterior antiga.saldo_anterior = extraida.saldo_anterior
antiga.debito = extraida.debito antiga.debito = extraida.debito
@ -3854,8 +3859,8 @@ def _contabil_sincroniza_contas(
saldo_atual=extraida.saldo_atual, saldo_atual=extraida.saldo_atual,
) )
for codigo, antiga in antigas.items(): for chave, antiga in antigas.items():
if codigo not in vistos: if chave not in vistos:
antiga.delete() antiga.delete()
@ -4322,7 +4327,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
observacao.ancora = f"{prefixo}-{alvo_id}" if alvo_id is not None else None observacao.ancora = f"{prefixo}-{alvo_id}" if alvo_id is not None else None
return observacoes return observacoes
mapa_conta_por_chave = {ContabilObservacao.chave_conta(c.codigo): c.id for c in contas} mapa_conta_por_chave = {ContabilObservacao.chave_conta(c.codigo, c.descricao): c.id for c in contas}
mapa_dre_por_chave = {ContabilObservacao.chave_linha(l.descricao, l.nivel): l.id for l in linhas_dre} mapa_dre_por_chave = {ContabilObservacao.chave_linha(l.descricao, l.nivel): l.id for l in linhas_dre}
mapa_av_por_chave = { mapa_av_por_chave = {
ContabilObservacao.chave_linha(l.descricao, l.nivel): l.id for l in linhas_analise_vertical ContabilObservacao.chave_linha(l.descricao, l.nivel): l.id for l in linhas_analise_vertical
@ -4337,7 +4342,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
lambda c: c.codigo.count("."), lambda c: c.codigo.count("."),
18, 18,
observacoes_por_tipo[ContabilObservacao.ALVO_CONTA], observacoes_por_tipo[ContabilObservacao.ALVO_CONTA],
lambda c: ContabilObservacao.chave_conta(c.codigo), lambda c: ContabilObservacao.chave_conta(c.codigo, c.descricao),
), ),
"linhas_dre": _contabil_arvore_contexto( "linhas_dre": _contabil_arvore_contexto(
linhas_dre, linhas_dre,

View File

@ -554,7 +554,7 @@
/* Tabelas de Balancete/DRE — reaproveitam .pa-table, só ajustes de alinhamento /* Tabelas de Balancete/DRE — reaproveitam .pa-table, só ajustes de alinhamento
numérico e da célula de observação (que embute um botão). */ numérico e da célula de observação (que embute um botão). */
.dc-contas-table td:nth-child(n + 3):nth-child(-n + 6), .dc-contas-table td:nth-child(n + 4):nth-child(-n + 7),
.dc-dre-table td:nth-child(2) { .dc-dre-table td:nth-child(2) {
text-align: right; text-align: right;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
@ -570,7 +570,7 @@
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.dc-contas-table td:nth-child(1) { .dc-contas-table td:nth-child(-n + 2) {
white-space: nowrap; white-space: nowrap;
color: var(--text-muted); color: var(--text-muted);
font-size: 0.82rem; font-size: 0.82rem;
@ -706,6 +706,15 @@
color: var(--accent); color: var(--accent);
} }
/* Sintética recolhida sem observação própria, mas com observação em algum
descendente (pedido explícito do usuário: dá pra perceber isso sem
precisar expandir o grupo) — cor `--gold`, mesmo tom do estado "parcial"
do botão "validado" (`.dc-conta-validado-btn--parcial` abaixo), pra não
confundir com `--accent` (observação na própria linha). */
.dc-conta-observacao-btn--descendente {
color: var(--gold);
}
/* Coluna "Observação" agora tem 2 botões lado a lado (validado + observação) /* 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* — 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 do `<td>`, nunca no próprio `<td>` — um `<td>` com `display:flex` deixa de
@ -874,8 +883,8 @@
então ele é ao mesmo tempo "primeiro" e "último" filho pro seletor CSS — então ele é ao mesmo tempo "primeiro" e "último" filho pro seletor CSS —
herdaria `text-align:right` de `.pa-table td:last-child` (bug de herdaria `text-align:right` de `.pa-table td:last-child` (bug de
alinhamento já visto no painel de detalhe de outras telas) E a cor alinhamento já visto no painel de detalhe de outras telas) E a cor
apagada de `.dc-contas-table td:nth-child(1)` (pensada pra coluna do apagada de `.dc-contas-table td:nth-child(-n + 2)` (pensada pras colunas
código da conta, `--text-muted`), deixando o texto da thread de de conta/classificação, `--text-muted`), deixando o texto da thread de
observações "apagado" mesmo já sem cor própria nele. Precisa do seletor observações "apagado" mesmo já sem cor própria nele. Precisa do seletor
com 2 classes (não só `.dc-obs-edit-row td`) pra empatar/ganhar em com 2 classes (não só `.dc-obs-edit-row td`) pra empatar/ganhar em
especificidade das duas regras acima (a versão de 1 classe perdia); força especificidade das duas regras acima (a versão de 1 classe perdia); força

View File

@ -1050,6 +1050,16 @@ document.addEventListener("DOMContentLoaded", async () => {
return descendentes; return descendentes;
} }
// Verdadeiro se algum descendente (filho, neto...) de uma conta/linha
// sintética tem observação vigente — usado pra destacar o ícone de
// observação de um grupo recolhido mesmo quando a própria sintética não
// tem observação própria (pedido explícito do usuário: "caso esteja
// recolhida, o usuário consegue visualizar se há ou não observações
// realizadas" nas contas/linhas dentro dela).
function dcTemObservacaoDescendente(tipo, itens, nivelFn, chaveFn, id) {
return dcDescendentes(itens, nivelFn, id).some((item) => dcObservacoesDe(tipo, chaveFn(item)).length > 0);
}
// Estado do botão "validado" de uma conta/linha COM filhos (sintética) — // Estado do botão "validado" de uma conta/linha COM filhos (sintética) —
// pedido explícito do usuário: "nenhum" (cor padrão) quando nem ela nem // pedido explícito do usuário: "nenhum" (cor padrão) quando nem ela nem
// nenhum descendente está validado; "completo" (verde, mesma cor de uma // nenhum descendente está validado; "completo" (verde, mesma cor de uma
@ -1377,8 +1387,14 @@ document.addEventListener("DOMContentLoaded", async () => {
// da aba "Dashboard" (os resumos por aba não precisam, já são de um tipo). // da aba "Dashboard" (os resumos por aba não precisam, já são de um tipo).
const PID_DC_OBS_ORIGENS = { conta: "Balancete", dre: "D.R.E.", analise_vertical: "Análise Vertical" }; const PID_DC_OBS_ORIGENS = { conta: "Balancete", dre: "D.R.E.", analise_vertical: "Análise Vertical" };
// Mesma chave de ContabilObservacao.chave_conta() no backend — o `codigo`
// de classificação sozinho não é único (o Questor reaproveita o mesmo
// código pra várias contas analíticas de mesma natureza, ex. bancos
// diferentes sob "Depósitos Bancários à Vista"), então a descrição
// desambigua. Usar só `codigo` aqui fazia uma observação escrita numa
// conta aparecer em todas as outras que compartilhavam a classificação.
function dcChaveObsConta(conta) { function dcChaveObsConta(conta) {
return conta.codigo; return `${conta.codigo}|${conta.descricao}`;
} }
function dcChaveObsLinha(linha) { function dcChaveObsLinha(linha) {
@ -1435,7 +1451,16 @@ document.addEventListener("DOMContentLoaded", async () => {
// vigentes daquela conta/linha (as desta competência mais as herdadas), // vigentes daquela conta/linha (as desta competência mais as herdadas),
// colorido quando há alguma — o texto em si nunca aparece na tabela, só no // colorido quando há alguma — o texto em si nunca aparece na tabela, só no
// painel (pedido antigo do usuário pra não poluir a coluna). // painel (pedido antigo do usuário pra não poluir a coluna).
function dcObsBotaoHtml(tipo, alvoId, observacoes) { //
// `temObsDescendente` (pedido explícito do usuário: "quando é feito o
// comentário em uma das contas filhas deve aparecer ressaltado o ícone na
// conta mãe também, pois caso esteja recolhida, o usuário consegue
// visualizar se há ou não observações realizadas") só importa quando a
// própria conta/linha não tem observação própria — nesse caso o ícone
// ganha um destaque mais discreto (`--descendente`, cor `--gold`, mesmo
// tom do estado "parcial" do botão "validado") em vez do `--preenchida`
// (`--accent`) usado quando a observação é da própria linha.
function dcObsBotaoHtml(tipo, alvoId, observacoes, temObsDescendente) {
const doMes = observacoes.filter((obs) => !obs.historica).length; const doMes = observacoes.filter((obs) => !obs.historica).length;
const herdadas = observacoes.length - doMes; const herdadas = observacoes.length - doMes;
let titulo = "Adicionar observação"; let titulo = "Adicionar observação";
@ -1444,9 +1469,16 @@ document.addEventListener("DOMContentLoaded", async () => {
if (doMes) partes.push(`${doMes} desta competência`); if (doMes) partes.push(`${doMes} desta competência`);
if (herdadas) partes.push(`${herdadas} do histórico`); if (herdadas) partes.push(`${herdadas} do histórico`);
titulo = `${observacoes.length} observação(ões): ${partes.join(", ")}`; titulo = `${observacoes.length} observação(ões): ${partes.join(", ")}`;
} else if (temObsDescendente) {
titulo = "Nenhuma observação nesta conta, mas há observações em contas dentro deste grupo";
} }
const classeDestaque = observacoes.length
? " dc-conta-observacao-btn--preenchida"
: temObsDescendente
? " dc-conta-observacao-btn--descendente"
: "";
const contador = observacoes.length ? `<span class="dc-obs-contador">${observacoes.length}</span>` : ""; const contador = observacoes.length ? `<span class="dc-obs-contador">${observacoes.length}</span>` : "";
return `<button type="button" class="dc-conta-observacao-btn${observacoes.length ? " dc-conta-observacao-btn--preenchida" : ""}" data-dc-obs-abrir="${tipo}:${alvoId}" title="${pidDcEscapeHtml(titulo)}" aria-label="${pidDcEscapeHtml(titulo)}"> return `<button type="button" class="dc-conta-observacao-btn${classeDestaque}" data-dc-obs-abrir="${tipo}:${alvoId}" title="${pidDcEscapeHtml(titulo)}" aria-label="${pidDcEscapeHtml(titulo)}">
${PID_DC_OBSERVACAO_ICONE}${contador} ${PID_DC_OBSERVACAO_ICONE}${contador}
</button>`; </button>`;
} }
@ -1771,11 +1803,14 @@ document.addEventListener("DOMContentLoaded", async () => {
const validadoInfo = dcValidadoInfo(conta, contas, dcContaNivel, temFilhos[i]); const validadoInfo = dcValidadoInfo(conta, contas, dcContaNivel, temFilhos[i]);
const observacoesDaConta = dcObservacoesDe("conta", dcChaveObsConta(conta)); const observacoesDaConta = dcObservacoesDe("conta", dcChaveObsConta(conta));
const temObsDescendente =
temFilhos[i] && dcTemObservacaoDescendente("conta", contas, dcContaNivel, dcChaveObsConta, conta.id);
const tr = document.createElement("tr"); const tr = document.createElement("tr");
if (conta.tipo === "S") tr.classList.add("dc-conta-row--sintetica"); if (conta.tipo === "S") tr.classList.add("dc-conta-row--sintetica");
if (dcContasDestaque.has(conta.id)) tr.classList.add("dc-conta-row--destaque"); if (dcContasDestaque.has(conta.id)) tr.classList.add("dc-conta-row--destaque");
tr.innerHTML = ` tr.innerHTML = `
<td>${pidDcEscapeHtml(conta.conta_numero)}</td>
<td>${pidDcEscapeHtml(conta.codigo)}</td> <td>${pidDcEscapeHtml(conta.codigo)}</td>
<td style="padding-left: calc(var(--space-4) + ${nivel * 18}px)"> <td style="padding-left: calc(var(--space-4) + ${nivel * 18}px)">
<span class="dc-conta-desc-cell">${toggle}<span>${pidDcEscapeHtml(conta.descricao)}</span></span> <span class="dc-conta-desc-cell">${toggle}<span>${pidDcEscapeHtml(conta.descricao)}</span></span>
@ -1789,7 +1824,7 @@ document.addEventListener("DOMContentLoaded", async () => {
<button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-conta-validado="${conta.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}> <button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-conta-validado="${conta.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}>
${PID_DC_VALIDADO_ICONE} ${PID_DC_VALIDADO_ICONE}
</button> </button>
${dcObsBotaoHtml("conta", conta.id, observacoesDaConta)} ${dcObsBotaoHtml("conta", conta.id, observacoesDaConta, temObsDescendente)}
${pidDcAlteradaBadgeHtml( ${pidDcAlteradaBadgeHtml(
conta.alterada_reprocessamento, conta.alterada_reprocessamento,
conta.validado, conta.validado,
@ -1803,7 +1838,7 @@ document.addEventListener("DOMContentLoaded", async () => {
if (dcObsPainelAberto.conta === conta.id) { if (dcObsPainelAberto.conta === conta.id) {
const editRow = document.createElement("tr"); const editRow = document.createElement("tr");
editRow.className = "dc-obs-edit-row"; editRow.className = "dc-obs-edit-row";
editRow.innerHTML = dcObsPainelHtml("conta", conta.id, observacoesDaConta, 7, concluida); editRow.innerHTML = dcObsPainelHtml("conta", conta.id, observacoesDaConta, 8, concluida);
body.appendChild(editRow); body.appendChild(editRow);
} }
}); });
@ -1881,6 +1916,9 @@ document.addEventListener("DOMContentLoaded", async () => {
const validadoInfo = dcValidadoInfo(linha, linhas, (l) => Math.max(0, l.nivel), temFilhos[i]); const validadoInfo = dcValidadoInfo(linha, linhas, (l) => Math.max(0, l.nivel), temFilhos[i]);
const observacoesDaLinha = dcObservacoesDe("dre", dcChaveObsLinha(linha)); const observacoesDaLinha = dcObservacoesDe("dre", dcChaveObsLinha(linha));
const temObsDescendente =
temFilhos[i] &&
dcTemObservacaoDescendente("dre", linhas, (l) => Math.max(0, l.nivel), dcChaveObsLinha, linha.id);
const tr = document.createElement("tr"); const tr = document.createElement("tr");
tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row"; tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row";
@ -1896,7 +1934,7 @@ document.addEventListener("DOMContentLoaded", async () => {
<button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-dre-validado="${linha.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}> <button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-dre-validado="${linha.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}>
${PID_DC_VALIDADO_ICONE} ${PID_DC_VALIDADO_ICONE}
</button> </button>
${dcObsBotaoHtml("dre", linha.id, observacoesDaLinha)} ${dcObsBotaoHtml("dre", linha.id, observacoesDaLinha, temObsDescendente)}
${pidDcAlteradaBadgeHtml( ${pidDcAlteradaBadgeHtml(
linha.alterada_reprocessamento, linha.alterada_reprocessamento,
linha.validado, linha.validado,
@ -2014,6 +2052,8 @@ document.addEventListener("DOMContentLoaded", async () => {
const validadoInfo = dcValidadoInfo(linha, linhas, nivelFn, temFilhos[i]); const validadoInfo = dcValidadoInfo(linha, linhas, nivelFn, temFilhos[i]);
const observacoesDaLinha = dcObservacoesDe("analise_vertical", dcChaveObsLinha(linha)); const observacoesDaLinha = dcObservacoesDe("analise_vertical", dcChaveObsLinha(linha));
const temObsDescendente =
temFilhos[i] && dcTemObservacaoDescendente("analise_vertical", linhas, nivelFn, dcChaveObsLinha, linha.id);
const colunasMensais = linha.valores const colunasMensais = linha.valores
.map( .map(
(v) => (v) =>
@ -2035,7 +2075,7 @@ document.addEventListener("DOMContentLoaded", async () => {
<button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-av-validado="${linha.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}> <button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-av-validado="${linha.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}>
${PID_DC_VALIDADO_ICONE} ${PID_DC_VALIDADO_ICONE}
</button> </button>
${dcObsBotaoHtml("analise_vertical", linha.id, observacoesDaLinha)} ${dcObsBotaoHtml("analise_vertical", linha.id, observacoesDaLinha, temObsDescendente)}
${pidDcAlteradaBadgeHtml(linha.alterada_reprocessamento, linha.validado, pidDcValorAnteriorAvTexto(linha))} ${pidDcAlteradaBadgeHtml(linha.alterada_reprocessamento, linha.validado, pidDcValorAnteriorAvTexto(linha))}
</div> </div>
</td> </td>

View File

@ -450,6 +450,7 @@
<thead id="dc-contas-thead"> <thead id="dc-contas-thead">
<tr> <tr>
<th>Conta</th> <th>Conta</th>
<th>Classificação</th>
<th>Descrição</th> <th>Descrição</th>
<th>Saldo Anterior</th> <th>Saldo Anterior</th>
<th>Débito</th> <th>Débito</th>