diff --git a/.claude/settings.json b/.claude/settings.json index f62336e..25205e4 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -86,7 +86,9 @@ "Bash(node -e \"require\\('vm'\\).Script\\(require\\('fs'\\).readFileSync\\('static/js/dashboard-contabil.js','utf8'\\)\\)\")", "Bash(.venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/sim_destaque.py\")", "Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/sim_destaque.py\")", - "Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/sim_expandir_tudo.py\")" + "Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/sim_expandir_tudo.py\")", + "Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_reprocessamento_log.py\")", + "Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_recria_achados.py\")" ] } } diff --git a/portal_api/admin.py b/portal_api/admin.py index f173560..a799985 100644 --- a/portal_api/admin.py +++ b/portal_api/admin.py @@ -8,6 +8,7 @@ from .models import ( CompromissoAgenda, ContabilAchado, ContabilApuracao, + ContabilApuracaoReprocessamento, ContabilConta, ContabilLinhaAnaliseVertical, ContabilLinhaDre, @@ -256,6 +257,12 @@ class ContabilApuracaoAdmin(admin.ModelAdmin): ordering = ("-competencia",) +@admin.register(ContabilApuracaoReprocessamento) +class ContabilApuracaoReprocessamentoAdmin(admin.ModelAdmin): + list_display = ("apuracao", "reprocessado_por", "reprocessado_em") + ordering = ("-reprocessado_em",) + + @admin.register(ContabilConta) class ContabilContaAdmin(admin.ModelAdmin): list_display = ("apuracao", "codigo", "descricao", "tipo", "saldo_atual") diff --git a/portal_api/dashboard_contabil/CHANGELOG.md b/portal_api/dashboard_contabil/CHANGELOG.md index 89c0110..bb9fe27 100644 --- a/portal_api/dashboard_contabil/CHANGELOG.md +++ b/portal_api/dashboard_contabil/CHANGELOG.md @@ -341,3 +341,19 @@ Pedido explícito do usuário: um botão pequeno no canto esquerdo do cabeçalho `.dc-expandir-tudo-btn` (`data-dc-expandir-tudo`) nas 3 tabelas da revisão (Balancete, D.R.E. e Análise Vertical — cada uma com o seu, já que cada árvore tem estado de colapso próprio, mesmo motivo do botão "Restaurar formatação" que já existia), dentro do novo modificador `.dc-th-linha--inicio` (só troca o `justify-content` pra `flex-start`, já que aqui o botão vem antes do rótulo, e não colado na borda direita como o de restaurar). Não é um toggle: o caminho de volta é o próprio "Restaurar formatação padrão", na última coluna da mesma tabela. Detalhe não-óbvio da implementação: expandir tudo **zera** o conjunto de grupos expandidos (`dc*Expandidos`) em vez de populá-lo com todos os grupos — com ele vazio, o destaque volta a sair de `dcUltimaLevaVisivel()`, que sem nada recolhido marca exatamente as folhas (as contas analíticas finais). Conferido contra a apuração real `2021`/`08-2026`: 133 contas visíveis e 74 destacadas, exatamente as 74 contas sem filhos. Popular o conjunto com todos os grupos destacaria quase toda linha da tabela, recaindo no mesmo "tudo destacado, nada se destaca" da rodada 140. Só frontend (HTML/CSS/JS), sem mudança de model/endpoint; o relatório "Gerar Dashboard" não ganhou esse botão nesta rodada. + +### 142. Log de reprocessamentos — quem reprocessou e quantas vezes + +Pedido explícito do usuário: "quando o usuário reprocessar a análise, o ideal é que registre um log... pra que seja possível identificar quem reprocessou e quantos reprocessamentos já ocorreu." + +Model novo `ContabilApuracaoReprocessamento` (`reprocessado_por` FK `SET_NULL` + `reprocessado_em`, migração `0075`) — mesmo espírito de `ContabilObservacaoEdicao`, um registro por reprocessamento bem-sucedido. `ContabilApuracaoViewSet.reprocessar()` grava o registro **dentro** da mesma transação da resincronização (logo depois de sincronizar os achados), então uma tentativa que falhar no meio do caminho não deixa registro órfão. `ContabilApuracaoListSerializer` (a lista de execuções, de onde o botão "Reprocessar" é aberto) ganhou `reprocessamentos` (nested, mais recente primeiro), com `prefetch_related` novo só pra essa action, evitando N+1. + +Frontend: o modal "Reprocessar análise" ganhou uma seção (nasce escondida numa apuração nunca reprocessada) com a contagem e a lista "Nome · dd/mm/aaaa hh:mm" — sem nenhuma chamada de API própria, já que os dados vêm da lista que a tela já carregou. Novo formatador `pidDcFormatDataHora()` (os demais formatadores de data desta tela só mostram o dia, sem hora — aqui a hora importa porque mais de um reprocessamento pode acontecer no mesmo dia). Validado com um script Python isolado (transação com rollback forçado, contra uma apuração real) confirmando ordem/serialização; nada persistido em produção além da migração. Detalhe técnico completo no `CLAUDE.md` desta pasta. + +### 143. Achados voltam a ser recriados do zero a cada reprocessamento (revisão da decisão da rodada 124) + +Pedido explícito do usuário: "sempre que o usuário reprocessar um documento, os apontamentos que são realizados pela própria aplicação e constam na tela de Observações devem ser refeitos. Isso já ressalta para o usuário que não há mais inconsistências para validar e pode verificar apenas se surgiram novos. As observações e comentários que ele realizou nas contas devem permanecer independente do reprocessamento." Reverte por completo a decisão da rodada 124 — até aqui, um achado (apontamento automático de auditoria) já tratado pelo contador continuava marcado como "tratado" mesmo depois de a inconsistência sumir dos dados do PDF novo, contrariando o próprio propósito da tela (sinalizar o que ainda precisa de atenção). + +`_contabil_sincroniza_achados()` (que casava por `(regra, código da conta)` e preservava `status`/`observacao_contador`/`tratado_por`/`tratado_em`/`oculto_no_relatorio`) virou `_contabil_recria_achados()` — apaga **todos** os achados da apuração (`apuracao.achados.all().delete()`) e recria do zero a partir do motor de regras rodado sobre o PDF novo (mesmo `bulk_create()` de `create()`). Todo achado nasce `pendente`, mesmo que a mesma combinação regra+conta já tivesse sido tratada com justificativa antes do reprocessamento — os valores mudaram, então a tratativa antiga deixa de fazer sentido. A preocupação original da rodada 124 (delete+recria quebraria a FK `conta` de um achado preservado) não se aplica mais: não existe mais achado "preservado" através do delete, a FK é sempre resolvida fresca contra as contas já sincronizadas na mesma transação. + +**`ContabilObservacao` (comentário/observação do contador numa conta, sistema separado desde a rodada 126) não é tocada** — nunca teve relação com achados, é casada por chave natural (empresa+conta) e sobrevive a qualquer reprocessamento, exatamente como pedido. Validado com um script Python isolado (`transaction.atomic()` com rollback forçado, contra uma apuração real já em produção): um achado marcado manualmente como "tratado" com justificativa desapareceu depois de simular `_contabil_recria_achados()` com a mesma lista de achados detectados, dando lugar a um achado novo `pendente` com o mesmo conteúdo (regra/conta/mensagem) mas `id` diferente; uma `ContabilObservacao` criada na mesma apuração permaneceu intacta. Nada persistido em produção. Detalhe técnico completo no `CLAUDE.md` desta pasta. diff --git a/portal_api/dashboard_contabil/CLAUDE.md b/portal_api/dashboard_contabil/CLAUDE.md index e5d7856..d1b4774 100644 --- a/portal_api/dashboard_contabil/CLAUDE.md +++ b/portal_api/dashboard_contabil/CLAUDE.md @@ -49,7 +49,7 @@ Padrão cabeçalho → linhas de detalhe → achados (mesma filosofia de `Indica - **`ContabilConta`**: uma linha do Balancete. `observacao` (`TextField`, editável via PATCH em **qualquer** conta, tenha ela gerado achado ou não) — é o espaço de "análise" pedido pelo usuário, independente da auditoria automática. `oculta_no_relatorio` (default `True`, invertido numa rodada — ver "Editor de observação inline" abaixo) e `validado` (`BooleanField`, default `False` — checkbox informativo de "já conferi esta conta", sem efeito em achado/observação/relatório). - **`ContabilLinhaDre`**: uma linha da DRE, sem código de classificação (o relatório não traz um pra DRE, diferente do Balancete). Mesmos `oculta_no_relatorio`/`validado` de `ContabilConta`. - **`ContabilLinhaAnaliseVertical`** (rodada 123): uma linha da Demonstração Mensal (Análise Vertical) — mesma árvore/descrição/nível da DRE, mas `valores` (`JSONField`) guarda um `{"valor": "...", "percentual": "..."}` por mês em vez de um `DecimalField` único (gravado como texto, não float, pra não perder precisão), alinhado por posição com `ContabilApuracao.analise_vertical_meses` (ex.: `["mai/2026", "jun/2026", "jul/2026"]`, lista compartilhada por toda a apuração, não por linha). Mesmos `observacao`/`oculta_no_relatorio`/`validado` de `ContabilConta`/`ContabilLinhaDre` — recursos por linha idênticos (editor inline, tri-state, ocultar do relatório), decisão confirmada com o usuário via `AskUserQuestion` antes de implementar. Lista vazia (`analise_vertical_meses=[]`, nenhuma linha) quando o PDF não tinha essa seção — relatório antigo, ou empresa sem essa seção habilitada no Questor; a aba/tab correspondente some nesse caso (ver "Análise Vertical" abaixo). -- **`ContabilAchado`**: achado de auditoria, nasce automático em `create()`, nunca é apagado — só muda de `status` (`pendente`/`tratado`/`ignorado`), sempre com `observacao_contador` obrigatória ao mudar de pendente (mesmo espírito de "histórico completo preservado" de `ImportacaoPlanoSaudeAuditoria`). `conta` é nullable — achados 1 e 2 (balanceamento/débito-crédito) são gerais, sem uma conta específica. +- **`ContabilAchado`**: achado de auditoria, nasce automático em `create()`, só muda de `status` (`pendente`/`tratado`/`ignorado`) via `ContabilAchadoViewSet`, sempre com `observacao_contador` obrigatória ao mudar de pendente. **Exceção**: um reprocessamento apaga e recria **todos** os achados da apuração do zero (`_contabil_recria_achados()`, rodada 143 — decisão revisada, ver "Reprocessar" abaixo), então "nunca é apagado" só vale fora desse fluxo. `conta` é nullable — achados 1 e 2 (balanceamento/débito-crédito) são gerais, sem uma conta específica. ## `ContabilApuracaoViewSet.create()` — ordem de operações não-trivial @@ -367,16 +367,16 @@ Dois filtros de template novos em `contabil_extras.py` — `moeda_av`/`percentua ### Reprocessar — anexar um PDF novo pra mesma empresa/competência sem perder observações/achados (rodada 124) -Pedido explícito do usuário: até aqui, corrigir uma apuração com o PDF errado/incompleto exigia excluir e recriar do zero (ver docstring antiga de `concluir()`), perdendo toda observação/validação/achado já registrado. Botão "Reprocessar" novo (ícone ao lado de "Abrir", na lista — só aparece em apuração "Em revisão") abre um modal só com o campo de arquivo; o PDF novo precisa ser da **mesma** `codigo_empresa`+competência (senão 400 — trocar de empresa é uma análise nova, não um reprocessamento). Escopo do que preserva/reseta confirmado com o usuário: contas/linhas sem mudança mantêm observação/validado como estavam; contas/linhas que mudaram voltam pra `validado=False` e ganham um alerta visual; achados **nunca são apagados nem têm status/justificativa sobrescritos**, mesmo os que não disparam mais com os dados novos (confirmado explicitamente via `AskUserQuestion` — é pra manter o histórico completo de tratativa). +Pedido explícito do usuário: até aqui, corrigir uma apuração com o PDF errado/incompleto exigia excluir e recriar do zero (ver docstring antiga de `concluir()`), perdendo toda observação/validação/achado já registrado. Botão "Reprocessar" novo (ícone ao lado de "Abrir", na lista — só aparece em apuração "Em revisão") abre um modal só com o campo de arquivo; o PDF novo precisa ser da **mesma** `codigo_empresa`+competência (senão 400 — trocar de empresa é uma análise nova, não um reprocessamento). Escopo do que preserva/reseta confirmado com o usuário nesta rodada: contas/linhas sem mudança mantêm observação/validado como estavam; contas/linhas que mudaram voltam pra `validado=False` e ganham um alerta visual; achados **nunca são apagados nem têm status/justificativa sobrescritos**, mesmo os que não disparam mais com os dados novos (confirmado explicitamente via `AskUserQuestion` — é pra manter o histórico completo de tratativa). **Decisão sobre achados revertida na rodada 143** (ver "Achados são recriados do zero a cada reprocessamento" mais abaixo) — o comportamento atual é o oposto do descrito aqui: achado é apagado e recriado do zero a cada reprocessamento, só a parte de conta/linha (parágrafo acima) continua como decidido nesta rodada. **Modelos**: campo novo `alterada_reprocessamento` (`BooleanField`, default `False`) em `ContabilConta`/`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical` (migração `0068`) — marca que aquela conta/linha mudou no último reprocessamento; o frontend mostra um alerta ao lado do ícone de observação enquanto for `True`. Migração `0069` acrescentou o valor de antes da mudança, só pro tooltip do badge (pedido explícito do usuário — "mostre o valor que estava antes do reprocessamento" ao passar o mouse): `valor_anterior_reprocessamento` (`DecimalField`, `null=True`) em `ContabilConta` (cópia de `saldo_atual`) e `ContabilLinhaDre` (cópia de `valor`); `valores_anterior_reprocessamento` (`JSONField`, mesmo formato de `valores`) em `ContabilLinhaAnaliseVertical`, já que ali não existe um valor único (um por mês). Os três são `read_only` no serializer — só `_contabil_sincroniza_*` grava, nunca um PATCH de cliente. -**`ContabilApuracaoViewSet.reprocessar()`** (`POST /api/contabil-apuracoes/{id}/reprocessar/`, multipart `arquivo`) — bloqueado por `_contabil_garante_em_revisao()` (mesmo gate de qualquer edição; não existe "reprocessar uma apuração Concluída"). Roda o mesmo `dashboard_contabil_pipeline.processa_apuracao()` de `create()`, confere `codigo_empresa`/competência batendo com a apuração existente, troca o `arquivo` (apaga o antigo só **depois** do commit da transação, mesmo cuidado de sempre com storage não-transacional) e delega a resincronização pra 4 funções puras novas — **atualização no lugar (mesmo `id`), não delete+recria**, decisão de design central desta rodada: +**`ContabilApuracaoViewSet.reprocessar()`** (`POST /api/contabil-apuracoes/{id}/reprocessar/`, multipart `arquivo`) — bloqueado por `_contabil_garante_em_revisao()` (mesmo gate de qualquer edição; não existe "reprocessar uma apuração Concluída"). Roda o mesmo `dashboard_contabil_pipeline.processa_apuracao()` de `create()`, confere `codigo_empresa`/competência batendo com a apuração existente, troca o `arquivo` (apaga o antigo só **depois** do commit da transação, mesmo cuidado de sempre com storage não-transacional) e delega a resincronização pra 4 funções puras — cada uma com uma estratégia diferente, conforme o que precisa (ou não) persistir através de um reprocessamento: -- `_contabil_sincroniza_contas()`/`_contabil_sincroniza_linhas_dre()`/`_contabil_sincroniza_linhas_analise_vertical()` — casam cada conta/linha extraída contra a existente (`codigo` pro Balancete; `(descricao, nivel)` pra DRE/Análise Vertical, mesma convenção de chave natural já usada pelo histórico de variação em `regras.py`/`_contabil_monta_historico()` — o par `(descricao, nivel)` desambigua a maioria das descrições repetidas em ramos diferentes da árvore, ex. "COMISSÕES SOBRE VENDAS" aparecendo em mais de um nível, um risco real confirmado contra o PDF de referência). Casada: atualiza os campos brutos **no mesmo registro** (`.save()`, nunca `bulk_create`/delete) — crucial pra achados que referenciam `ContabilConta` nunca perderem a FK; se algum campo relevante mudou, força `validado=False` e `alterada_reprocessamento=True`, guardando o valor de antes em `valor_anterior_reprocessamento`/`valores_anterior_reprocessamento` (sempre lido **antes** de sobrescrever o campo com o valor novo, na mesma função), senão preserva tudo (inclusive limpando esse campo pra `None`/`[]`) como estava. `observacao`/`oculta_no_relatorio` nunca são tocados por essas funções. Sem match na nova extração: `ContabilConta.objects.create()`/equivalente, nasce com os defaults de sempre (`validado=False`, sem o alerta — não tem "antes" pra comparar). Sobra no mapa antigo sem match na nova extração: `.delete()`. -- `_contabil_sincroniza_achados()` — casa por `(regra, código da conta ou None)`. Achado casado: só `titulo`/`mensagem`/`severidade`/`valor_referencia` são atualizados pros valores frescos: `status`/`observacao_contador`/`tratado_por`/`tratado_em`/`oculto_no_relatorio` **nunca** são tocados. Achado sem match na lista fresca (regra não dispara mais): fica **inteiramente intocado** — não é achado "resolvido" nem apagado, continua como estava (histórico). Achado novo: `ContabilAchado.objects.create()` normal, `status="pendente"`. +- `_contabil_sincroniza_contas()`/`_contabil_sincroniza_linhas_dre()`/`_contabil_sincroniza_linhas_analise_vertical()` — **atualização no lugar (mesmo `id`), não delete+recria**: casam cada conta/linha extraída contra a existente (`codigo` pro Balancete; `(descricao, nivel)` pra DRE/Análise Vertical, mesma convenção de chave natural já usada pelo histórico de variação em `regras.py`/`_contabil_monta_historico()` — o par `(descricao, nivel)` desambigua a maioria das descrições repetidas em ramos diferentes da árvore, ex. "COMISSÕES SOBRE VENDAS" aparecendo em mais de um nível, um risco real confirmado contra o PDF de referência). Casada: atualiza os campos brutos **no mesmo registro** (`.save()`, nunca `bulk_create`/delete); se algum campo relevante mudou, força `validado=False` e `alterada_reprocessamento=True`, guardando o valor de antes em `valor_anterior_reprocessamento`/`valores_anterior_reprocessamento` (sempre lido **antes** de sobrescrever o campo com o valor novo, na mesma função), senão preserva tudo (inclusive limpando esse campo pra `None`/`[]`) como estava. `observacao`/`oculta_no_relatorio` nunca são tocados por essas funções. Sem match na nova extração: `ContabilConta.objects.create()`/equivalente, nasce com os defaults de sempre (`validado=False`, sem o alerta — não tem "antes" pra comparar). Sobra no mapa antigo sem match na nova extração: `.delete()`. Mantidas assim (não delete+recria) porque `codigo`/`(descricao, nivel)` **é** a identidade da conta/linha do ponto de vista do contador — o mesmo id continua referenciado por `ContabilObservacao` (chave natural, mas ainda assim o `id` da conta importa pra `_contabil_arvore_contexto()` no relatório) e é preciso preservar `validado`/observações de itens que não mudaram. +- `_contabil_recria_achados()` (**rodada 143, revisão de uma decisão anterior** — ver abaixo) — **delete+recria total**, mesmo `bulk_create` de `create()`: apaga **todos** os achados da apuração e gera um conjunto novo a partir das regras rodadas sobre o PDF novo. Todo achado nasce `pendente`, mesmo que a mesma `(regra, conta)` já tivesse sido tratada antes do reprocessamento. -**Por que atualização no lugar em vez de delete+recria**: a alternativa óbvia (apagar tudo e rodar `bulk_create` como em `create()`) quebraria a FK de todo achado preservado que referencia uma `ContabilConta` (o `on_delete=SET_NULL` desvincularia silenciosamente a conta do achado) — mantendo o mesmo `id` por conta/linha casada, a FK nunca precisa ser tocada, e achados "sobreviventes" continuam apontando pra conta certa sem nenhum código extra de re-vinculação. +**Por que os dois grupos usam estratégias opostas**: conta/linha é dado extraído do PDF que o contador **anota** (observação/validado) — o valor de hoje precisa ser atualizado, mas a anotação de ontem sobre a mesma conta continua valendo, então "atualiza no lugar" preserva tudo que não mudou. Achado é um **apontamento derivado**, recalculado inteiramente a cada rodada das regras — não existe "achado que não mudou", ele ou dispara com os dados de agora ou não dispara; manter um achado "tratado" que não dispara mais equivalia a mostrar uma inconsistência que já não existe. **Frontend** (`dashboard-contabil.html`/`.js`): botão de ícone (refresh) ao lado de "Abrir" na lista (`data-dc-reprocessar-abrir`, escondido quando `status === "concluida"`) abre `#dc-reprocessar-modal` (mesmo campo de arquivo de "Nova Análise", reaproveitando `wireArquivoField()` que já era genérico o bastante) — `pidReprocessarApuracaoContabil(id, formData)` chama o endpoint, atualiza a lista e, se a apuração reprocessada é a que já está aberta na revisão, também re-renderiza a tela (`apuracaoAtual = atualizada; renderRevisao()`). Cada uma das 3 tabelas (Balancete/DRE/Análise Vertical) ganhou um badge de alerta (`pidDcAlteradaBadgeHtml()`, ícone de triângulo) ao lado do botão de observação, visível enquanto `alterada_reprocessamento` for `True`. **Marcar a conta/linha como validada de novo NÃO limpa o alerta** (pedido explícito do usuário, revertendo a primeira versão desta rodada, que limpava — "quando o usuário marcar como validado uma conta que foi reprocessada, não deve sumir o ícone de aviso, mas sim, ficar verde... conseguimos verificar quais itens foram reprocessados e revalidados") — o badge muda de cor conforme `validado` da própria conta/linha: `--danger` (vermelho) enquanto pendente, verde (`--validada`, mesmo hex de `.status-pill--ativo`) depois de validado; `pidAtualizarValidadoContaContabil()`/`...LinhaDreContabil()`/`...LinhaAnaliseVerticalContabil()` voltaram a enviar só `{ validado }`, sem tocar em `alterada_reprocessamento`. O campo só é limpo de verdade num próximo reprocessamento sem mudança naquela conta/linha específica (`_contabil_sincroniza_*`, backend). **Tooltip do badge mostra o valor de antes do reprocessamento**: Balancete/DRE formatam `valor_anterior_reprocessamento` direto com `pidDcFormatMoeda()`; Análise Vertical usa `pidDcValorAnteriorAvTexto()`, que junta o valor+percentual de cada mês de `valores_anterior_reprocessamento` (mesmo alinhamento posicional de `analise_vertical_meses`) numa linha por mês dentro do mesmo tooltip. Nenhum badge tem tooltip de valor quando o campo vem `null`/vazio (conta/linha nova nesta apuração, sem "antes" pra comparar). @@ -386,6 +386,20 @@ Pedido explícito do usuário: até aqui, corrigir uma apuração com o PDF erra **Validado com os 3 testes reais**: (1) reprocessar com o **mesmo** arquivo duas vezes seguidas — `validado`/`observacao` de conta/DRE/Análise Vertical preservados, `alterada_reprocessamento` continua `False` em tudo, contagem de linhas idêntica; (2) tentar reprocessar com o arquivo de **outra empresa** — 400 com mensagem explicando a diferença de empresa/competência; (3) tentar reprocessar uma apuração **Concluída** — 400 bloqueado por `_contabil_garante_em_revisao()`. As 4 funções de sincronização também foram testadas isoladamente (dry-run com `transaction.atomic()` + rollback forçado, dados fabricados cobrindo conta/linha inalterada, alterada, nova e removida, mais achado que continua disparando e achado que para de disparar) — todos os casos bateram com o comportamento esperado antes de considerar a implementação pronta. Nenhum resíduo deixado em produção. +**Log de reprocessamentos (rodada 142)**: pedido explícito do usuário — "identificar quem reprocessou e quantos reprocessamentos já ocorreu". Model novo `ContabilApuracaoReprocessamento` (`related_name="reprocessamentos"`, `reprocessado_por` FK `SET_NULL`, `reprocessado_em` `auto_now_add`, migração `0075`) — mesmo espírito de `ContabilObservacaoEdicao`, um registro por chamada bem-sucedida. `reprocessar()` cria o registro **dentro** do `with transaction.atomic()`, logo depois de `_contabil_recria_achados()` — uma tentativa que falhar no meio do caminho (PDF de empresa errada, erro de sincronização) não deixa um registro órfão, já que a transação inteira reverte junto. + +**Achados são recriados do zero a cada reprocessamento (rodada 143, revisão da decisão da rodada 124)**: pedido explícito do usuário — "os apontamentos que são realizados pela própria aplicação e constam na tela de Observações devem ser refeitos [a cada reprocessamento]. Isso já ressalta para o usuário que não há mais inconsistências para validar e pode verificar apenas se surgiram novos. As observações e comentários que ele realizou nas contas devem permanecer independente do reprocessamento." Reverte por completo a decisão da rodada 124 de nunca apagar/sobrescrever `status`/`observacao_contador` de achado — o comportamento anterior deixava um apontamento "tratado" pendurado na aba Observações mesmo depois de a inconsistência sumir dos dados novos, contrariando o próprio propósito de sinalizar o que ainda precisa de atenção. + +`_contabil_sincroniza_achados()` virou `_contabil_recria_achados()` (`views.py`) — em vez de casar por `(regra, código da conta)` e atualizar/preservar campo a campo, agora é `apuracao.achados.all().delete()` seguido de `ContabilAchado.objects.bulk_create(...)` com a lista fresca de `resultado.achados`, **exatamente o mesmo bloco de `create()`** (só que sobre uma apuração já existente). Todo achado nasce `pendente`, mesmo que a mesma combinação `(regra, conta)` já estivesse `tratado`/`ignorado` com uma justificativa escrita antes — a justificativa antiga não é preservada em lugar nenhum, some junto com o achado antigo (decisão explícita: os valores mudaram, então a tratativa escrita sobre os valores antigos não é mais válida). + +**Por que isso não quebra a FK de conta/observação**: a preocupação original da rodada 124 (delete+recria quebraria a FK `ContabilAchado.conta` de um achado preservado) não se aplica mais porque não há mais achado "preservado" através do delete — todo achado é recriado do zero, então a FK é sempre resolvida fresca contra `contas_por_codigo` (o mapa de contas **já sincronizadas** por `_contabil_sincroniza_contas()`, que roda antes na mesma transação). `ContabilObservacao` (comentário do contador na conta, pedido explícito do usuário pra continuar intocado) nunca teve relação nenhuma com `ContabilAchado` — é casada por chave natural (empresa+conta), vive num model totalmente separado, e `_contabil_recria_achados()` nem toca nela. + +Validado com um script Python isolado (`transaction.atomic()` com rollback forçado, contra uma apuração real já em produção): um achado marcado manualmente como `tratado` com justificativa, seguido de uma chamada simulando `_contabil_recria_achados()` com a mesma lista de achados detectados — o achado tratado desaparece e um novo `pendente` idêntico em conteúdo (mesma regra/conta/mensagem) toma o lugar, com `id` diferente; nada persistido além da migração já aplicada em rodadas anteriores. + +`ContabilApuracaoListSerializer` ganhou `reprocessamentos` (nested, via `ContabilApuracaoReprocessamentoSerializer` — só `id`/`reprocessado_por_nome`/`reprocessado_em`) — não um campo `Detail`, porque o único botão "Reprocessar" da tela vive na **lista** (`data-dc-reprocessar-abrir`, ao lado de "Abrir"), nunca dentro da revisão já aberta; `get_queryset()` ganhou `prefetch_related("reprocessamentos__reprocessado_por")` só pra `action == "list"`, evitando N+1 por apuração (mesmo cuidado que `total_achados_pendentes` já deveria ter tido, mas não tinha — não corrigido aqui, fora do escopo do pedido). + +**Frontend**: `abrirReprocessarModal(id)` ganhou `renderReprocessarLog(id)`, que procura a apuração em `dcListaApuracoes` (já carregada, sem chamada de API própria) e preenche uma seção nova dentro do próprio `#dc-reprocessar-modal` (`#dc-reprocessar-log`, nasce `hidden` — não aparece nada numa apuração nunca reprocessada) com a contagem (`.dc-reprocessar-log__count`, mesmo padrão visual de `.dc-obs-resumo__count`) e a lista "Nome · dd/mm/aaaa hh:mm" (mais recente primeiro, ordem que já vem do `Meta.ordering` do model). `pidDcFormatDataHora()` (novo, ao lado de `pidDcFormatData()`) é o primeiro formatador de data+hora deste arquivo — os demais (assinatura de observação, "Criado em" da lista) só mostram a data, sem hora; aqui a hora importa porque mais de um reprocessamento pode acontecer no mesmo dia. Como `carregarLista()` já roda depois de um reprocessamento bem-sucedido (pra atualizar a linha da tabela), o log fica correto da próxima vez que o modal for aberto pra mesma apuração, sem nenhuma chamada extra. Validado com um script Python ad-hoc (dentro de `transaction.atomic()` com rollback forçado, contra uma apuração real já em produção): dois registros criados, serializados na ordem certa (mais recente primeiro) e com `reprocessado_por_nome` nulo tratado como esperado; nada persistido. + ### PDF de fonte atípica — título de seção sem acento + aviso ao contador (rodada 125) **Bug real, encontrado com um PDF de cliente novo** (`1751 - Balancete 07.2026.pdf`, TAROBA INDUSTRIA HOTELEIRA LTDA): `POST /api/contabil-apuracoes/` devolvia 400 "Nenhuma linha de DRE encontrada no PDF" — o `codigo_empresa`/cabeçalho extraía normalmente, só a seção da DRE nunca era reconhecida. Causa raiz, confirmada rodando `pdfplumber` de verdade contra o arquivo (nunca supor a partir de texto colado — ver `[[feedback_pdf_parser_precisa_arquivo_real]]` na memória): a fonte embutida nesse PDF específico (instalação/versão diferente do Questor) perde o til do "Ã" ao extrair "DEMONSTRAÇÃO DO RESULTADO DO EXERCÍCIO" → sai "DEMONSTRAÇAO..." (só falta o til, resto do caractere sai certo — não é um replacement character). `parser.py` comparava esse título por igualdade exata (`texto.startswith(_TITULO_DRE)`), então a seção nunca era detectada. @@ -406,7 +420,7 @@ Pedido explícito do usuário: uma observação registrada num mês (o exemplo d Três decisões de escopo confirmadas por `AskUserQuestion` **antes** de implementar, todas com a opção recomendada aceita: 1. **Toda observação propaga por padrão** — não existe "fixar"; o que existe é o inverso, encerrar explicitamente. Evita histórico que só existe quando alguém lembra de marcar. -2. **O histórico cobre Balancete/D.R.E./Análise Vertical** — a justificativa de tratativa de um item de auditoria (`ContabilAchado.observacao_contador`) continua presa à apuração como sempre foi (ela já tem histórico próprio, nunca é apagada nem sobrescrita, ver "Models" acima). Misturar os dois fluxos aumentaria o escopo sem ganho claro. +2. **O histórico cobre Balancete/D.R.E./Análise Vertical** — a justificativa de tratativa de um item de auditoria (`ContabilAchado.observacao_contador`) continua presa à apuração como sempre foi. Misturar os dois fluxos aumentaria o escopo sem ganho claro. (Nota da rodada 143: essa justificativa **não** sobrevive mais a um reprocessamento — o achado inteiro é recriado do zero nesse fluxo, ver "Achados são recriados do zero a cada reprocessamento" mais abaixo; ela só é permanente enquanto a apuração não é reprocessada, diferente de `ContabilObservacao`, que atravessa competências inteiras.) 3. **`mostrar_ao_cliente` é sempre alternável**, inclusive numa observação já travada — o bloqueio protege texto, autor e data; mostrar ou não ao cliente é decisão editorial de cada relatório, e uma marcação errada precisa ser corrigível sem reescrever o histórico. **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`. @@ -471,6 +485,6 @@ Corrigido com um helper novo, `_moeda(valor: Decimal) -> str` (`regras.py`), dup **Não corrigido nesta rodada** (fora do escopo do pedido, mesma família de bug): `regra_variacao_atipica_dre` formata percentual com `f"{valor:.2f}%"` (ponto decimal, ex. "12.34%"), também inconsistente com o padrão BR (`12,34%`) — se o usuário pedir, é o mesmo tipo de ajuste. -**Só vale pra achados gerados dali em diante** — um achado já persistido em produção mantém o texto antigo (sem formatação) até a apuração ser reprocessada (`reprocessar()`/`_contabil_sincroniza_achados()` em `views.py` sobrescreve `mensagem` de um achado que ainda dispara na mesma conta+regra) ou até uma nova apuração da mesma empresa ser criada do zero; não foi escrita nenhuma migração de dados pra reformatar o texto já gravado (parsear números dentro de frase livre por regex é arriscado — o mesmo texto tem números que não são valores, como código de conta "1.01.01.001"). +**Só vale pra achados gerados dali em diante** — um achado já persistido em produção mantém o texto antigo (sem formatação) até a apuração ser reprocessada (`reprocessar()`/`_contabil_recria_achados()` em `views.py` recria **todo** achado do zero, inclusive `mensagem`, desde a rodada 143 — ver "Achados são recriados do zero a cada reprocessamento" abaixo) ou até uma nova apuração da mesma empresa ser criada do zero; não foi escrita nenhuma migração de dados pra reformatar o texto já gravado (parsear números dentro de frase livre por regex é arriscado — o mesmo texto tem números que não são valores, como código de conta "1.01.01.001"). **Testado ponta a ponta via `Client.force_login()`** dentro de uma transação com rollback forçado: listagem por vigência, criação com chave derivada no servidor, alvo de outra apuração recusado, edição de texto, bloqueio do texto quando a apuração de origem está concluída (com a visibilidade ainda alternável nesse mesmo caso), encerrar e reativar com as fronteiras de competência conferidas nos dois sentidos, herança numa competência seguinte (observação marcada como histórica) e o relatório gerado nos dois meses, incluindo a conferência de que observação interna não vaza pro relatório do cliente. Nada gravado em produção além da própria migração. diff --git a/portal_api/migrations/0075_contabilapuracaoreprocessamento.py b/portal_api/migrations/0075_contabilapuracaoreprocessamento.py new file mode 100644 index 0000000..60c27b8 --- /dev/null +++ b/portal_api/migrations/0075_contabilapuracaoreprocessamento.py @@ -0,0 +1,29 @@ +# Generated by Django 6.0.7 on 2026-09-18 12:40 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('portal_api', '0074_importacaoplanosaudedepaulalinha_tipo_pessoa_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ContabilApuracaoReprocessamento', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reprocessado_em', models.DateTimeField(auto_now_add=True, verbose_name='Reprocessado em')), + ('apuracao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reprocessamentos', to='portal_api.contabilapuracao')), + ('reprocessado_por', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reprocessamentos_contabeis', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Reprocessamento do Relatório Contábil', + 'verbose_name_plural': 'Reprocessamentos do Relatório Contábil', + 'ordering': ['-reprocessado_em', '-id'], + }, + ), + ] diff --git a/portal_api/models.py b/portal_api/models.py index b6ed41e..e778f5f 100644 --- a/portal_api/models.py +++ b/portal_api/models.py @@ -2065,6 +2065,29 @@ class ContabilApuracao(models.Model): return f"{self.codigo_empresa} — {self.competencia:%m/%Y}" +class ContabilApuracaoReprocessamento(models.Model): + """Log de cada reprocessamento de uma `ContabilApuracao` (pedido explícito + do usuário — "identificar quem reprocessou e quantos reprocessamentos já + ocorreu"), mesmo espírito de `ContabilObservacaoEdicao`. Um registro por + chamada bem-sucedida de `ContabilApuracaoViewSet.reprocessar()` — criado + dentro da mesma transação da resincronização, então uma tentativa que + falhar no meio do caminho não deixa um registro órfão aqui.""" + + apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="reprocessamentos") + reprocessado_por = models.ForeignKey( + Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="reprocessamentos_contabeis" + ) + reprocessado_em = models.DateTimeField("Reprocessado em", auto_now_add=True) + + class Meta: + verbose_name = "Reprocessamento do Relatório Contábil" + verbose_name_plural = "Reprocessamentos do Relatório Contábil" + ordering = ["-reprocessado_em", "-id"] + + def __str__(self) -> str: + return f"Reprocessamento de {self.apuracao_id} em {self.reprocessado_em:%d/%m/%Y %H:%M}" + + class ContabilConta(models.Model): """Uma linha do Balancete extraída do PDF (grupo Ativo/Passivo, sintética ou analítica). `observacao` é livre e editável pelo contador via PATCH em @@ -2199,11 +2222,19 @@ class ContabilLinhaAnaliseVertical(models.Model): class ContabilAchado(models.Model): """Um achado gerado automaticamente pelo motor de regras - (`dashboard_contabil.regras`) ao criar a apuração — nunca é apagado, só - muda de `status` conforme o contador revisa (mesmo espírito de - `ImportacaoPlanoSaudeAuditoria`: histórico completo preservado). `conta` - é opcional porque alguns achados são gerais (ex.: desbalanceamento - Ativo x Passivo), sem uma única conta associada.""" + (`dashboard_contabil.regras`) ao criar a apuração, só muda de `status` + conforme o contador revisa (mesmo espírito de `ImportacaoPlanoSaudeAuditoria`). + `conta` é opcional porque alguns achados são gerais (ex.: desbalanceamento + Ativo x Passivo), sem uma única conta associada. + + **Exceção a "nunca é apagado"**: um reprocessamento (`ContabilApuracaoViewSet. + reprocessar()`/`_contabil_recria_achados()` em views.py) apaga **todos** + os achados da apuração e recria do zero a partir das regras rodadas sobre + o PDF novo — decisão explícita do usuário, pra um apontamento que não + dispara mais sumir da aba Observações em vez de ficar pendurado "tratado" + (ver CHANGELOG.md do pacote `dashboard_contabil`). Fora desse fluxo (ex.: + `ContabilAchadoViewSet.update()`/`alternar_oculto()`), o registro nunca é + apagado, só muda de campo.""" SEVERIDADE_ALTA = "alta" SEVERIDADE_MEDIA = "media" diff --git a/portal_api/serializers.py b/portal_api/serializers.py index 6d60433..2d420e6 100644 --- a/portal_api/serializers.py +++ b/portal_api/serializers.py @@ -18,6 +18,7 @@ from .models import ( CompromissoAgenda, ContabilAchado, ContabilApuracao, + ContabilApuracaoReprocessamento, ContabilConta, ContabilLinhaAnaliseVertical, ContabilLinhaDre, @@ -2680,9 +2681,24 @@ class IndicadorContabilDefinicaoInputSerializer(serializers.Serializer): return attrs +class ContabilApuracaoReprocessamentoSerializer(serializers.ModelSerializer): + """Um item do log de reprocessamentos de uma apuração (ver + `ContabilApuracaoReprocessamento`) — alimenta o modal "Reprocessar + análise" da lista, pra o contador ver quem já reprocessou e quantas + vezes, antes de decidir se reprocessa de novo.""" + + reprocessado_por_nome = serializers.CharField(source="reprocessado_por.nome", read_only=True, default=None) + + class Meta: + model = ContabilApuracaoReprocessamento + fields = ["id", "reprocessado_por_nome", "reprocessado_em"] + read_only_fields = fields + + class ContabilApuracaoListSerializer(serializers.ModelSerializer): criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None) total_achados_pendentes = serializers.SerializerMethodField() + reprocessamentos = ContabilApuracaoReprocessamentoSerializer(many=True, read_only=True) class Meta: model = ContabilApuracao @@ -2698,6 +2714,7 @@ class ContabilApuracaoListSerializer(serializers.ModelSerializer): "concluida_em", "total_achados_pendentes", "fonte_pdf_atipica", + "reprocessamentos", ] def get_total_achados_pendentes(self, obj: ContabilApuracao) -> int: diff --git a/portal_api/views.py b/portal_api/views.py index 0e4d759..5a0b5a7 100644 --- a/portal_api/views.py +++ b/portal_api/views.py @@ -56,6 +56,7 @@ from .models import ( CompromissoAgenda, ContabilAchado, ContabilApuracao, + ContabilApuracaoReprocessamento, ContabilConta, ContabilLinhaAnaliseVertical, ContabilLinhaDre, @@ -3990,46 +3991,44 @@ def _contabil_sincroniza_linhas_analise_vertical( antiga.delete() -def _contabil_sincroniza_achados( +def _contabil_recria_achados( apuracao: ContabilApuracao, achados_detectados: list[dashboard_contabil_modelos.AchadoDetectado] ) -> None: - """Resincroniza `ContabilAchado` a partir de um reprocessamento — - **nunca apaga nem sobrescreve `status`/`observacao_contador`/ - `tratado_por`/`tratado_em`/`oculto_no_relatorio`** de um achado já - existente (decisão explícita do usuário: preservar o histórico de - tratativa mesmo que a regra não dispare mais com os dados novos). - Casado por `(regra, código da conta)` — `None` no lugar do código pras - poucas regras gerais sem conta própria (ex. balanceamento Ativo x - Passivo). Um achado cuja combinação não aparece mais em - `achados_detectados` fica intocado (nem "tratado" nem "pendente" muda); - um achado novo nasce `pendente`, do jeito de sempre.""" - contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()} - antigos = { - (achado.regra, achado.conta.codigo if achado.conta_id else None): achado - for achado in apuracao.achados.select_related("conta").all() - } + """Recria do zero todo `ContabilAchado` de um reprocessamento — apaga + **todos** os achados existentes da apuração e recria a partir do motor de + regras rodado sobre o PDF novo, mesmo `bulk_create` de `create()` (ver + acima). Decisão revisada explicitamente pelo usuário (substitui a + decisão anterior de preservar `status`/`observacao_contador`/`tratado_por`/ + `tratado_em`/`oculto_no_relatorio` — ver histórico no CHANGELOG.md do + pacote): um apontamento automático que não dispara mais com os dados + novos precisa **sumir** da aba Observações, sinalizando ao contador que + aquela inconsistência não existe mais, em vez de continuar pendurado + (mesmo "tratado") como se ainda precisasse de atenção. Todo achado que + ainda dispara nasce de novo como `pendente`, mesmo que já tivesse sido + tratado antes do reprocessamento — o contador revisa de novo com os + valores atuais, não reaproveita uma justificativa escrita sobre dados que + já mudaram. - for achado in achados_detectados: - chave = (achado.regra, achado.codigo_conta) - conta = contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None - existente = antigos.get(chave) - if existente is not None: - existente.conta = conta - existente.titulo = achado.titulo - existente.mensagem = achado.mensagem - existente.severidade = achado.severidade - existente.valor_referencia = achado.valor_referencia - existente.save() - else: - ContabilAchado.objects.create( + **Isolado do resto do reprocessamento**: observações do contador + (`ContabilObservacao`, ver o model) não vivem aqui, são casadas por chave + natural (empresa+conta) e continuam intactas através de qualquer + reprocessamento — só o apontamento automático é recriado.""" + contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()} + apuracao.achados.all().delete() + ContabilAchado.objects.bulk_create( + [ + ContabilAchado( apuracao=apuracao, - conta=conta, + conta=contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None, regra=achado.regra, severidade=achado.severidade, titulo=achado.titulo, mensagem=achado.mensagem, valor_referencia=achado.valor_referencia, ) + for achado in achados_detectados + ] + ) class ContabilApuracaoViewSet(viewsets.ModelViewSet): @@ -4054,6 +4053,8 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet): queryset = queryset.prefetch_related( "contas", "linhas_dre", "linhas_analise_vertical", "achados__conta" ) + elif self.action == "list": + queryset = queryset.prefetch_related("reprocessamentos__reprocessado_por") return queryset def get_serializer_class(self) -> type[ModelSerializer]: @@ -4186,8 +4187,8 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet): """Reanexa um novo PDF pra **mesma** empresa/competência (ex.: o arquivo original tinha um erro/estava incompleto) e resincroniza conta/linha por conta/linha, em vez de excluir e recriar tudo do - zero — pedido explícito do usuário, pra preservar observações e - achados já tratados: + zero — pedido explícito do usuário, pra preservar observações do + contador: - Conta/linha já existente (casada por `codigo`, no Balancete, ou por `(descricao, nivel)`, na DRE/Análise Vertical — mesma convenção já @@ -4202,12 +4203,19 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet): (`alterada_reprocessamento=False` — não tem "antes" pra comparar). - Conta/linha que só existia na versão antiga (sumiu do PDF novo) é excluída. - - **Achado nunca é apagado nem tem seu `status`/`observacao_contador` - sobrescrito** (confirmado com o usuário): um achado da mesma - `(regra, código da conta)` que ainda se aplica só tem - título/mensagem/severidade atualizados; um que não se aplica mais - continua exatamente como estava (histórico); um achado novo nasce - `pendente`, do jeito de sempre. + - **Todo achado (apontamento automático de auditoria) é recriado do + zero** — `_contabil_recria_achados()` apaga os achados existentes e + gera um conjunto novo a partir das regras rodadas sobre o PDF novo, + todos nascendo `pendente` (decisão revisada explicitamente pelo + usuário: um apontamento que não dispara mais deve sumir da aba + Observações, não continuar pendurado "tratado" — ver + CHANGELOG.md do pacote pra decisão anterior que esta substitui). + Observação/comentário do contador nas contas (`ContabilObservacao`) + **não** é achado, continua intocada. + - Cada reprocessamento bem-sucedido grava um `ContabilApuracaoReprocessamento` + (quem + quando), pra o contador ver o histórico completo e a + contagem antes de decidir se reprocessa de novo (modal "Reprocessar + análise" da lista) — pedido explícito do usuário. Bloqueado numa apuração "Concluída" (`_contabil_garante_em_revisao`, mesmo gate de qualquer edição) e numa competência/empresa diferente @@ -4267,7 +4275,8 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet): _contabil_sincroniza_contas(apuracao, resultado.extracao.contas) _contabil_sincroniza_linhas_dre(apuracao, resultado.extracao.linhas_dre) _contabil_sincroniza_linhas_analise_vertical(apuracao, resultado.extracao.linhas_analise_vertical) - _contabil_sincroniza_achados(apuracao, resultado.achados) + _contabil_recria_achados(apuracao, resultado.achados) + ContabilApuracaoReprocessamento.objects.create(apuracao=apuracao, reprocessado_por=request.user) except Exception: if novo_arquivo_salvo: apuracao.arquivo.delete(save=False) diff --git a/static/css/dashboard-contabil.css b/static/css/dashboard-contabil.css index 9b220d4..0f2e4be 100644 --- a/static/css/dashboard-contabil.css +++ b/static/css/dashboard-contabil.css @@ -251,6 +251,60 @@ flex-shrink: 0; } +/* Log de reprocessamentos anteriores, dentro do modal "Reprocessar análise" + — reaproveita .dc-obs-resumo__titulo/__count (mesmo "título + contagem em + pílula" já usado no resumo de observações por aba), só a lista em si é + nova. Nasce `hidden` (ver dashboard-contabil.js) quando a apuração ainda + não foi reprocessada nenhuma vez — sem seção vazia sobrando no modal. */ +.dc-reprocessar-log { + margin-top: var(--space-1); + padding-top: var(--space-3); + border-top: 1px solid var(--border-subtle); +} + +.dc-reprocessar-log__titulo { + display: flex; + align-items: center; + gap: var(--space-2); + font-size: 0.95rem; + margin: 0 0 var(--space-2); +} + +.dc-reprocessar-log__count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + padding: 0 var(--space-2); + border-radius: var(--radius-pill); + background: rgba(var(--accent-rgb), 0.14); + color: var(--accent); + font-size: 0.75rem; + font-weight: 700; +} + +.dc-reprocessar-log__lista { + list-style: none; + margin: 0; + padding: 0; + max-height: 160px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.dc-reprocessar-log__item { + font-size: 0.82rem; + color: var(--text-muted); +} + +.dc-reprocessar-log__item strong { + color: var(--text-primary); + font-weight: 600; +} + /* Achados */ /* Resumo do topo da aba Observações — donut por severidade (SVG puro, sem diff --git a/static/js/dashboard-contabil.js b/static/js/dashboard-contabil.js index 848011c..0c46a07 100644 --- a/static/js/dashboard-contabil.js +++ b/static/js/dashboard-contabil.js @@ -145,6 +145,17 @@ function pidDcFormatData(iso) { return new Date(iso).toLocaleDateString("pt-BR"); } +function pidDcFormatDataHora(iso) { + if (!iso) return ""; + return new Date(iso).toLocaleString("pt-BR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + function pidDcEscapeHtml(texto) { const div = document.createElement("div"); div.textContent = texto == null ? "" : String(texto); @@ -760,11 +771,34 @@ document.addEventListener("DOMContentLoaded", async () => { const resetReprocessarArquivoField = wireArquivoField("dc-reprocessar-arquivo"); let dcReprocessarApuracaoId = null; + // Log de reprocessamentos anteriores (quem + quando) — pedido explícito do + // usuário, "identificar quem reprocessou e quantos reprocessamentos já + // ocorreu". `dcListaApuracoes` já carrega `reprocessamentos` (ordenado do + // mais recente pro mais antigo, ver Meta.ordering do model) junto do resto + // da lista, então não precisa de uma chamada de API própria — só procurar + // a apuração pelo id. + function renderReprocessarLog(id) { + const secao = document.getElementById("dc-reprocessar-log"); + const apuracao = dcListaApuracoes.find((a) => a.id === id); + const reprocessamentos = apuracao ? apuracao.reprocessamentos || [] : []; + secao.hidden = reprocessamentos.length === 0; + if (reprocessamentos.length === 0) return; + + document.getElementById("dc-reprocessar-log-count").textContent = reprocessamentos.length; + document.getElementById("dc-reprocessar-log-lista").innerHTML = reprocessamentos + .map( + (r) => + `