diff --git a/portal_api/dashboard_contabil/CHANGELOG.md b/portal_api/dashboard_contabil/CHANGELOG.md index 6ccc19a..3a42e6a 100644 --- a/portal_api/dashboard_contabil/CHANGELOG.md +++ b/portal_api/dashboard_contabil/CHANGELOG.md @@ -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. **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. diff --git a/portal_api/dashboard_contabil/CLAUDE.md b/portal_api/dashboard_contabil/CLAUDE.md index 1b03892..d6d8d53 100644 --- a/portal_api/dashboard_contabil/CLAUDE.md +++ b/portal_api/dashboard_contabil/CLAUDE.md @@ -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`. -**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. @@ -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. +**Í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. **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. diff --git a/portal_api/migrations/0073_contabil_observacao_chave_conta_descricao.py b/portal_api/migrations/0073_contabil_observacao_chave_conta_descricao.py new file mode 100644 index 0000000..95966b2 --- /dev/null +++ b/portal_api/migrations/0073_contabil_observacao_chave_conta_descricao.py @@ -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), + ] diff --git a/portal_api/models.py b/portal_api/models.py index 74a8aae..bdc0fa6 100644 --- a/portal_api/models.py +++ b/portal_api/models.py @@ -2246,9 +2246,10 @@ class ContabilObservacao(models.Model): 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/reprocessamento): `codigo` de classificação no Balancete 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 + apuração/reprocessamento): `"codigo|descricao"` no Balancete (ver + `chave_conta()` — `codigo` de classificação sozinho não é único, ver + 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 toca em observação nenhuma, já que elas não moram mais na linha que é resincronizada. @@ -2326,10 +2327,18 @@ class ContabilObservacao(models.Model): return f"{self.codigo_empresa} {self.alvo_chave} ({self.competencia_origem:%m/%Y})" @staticmethod - def chave_conta(codigo: str) -> str: - """Chave natural de uma conta do Balancete — mesmo critério de - `_contabil_sincroniza_contas()` (casa por `codigo` de classificação).""" - return codigo + def chave_conta(codigo: str, descricao: str) -> str: + """Chave natural de uma conta do Balancete — o par `(codigo, descricao)`, + mesmo critério de `_contabil_sincroniza_contas()`. `codigo` sozinho + **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 def chave_linha(descricao: str, nivel: int) -> str: diff --git a/portal_api/views.py b/portal_api/views.py index 37be899..3ea2a85 100644 --- a/portal_api/views.py +++ b/portal_api/views.py @@ -3773,11 +3773,12 @@ def _contabil_arvore_contexto( def _contabil_chave_alvo(alvo_tipo: str, alvo: Any) -> str: """Chave natural de uma conta/linha pro histórico de observações — as - mesmas usadas por `_contabil_sincroniza_*()` no reprocessamento (`codigo` - no Balancete, `(descricao, nivel)` na DRE/Análise Vertical), pra uma - observação seguir a mesma conta de uma competência pra outra.""" + mesmas usadas por `_contabil_sincroniza_*()` no reprocessamento + (`(codigo, descricao)` no Balancete, `(descricao, nivel)` na DRE/Análise + Vertical), pra uma observação seguir a mesma conta de uma competência pra + outra.""" 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) @@ -3801,28 +3802,33 @@ def _contabil_sincroniza_contas( apuracao: ContabilApuracao, contas_extraidas: list[dashboard_contabil_modelos.LinhaBalanceteExtraida] ) -> None: """Resincroniza `ContabilConta` a partir de um reprocessamento - (`ContabilApuracaoViewSet.reprocessar()`) — casa pelo `codigo` de - classificação (chave natural já usada pelo histórico de variação em - `regras.py`) e atualiza os registros **no lugar** (mesmo `id`), pra + (`ContabilApuracaoViewSet.reprocessar()`) — casa pelo par + `(codigo, descricao)` (chave natural já usada pelo histórico de + 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 mudança real mantém `validado`/`alterada_reprocessamento` como estavam; conta com algum campo divergente da versão anterior volta pra `validado=False` e `alterada_reprocessamento=True`, guardando o `saldo_atual` de antes em `valor_anterior_reprocessamento` (só pro - tooltip do badge no frontend). Observação nunca é afetada por aqui: ela - não mora mais na linha, e sim em `ContabilObservacao` (histórico por + tooltip do badge no frontend). Uma conta renomeada (mesmo código, outra + 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).""" - antigas = {conta.codigo: conta for conta in apuracao.contas.all()} - vistos: set[str] = set() + antigas = {(conta.codigo, conta.descricao): conta for conta in apuracao.contas.all()} + vistos: set[tuple[str, str]] = set() for indice, extraida in enumerate(contas_extraidas): tipo = extraida.tipo or "A" - antiga = antigas.get(extraida.codigo) + chave = (extraida.codigo, extraida.descricao) + antiga = antigas.get(chave) if antiga is not None: - vistos.add(extraida.codigo) + vistos.add(chave) alterou = ( - antiga.descricao != extraida.descricao - or antiga.tipo != tipo + antiga.tipo != tipo or antiga.saldo_anterior != extraida.saldo_anterior or antiga.debito != extraida.debito 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.ordem = indice antiga.conta_numero = extraida.conta_numero - antiga.descricao = extraida.descricao antiga.tipo = tipo antiga.saldo_anterior = extraida.saldo_anterior antiga.debito = extraida.debito @@ -3854,8 +3859,8 @@ def _contabil_sincroniza_contas( saldo_atual=extraida.saldo_atual, ) - for codigo, antiga in antigas.items(): - if codigo not in vistos: + for chave, antiga in antigas.items(): + if chave not in vistos: antiga.delete() @@ -4322,7 +4327,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet): observacao.ancora = f"{prefixo}-{alvo_id}" if alvo_id is not None else None 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_av_por_chave = { 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("."), 18, 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, diff --git a/static/css/dashboard-contabil.css b/static/css/dashboard-contabil.css index f76c660..19b9596 100644 --- a/static/css/dashboard-contabil.css +++ b/static/css/dashboard-contabil.css @@ -554,7 +554,7 @@ /* 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). */ -.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) { text-align: right; font-variant-numeric: tabular-nums; @@ -570,7 +570,7 @@ font-variant-numeric: tabular-nums; } -.dc-contas-table td:nth-child(1) { +.dc-contas-table td:nth-child(-n + 2) { white-space: nowrap; color: var(--text-muted); font-size: 0.82rem; @@ -706,6 +706,15 @@ 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) — pedido explícito do usuário. Este `display:flex` fica num `
` *dentro* do ``, nunca no próprio `` — um `` com `display:flex` deixa de @@ -874,8 +883,8 @@ 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 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 - código da conta, `--text-muted`), deixando o texto da thread de + apagada de `.dc-contas-table td:nth-child(-n + 2)` (pensada pras colunas + 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 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 diff --git a/static/js/dashboard-contabil.js b/static/js/dashboard-contabil.js index 0ae5ff6..39dc2ad 100644 --- a/static/js/dashboard-contabil.js +++ b/static/js/dashboard-contabil.js @@ -1050,6 +1050,16 @@ document.addEventListener("DOMContentLoaded", async () => { 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) — // pedido explícito do usuário: "nenhum" (cor padrão) quando nem ela nem // 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). 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) { - return conta.codigo; + return `${conta.codigo}|${conta.descricao}`; } function dcChaveObsLinha(linha) { @@ -1435,7 +1451,16 @@ document.addEventListener("DOMContentLoaded", async () => { // 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 // 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 herdadas = observacoes.length - doMes; let titulo = "Adicionar observação"; @@ -1444,9 +1469,16 @@ document.addEventListener("DOMContentLoaded", async () => { if (doMes) partes.push(`${doMes} desta competência`); if (herdadas) partes.push(`${herdadas} do histórico`); 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 ? `${observacoes.length}` : ""; - return ``; } @@ -1771,11 +1803,14 @@ document.addEventListener("DOMContentLoaded", async () => { const validadoInfo = dcValidadoInfo(conta, contas, dcContaNivel, temFilhos[i]); const observacoesDaConta = dcObservacoesDe("conta", dcChaveObsConta(conta)); + const temObsDescendente = + temFilhos[i] && dcTemObservacaoDescendente("conta", contas, dcContaNivel, dcChaveObsConta, conta.id); 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 = ` + ${pidDcEscapeHtml(conta.conta_numero)} ${pidDcEscapeHtml(conta.codigo)} ${toggle}${pidDcEscapeHtml(conta.descricao)} @@ -1789,7 +1824,7 @@ document.addEventListener("DOMContentLoaded", async () => { - ${dcObsBotaoHtml("conta", conta.id, observacoesDaConta)} + ${dcObsBotaoHtml("conta", conta.id, observacoesDaConta, temObsDescendente)} ${pidDcAlteradaBadgeHtml( conta.alterada_reprocessamento, conta.validado, @@ -1803,7 +1838,7 @@ document.addEventListener("DOMContentLoaded", async () => { if (dcObsPainelAberto.conta === conta.id) { const editRow = document.createElement("tr"); 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); } }); @@ -1881,6 +1916,9 @@ document.addEventListener("DOMContentLoaded", async () => { const validadoInfo = dcValidadoInfo(linha, linhas, (l) => Math.max(0, l.nivel), temFilhos[i]); 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"); tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row"; @@ -1896,7 +1934,7 @@ document.addEventListener("DOMContentLoaded", async () => { - ${dcObsBotaoHtml("dre", linha.id, observacoesDaLinha)} + ${dcObsBotaoHtml("dre", linha.id, observacoesDaLinha, temObsDescendente)} ${pidDcAlteradaBadgeHtml( linha.alterada_reprocessamento, linha.validado, @@ -2014,6 +2052,8 @@ document.addEventListener("DOMContentLoaded", async () => { const validadoInfo = dcValidadoInfo(linha, linhas, nivelFn, temFilhos[i]); const observacoesDaLinha = dcObservacoesDe("analise_vertical", dcChaveObsLinha(linha)); + const temObsDescendente = + temFilhos[i] && dcTemObservacaoDescendente("analise_vertical", linhas, nivelFn, dcChaveObsLinha, linha.id); const colunasMensais = linha.valores .map( (v) => @@ -2035,7 +2075,7 @@ document.addEventListener("DOMContentLoaded", async () => { - ${dcObsBotaoHtml("analise_vertical", linha.id, observacoesDaLinha)} + ${dcObsBotaoHtml("analise_vertical", linha.id, observacoesDaLinha, temObsDescendente)} ${pidDcAlteradaBadgeHtml(linha.alterada_reprocessamento, linha.validado, pidDcValorAnteriorAvTexto(linha))}
diff --git a/templates/dashboard-contabil.html b/templates/dashboard-contabil.html index 508ded6..bb6e8da 100644 --- a/templates/dashboard-contabil.html +++ b/templates/dashboard-contabil.html @@ -450,6 +450,7 @@ Conta + Classificação Descrição Saldo Anterior Débito