portal_publico/portal_api/nao_conformidades/classificacao.py
2026-09-09 09:57:46 -03:00

92 lines
3.6 KiB
Python

"""Regras de classificação de vencimento/prazo/categoria de ação — porta as
regras de negócio da skill "analise-ncs" (ver
`projects/Controladoria - Analise NCs/analise-ncs_SKILL.md`).
IMPORTANTE: `status_prazo()`/`dias_para_vencer()` dependem da data de hoje —
NUNCA persistir o resultado no banco, sempre calcular em tempo de leitura
(serializer/dashboard). Só `vencimento_efetivo()` é seguro cachear num model
(só muda com uma importação nova)."""
from __future__ import annotations
from datetime import date
STATUS_FINALIZADA = "finalizada"
STATUS_VENCIDA = "vencida"
STATUS_VENCE_7 = "vence_7_dias"
STATUS_VENCE_30 = "vence_30_dias"
STATUS_NO_PRAZO = "no_prazo"
STATUS_SEM_VENCIMENTO = "sem_vencimento"
# Não é uma classificação por-ação real (status_prazo() nunca retorna isto) —
# é só um nome de bucket agregado usado por `_filtra_status_prazo()`
# (portal_api/views.py) e pelo chip "Em aberto" do frontend, equivalente a
# "qualquer bucket exceto finalizada". NUNCA adicionar em STATUS_PRAZO_LABELS:
# esse dict é iterado pra montar os contadores do dashboard, uma contagem por
# classificação mutuamente exclusiva de ação — "não finalizada" não é
# mutuamente exclusiva com vencida/vence_7_dias/etc, é a união delas.
STATUS_NAO_FINALIZADA = "nao_finalizada"
STATUS_PRAZO_LABELS = {
STATUS_FINALIZADA: "Finalizada",
STATUS_VENCIDA: "Vencida",
STATUS_VENCE_7: "Vence em ≤7 dias",
STATUS_VENCE_30: "Vence em ≤30 dias",
STATUS_NO_PRAZO: "No prazo",
STATUS_SEM_VENCIMENTO: "Sem vencimento",
}
# Valor de "Fase" (coluna do Sigsistem, texto livre) que indica que o item
# está fechado — usado tanto pra NCAcao.fase quanto pra NCOcorrencia.fase
# (ocorrência fechada sem precisar de ação, ex.: "Elogios de Clientes").
FASE_FINALIZADA = "Finalizada"
CATEGORIA_CORRETIVA = "Ação Corretiva"
CATEGORIA_CORRECAO = "Correção"
CATEGORIA_OUTROS = "Outros"
# Tipos de ocorrência que exigem Correção + Ação Corretiva (regra #9 da skill).
TIPOS_NC = ("Não Conformidade", "Reclamação de Cliente")
def vencimento_efetivo(prazo_prorrogado: date | None, data_conclusao: date | None) -> date | None:
"""Prazo Prorrogado se preenchido, senão Data da Conclusão da Ação (que no
export é a data-limite prevista, não a conclusão real)."""
return prazo_prorrogado or data_conclusao
def categoria_acao(tipo_acao: str) -> str:
tl = (tipo_acao or "").lower()
if "corretiva" in tl:
return CATEGORIA_CORRETIVA
if "correç" in tl or "correc" in tl:
return CATEGORIA_CORRECAO
return CATEGORIA_OUTROS
def status_prazo(venc: date | None, hoje: date, finalizada: bool = False) -> str:
"""`finalizada=True` (ação com "Data de Finalização" preenchida no
Sigsistem) sempre vence qualquer outra classificação — uma ação já
concluída nunca é "vencida"/"vence em X dias", mesmo que o prazo dela já
tenha passado há muito tempo (comum em export com histórico: o
`vencimento_efetivo` congelado continua no passado depois de concluída).
Bug real corrigido: antes disso, reimportar um export com histórico de
ações já finalizadas fazia a maioria aparecer como "vencida"."""
if finalizada:
return STATUS_FINALIZADA
if venc is None:
return STATUS_SEM_VENCIMENTO
if venc < hoje:
return STATUS_VENCIDA
dias = (venc - hoje).days
if dias <= 7:
return STATUS_VENCE_7
if dias <= 30:
return STATUS_VENCE_30
return STATUS_NO_PRAZO
def dias_para_vencer(venc: date | None, hoje: date) -> int | None:
if venc is None:
return None
return (venc - hoje).days