Reestruturação das auditorias de balancetes
This commit is contained in:
parent
a8db64d949
commit
86c738cc53
@ -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\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) =>
|
||||
`<li class="dc-reprocessar-log__item"><strong>${pidDcEscapeHtml(r.reprocessado_por_nome || "Usuário removido")}</strong> · ${pidDcFormatDataHora(r.reprocessado_em)}</li>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function abrirReprocessarModal(id) {
|
||||
dcReprocessarApuracaoId = id;
|
||||
document.getElementById("dc-reprocessar-arquivo").value = "";
|
||||
resetReprocessarArquivoField();
|
||||
document.getElementById("dc-reprocessar-error").textContent = "";
|
||||
renderReprocessarLog(id);
|
||||
document.getElementById("dc-reprocessar-modal").hidden = false;
|
||||
}
|
||||
|
||||
|
||||
@ -619,6 +619,10 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<section class="dc-reprocessar-log" id="dc-reprocessar-log" hidden>
|
||||
<h3 class="dc-reprocessar-log__titulo">Reprocessamentos anteriores <span class="dc-reprocessar-log__count" id="dc-reprocessar-log-count">0</span></h3>
|
||||
<ul class="dc-reprocessar-log__lista" id="dc-reprocessar-log-lista"></ul>
|
||||
</section>
|
||||
<p class="modal-error" id="dc-reprocessar-error"></p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-outline" id="dc-reprocessar-cancel-btn">Cancelar</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user