portal_publico/portal_api/nao_conformidades/CLAUDE.md

18 KiB

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=<bucket> (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 (4 KPIs + quebra por categoria, sobre todo o histórico, nunca filtrados por período — ver "Tempos médios" abaixo) + 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})

Tempos médios (nao_conformidades_dashboard_view, tempos_medios)

4 cards (.ncf-kpi, mesmo componente visual desde a rodada 89 — decisão explícita do usuário de manter o card simples em vez de um funil visual com setas, que chegou a ser implementado e foi revertido na mesma rodada): preenchimento da análise, abertura da ação, execução da ação (abertura → finalização) e ciclo completo (emissão da ocorrência → finalização da ação) — não é a soma dos 3 anteriores, porque cada etapa tem sua própria amostra (n) e nem toda ocorrência percorre todas as etapas até aqui. A etapa de execução (execucao_acao_dias/n) só existe desde que a planilha de ocorrências passou a trazer ações já finalizadas (Data de Finalização/Dias para Finalização, colunas que a exportação padrão do Sigsistem não preenche — só um export "com finalizadas" as traz, ver histórico do pacote); antes disso só os 2 primeiros KPIs tinham dado.

  • Cada média vem acompanhada do tamanho da amostra (*_n) — sem isso um número como "62 dias" parece mais sólido do que é, já que a amostra de quem já finalizou é normalmente bem menor que o total de ocorrências.
  • execucao_acao_dias/n é calculado só sobre NCAcao com data_finalizacao e data_emissao preenchidas (Avg(data_finalizacao - data_emissao) — confirmado que bate exatamente com o Dias para Finalização que o próprio Sigsistem calcula, então não precisa usar esse campo cru, só as datas). Não filtra por situacao ("Eficaz"/"Ineficaz") — os dois indicam que a ação foi concluída e avaliada, então os dois contam pro tempo de execução.
  • Quebrado por categoria (execucao_por_categoria, via classificacao.categoria_acao()) porque a média geral esconde uma variância enorme — na amostra real (import "com finalizadas", 3187 ações), de 1 a quase 800 dias, média 62. Correção/Ação Corretiva/Outros têm perfis de duração bem diferentes; misturar tudo numa média só seria enganoso.
  • Cálculo em Python (views.py, não uma annotate agrupada por categoria no ORM), porque categoria_acao() é uma heurística de texto ("corretiva" in tipo.lower() etc.) que não dá pra expressar como CASE/Q sem duplicar a lógica — o volume real (milhares de ações, não milhões) não justifica a complexidade de fazer isso no banco.

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 <select> de fato seriam mais familiares, mas o padrão chip já é o mesmo do período do Dashboard). A tabela mostra as duas dimensões em colunas separadas ("Prazo" e "Tratativa") — nunca juntar num "Status" só, é exatamente essa mistura que gerou confusão antes.
  • Expandir linha (pidNcfCriarLinhaExpandivel, mesmo padrão de .cc-painel-fiscal__toggle de custo-contratacao.css/.js — chevron que gira via [aria-expanded="true"] svg{transform:rotate(90deg)}) é o único mecanismo de "ver mais" das 3 listas: ocorrência (relato completo, causa, cliente, riscos etc. — parte já vem da própria listagem, o resto é buscado lazy em GET /ocorrencias/{id}/) e ação (texto completo + histórico de acompanhamentos, mais recente primeiro, 3 visíveis + "Mostrar mais", buscado lazy em GET /acoes/{id}/). Não usar modal pra isso — o modal de histórico existiu numa rodada anterior e foi removido a pedido do usuário (ficava colado no botão "Marcar como tratado").
  • Busca compartilhada da Gestão (#ncf-gestao-search): filtra as 3 listas ao mesmo tempo, sempre client-side sobre os dados já carregados (sem round-trip) — colaborador/responsável/emissor/cliente/assunto.

Reaproveita .status-pill (definido em perfis-acesso.css) com modificadores novos (--ok/--danger/--warning/--neutral, em nao-conformidades.css) em vez de inventar um componente de badge próprio. Chips de filtro/período (.ncf-chip/.ncf-chip-row) duplicam o visual de .ind-departamento-chip (indicador-desempenho.css) porque essa folha não é carregada nesta página.

Cuidado com especificidade CSS: .pa-table td:last-child{text-align:right} (de perfis-acesso.css) pega qualquer <td> que seja a única célula de uma linha — inclusive a célula do painel de detalhe (que usa colspan, então é sempre "a última"). Um bug real de alinhamento já aconteceu por isso; a correção foi dar à célula do painel uma classe própria com especificidade maior (.pa-table td.ncf-detail-cell), não confiar em .ncf-detail-row td (mesma especificidade da regra original, perderia).

Permissão e menu

catalogo.py: subgrupo qualidade dentro de MODULE_APPS["relatorios"], com uma única tool nao-conformidades — mesmo formato de subgrupo com toggle único já usado em Auditorias (consultoria-tributaria → controle-simples-nacional). Restrito por padrão: como relatorios está em SECTORAL_KEYS (herdaria True automático pra todo perfil setorial), seed_portal.py força permissoes["relatorios"]["apps"]["nao-conformidades"] = False pra todo perfil que não seja "Integração e Inovação" (código 8) — liberar outros perfis é manual, pela tela de Perfis de Acesso.

Riscos conhecidos

  1. Falso positivo na reabertura de ocorrência por texto: reformatação profunda (não cosmética) do mesmo conteúdo de análise pelo Sigsistem pode gerar hash diferente e reabrir sem necessidade real. Não validado contra múltiplos exports reais consecutivos da mesma ocorrência tratada — se acontecer na prática, considerar comparação mais tolerante (ex.: similaridade de texto) em vez de hash exato.
  2. NCOcorrencia.codigo/NCAcao.codigo como PositiveIntegerField: assumido do comportamento do parser de referência, validado só contra a amostra de 236 ocorrências/290 ações. Se um export real trouxer código não-numérico, a linha é descartada silenciosamente (mesmo comportamento de "linha inválida" da skill original) — considerar CharField se isso acontecer.
  3. Upload síncrono: aceitável no volume validado (processa em bem menos de 1s); se o Sigsistem exportar volumes muito maiores no futuro, reavaliar fila assíncrona (nenhum outro pacote do Portal usa hoje).