# CLAUDE.md — Não Conformidades (Relatórios > Qualidade) Gestão contínua das ocorrências e ações do Sistema de Gestão da Qualidade (Sigsistem, ISO 9001) da De Paula Contadores. Evolui uma skill do Claude (`projects/Controladoria - Analise NCs/analise-ncs.skill`) que gerava um Excel de 7 abas sob demanda, para uma ferramenta persistente dentro do Portal: cada Ocorrência/Ação é upsertada por código a cada importação, com um status interno de tratativa da Qualidade que reabre sozinho quando algo muda. ## Origem — a skill "analise-ncs" `projects/Controladoria - Analise NCs/` guarda a fonte de verdade das regras de negócio originais: - `analise-ncs_SKILL.md` — spec completa (conceitos, regras de classificação de cada aba do relatório antigo, formatação). - `analise-ncs.skill` (zip) → `scripts/gerar_relatorio.py` — script de referência (pandas), `scripts/colunas_xlsx.txt` — as 36 colunas do `.xlsx` de ocorrências. - `ocorrencias_pa_27082026.xlsx`/`acoes_27082026.xls` — amostras reais usadas para validar o parser novo contra o relatório de referência (`Relatorio_Qualidade_20260827.xlsx`) durante o desenvolvimento. Todo o parsing deste pacote foi portado (sem pandas, ver "Por que sem pandas" abaixo) linha a linha da lógica desse script, validado batendo os mesmos totais do relatório de referência (ocorrências, sem análise, análise sem ação, NCs sem ação corretiva, ações vencidas) — **exceto** "Total de Ações Abertas", que a skill original contava incluindo linhas de ocorrência sem nenhuma ação de fato aberta (artefato do script — `todas = v.copy()` sem filtrar por `Código da Ação` presente); esta ferramenta conta só ações reais. ## O que MUDA em relação à skill original 1. **Persistência contínua**: `NCOcorrencia`/`NCAcao` (models em `portal_api/models.py`) são upsertadas por código a cada importação, não descartadas depois de gerar um arquivo. 2. **Histórico completo de acompanhamentos**: `NCAcompanhamento` guarda TODAS as entradas de "Relato(s) do Acompanhamento" parseadas do `.xls`, não só a última (que é tudo que a skill original guardava). 3. **Status interno de tratativa da Qualidade** (`status_tratativa`, `pendente`/`tratado`) — não existe no Sigsistem. Fica `tratado` quando a Qualidade marca manualmente (`marcar-tratado`); **reabre sozinho** (`reabertura automática`) se uma importação nova trouxer algo diferente do que existia no momento do tratamento — ver `diff.py`. ## Pacote `portal_api/nao_conformidades/` (Python puro, sem ORM) | Arquivo | Conteúdo | |---|---| | `modelos.py` | Dataclasses `OcorrenciaExtraida`/`AcaoExtraida`/`AcompanhamentoExtraido`/`ResultadoProcessamento` — espelham os models Django campo a campo, sem nenhuma dependência de banco. | | `leiaute_ocorrencias.py` | Lê o `.xlsx` de ocorrências via `openpyxl` (sem pandas). `COLUNAS_ESPERADAS` lista as 36 colunas — se alguma faltar, vira aviso (não erro), pra tolerar pequenas mudanças de leiaute do Sigsistem. Filtra linhas com `Código da Ocorrência` não-numérico (lixo de fim de export). Detecta "Análise sem Ação" por LINHA (`tem_linha_analise_sem_acao`), não pelo agregado da ocorrência — uma ocorrência pode ter uma linha sem ação com análise preenchida mesmo já tendo outras ações abertas noutra linha. | | `leiaute_acoes.py` | Lê o `.xls` de ações — **é HTML, não Excel** (`encoding="latin-1"`, nunca abrir com `openpyxl`/leitor de planilha). Parseia por regex os blocos "Ocorrência N / Ação M" e a seção "Relato(s) do Acompanhamento" de cada um. `_separa_autor_texto()` é uma heurística (assume "DD/MM/AAAA - Nome - texto", nome ≤80 chars) — se o padrão não bater, `autor` fica vazio e `texto` guarda tudo, nunca perde informação. | | `classificacao.py` | `vencimento_efetivo()` (Prazo Prorrogado senão Data da Conclusão da Ação — só muda com importação nova, seguro cachear), `categoria_acao()` (Correção/Ação Corretiva/Outros), `status_prazo()`/`dias_para_vencer()` (**nunca persistidos** — dependem de "hoje", recalculados a cada leitura). | | `diff.py` | O núcleo da reabertura automática — ver seção própria abaixo. | | `pipeline.py` | `processa_importacao()` — só parsing/junção dos dois arquivos, não toca no banco. O upsert propriamente dito mora na view (`_aplica_upsert_nao_conformidades` em `portal_api/views.py`), mesma separação de responsabilidade de `planos_saude`/`indicadores`. | ### Por que sem pandas `planos_saude`/`indicadores` evitam pandas deliberadamente (parsing manual com `openpyxl`/`re`), apesar de `pandas` estar tecnicamente instalado no venv (dependência transitiva, provavelmente do `docling`). Este pacote segue o mesmo padrão — o volume real (amostra validada: 236 ocorrências, 290 ações, 929 entradas de acompanhamento) não justifica a complexidade de introduzir `pandas` como dependência direta de um pacote novo. ### Reabertura automática (`diff.py`) Cada `NCOcorrencia`/`NCAcao` tem `snapshot_tratativa` (`JSONField`) — o estado relevante congelado no momento em que foi marcada como `tratado` (`snapshot_ocorrencia()`/`snapshot_acao()`). A cada importação nova, se o item já estava `tratado`, `decide_reabertura_ocorrencia()`/`decide_reabertura_acao()` comparam o snapshot congelado contra o estado atual recém-extraído: - **Ação**: reabre se o **último acompanhamento tem data mais recente** que a congelada (comparação numérica de data, não de texto — robusto contra reformatação cosmética do export), ou se `situacao`/`fase`/`prazo_prorrogado` mudaram no Sigsistem. - **Ocorrência**: reabre se uma análise foi preenchida pela primeira vez, se o texto da análise mudou (hash de texto normalizado — espaços/capitalização não geram falso positivo, mas uma reformatação mais profunda do mesmo conteúdo pode gerar; risco residual documentado, não eliminado), ou se uma ação nova foi aberta. Quando reabre, `reaberto_em`/`reaberto_motivo` são preenchidos com o motivo textual (exibido na tela como selo "Reaberto" com tooltip) e `snapshot_tratativa` é limpo — o item volta a `pendente` até a Qualidade tratar de novo. A tratativa de fato (prorrogar/finalizar uma ação, decidir sobre uma análise) **acontece no Sigsistem**, sistema externo sem integração automática — o Portal só monitora e sinaliza; não há nenhuma escrita de volta pro Sigsistem. ### Upsert (`_aplica_upsert_nao_conformidades`, em `portal_api/views.py`) Fica na view, não no pacote puro (mesma separação de `planos_saude`/`indicadores`): `update_or_create` de `NCOcorrencia`/`NCAcao` por código, `get_or_create` de `NCAcompanhamento` deduplicado por `hash_entrada` (sha256 de data+autor+texto — idempotente entre reimportações do mesmo arquivo), e é aqui que a checagem de reabertura roda (só quando o item já estava `tratado`). Roda síncrono dentro de `transaction.atomic()`, mesmo padrão de `ImportacaoPlanoSaudeViewSet.create()`/`IndicadorApuracaoViewSet.create()` — volume real processa em bem menos de 1s. ## Models (`portal_api/models.py`) - **`NaoConformidadeImportacao`** — log de auditoria de cada upload (`arquivo_ocorrencias`/`arquivo_acoes`, `resumo` JSONField com contadores, `avisos`). Diferente de `ImportacaoPlanoSaude`, **não é "dona"** das ocorrências/ações — excluir uma importação (`DELETE`) remove só o log e os 2 arquivos de `MEDIA_ROOT`, nunca `NCOcorrencia`/`NCAcao` (entidades contínuas, upsertadas por código, sobrevivem à importação que as trouxe). - **`NCOcorrencia`** — upsert por `codigo` (único). Campos espelham as colunas do export + `status_tratativa`/`snapshot_tratativa`/`tratado_*`/`reaberto_*` (controle interno) + `analise_sem_acao_detectada` (persistido do parsing, não re-derivável de forma confiável só a partir de `descricao_analise`+`acoes.count()` porque uma ocorrência pode ter linhas mistas). - **`NCAcao`** — FK `NCOcorrencia`, upsert por `(ocorrencia, codigo)`. `vencimento_efetivo`/`ultimo_acompanhamento_em`/`ultimo_acompanhamento_eh_prorrogacao` são cache seguro (só mudam com importação nova); `status_tratativa`/`snapshot_tratativa`/`tratado_*`/`reaberto_*` iguais à ocorrência. - **`NCAcompanhamento`** — FK `NCAcao`, append-only (`UniqueConstraint(acao, hash_entrada)` deduplica reimportações, nunca apaga uma entrada já persistida). `assunto` é `TextField`, não `CharField` — export real trouxe valores com mais de 390 caracteres (texto livre do Sigsistem, sem limite aparente). ## API (`portal_api/views.py`/`urls.py`) Todos os endpoints sob `PermissaoApp("relatorios", "nao-conformidades")` (toggle único). | Endpoint | Método | Uso | |---|---|---| | `/api/nao-conformidades/importacoes/` | GET/POST | histórico + nova importação (multipart, 2 arquivos); roda o pipeline síncrono + upsert, devolve o `resumo` | | `/api/nao-conformidades/importacoes/{id}/` | DELETE | remove só o log + os 2 arquivos | | `/api/nao-conformidades/ocorrencias/` | GET | filtros `?sem_analise=true`, `?analise_sem_acao=true`, `?status_tratativa=`, `?search=` | | `/api/nao-conformidades/ocorrencias/{id}/marcar-tratado/`, `/reabrir/` | POST | congela snapshot / reabre manualmente | | `/api/nao-conformidades/acoes/` | GET | filtros `?status_tratativa=`, `?status_prazo=` (`vencida`/`vence_7_dias`/`vence_30_dias`/`no_prazo`/`sem_vencimento`, replica `classificacao.status_prazo()` como filtro de data via `_filtra_status_prazo`), `?search=`; inclui `status_prazo`/`status_prazo_label`/`dias_para_vencer`/`ocorrencia_area`/`ocorrencia_clientes` calculados no serializer (nunca persistidos) | | `/api/nao-conformidades/acoes/{id}/` | GET | detalhe com `acompanhamentos` completo (histórico) | | `/api/nao-conformidades/acoes/{id}/marcar-tratado/`, `/reabrir/` | POST | idem ocorrência | | `/api/nao-conformidades/dashboard/` | GET | `?meses=` (default 6) ou `?data_inicio=&data_fim=`; indicadores "abertos agora" (nunca filtrados por período) + `tempos_medios` (2 KPIs, sobre todo o histórico, nunca filtrados por período) + rankings de motivo (`tipos_causa`)/cliente (`clientes_relacionados`)/colaborador (`indicado_analise`) filtrados pelo período — clientes/colaboradores vêm quebrados por tipo (`{nome, nc, reclamacao, outros, total}`) | ## Frontend (`templates/nao-conformidades.html`, `static/{css,js}/nao-conformidades.js`) Uma página com 3 abas (`.pa-tabs`, reaproveitado de `perfis-acesso.css`): **Dashboard** (default — cards, KPIs de tempo, seletor de período em chips, 3 rankings), **Gestão** (Sem Análise/Análise sem Ação/Ações) e **Importar** (histórico + upload). Gestão/Importar carregam sob demanda (lazy, só na primeira vez que a aba é aberta ou via drill-down do Dashboard). - **Drill-down Dashboard → Gestão**: os 4 cards com lista equivalente ("Ações vencidas", "Vence em ≤7 dias", "Ocorrências sem análise", "Análises sem ação") e as linhas dos 3 rankings são clicáveis — trocam pra Gestão já filtrada/rolada até a seção certa. Ver `irParaAcoesComPrazo`/`irParaSecaoGestao`/`irParaGestaoComBusca` em `nao-conformidades.js`. **Cuidado com corrida**: `ativarTab()` é **só UI** (nunca dispara carregamento) de propósito — um bug real já aconteceu aqui quando ela tinha esse efeito colateral e corria em paralelo com o carregamento explícito do drill-down, e a chamada que terminasse por último vencia (às vezes sobrescrevendo o filtro certo). Qualquer novo caminho que leve à Gestão deve sempre `await` o carregamento, nunca disparar dois carregamentos concorrentes da mesma lista. - **Filtro combinável de Ações**: duas fileiras de chips independentes, Prazo (`?status_prazo=`) e Tratativa (`?status_tratativa=`), combinam com AND (dois `