Compare commits

..

No commits in common. "feat/sgmp-solicitacoes-only" and "main" have entirely different histories.

7 changed files with 34 additions and 239 deletions

View File

@ -1,21 +1,13 @@
from django import forms
from django.contrib import admin, messages
from django.core.exceptions import ValidationError
from django.forms import BaseInlineFormSet
from django.shortcuts import redirect
from django.urls import path, reverse
from django.utils import timezone
from .intf_sqlserver import listar_para_selecionar_colaborador
from .intf_winthor import buscar_colaborador_oracle
from .models import (
ConfiguracaoSGMP,
HeadGestor,
PessoaRM,
Solicitacao,
UsuarioPerfilExtra,
UsuarioSistema,
)
from .models import ConfiguracaoSGMP, HeadGestor, PessoaRM, Solicitacao, UsuarioSistema
class ConfiguracaoSGMPForm(forms.ModelForm):
@ -215,43 +207,12 @@ class PessoaRMAdmin(admin.ModelAdmin):
return redirect("..")
class UsuarioPerfilExtraInlineFormSet(BaseInlineFormSet):
def clean(self):
super().clean()
vistos = set()
principal = self.data.get("perfil") or getattr(self.instance, "perfil", None)
for form in self.forms:
if not hasattr(form, "cleaned_data"):
continue
if form.cleaned_data.get("DELETE"):
continue
perfil_extra = form.cleaned_data.get("perfil")
if not perfil_extra:
continue
if perfil_extra == principal:
raise ValidationError(
"Perfil extra não pode ser igual ao perfil principal."
)
if perfil_extra in vistos:
raise ValidationError(f"Perfil extra duplicado: {perfil_extra}.")
vistos.add(perfil_extra)
class UsuarioPerfilExtraInline(admin.TabularInline):
model = UsuarioPerfilExtra
formset = UsuarioPerfilExtraInlineFormSet
extra = 0
fields = ("perfil", "criado_em")
readonly_fields = ("criado_em",)
@admin.register(UsuarioSistema)
class UsuarioSistemaAdmin(admin.ModelAdmin):
list_display = (
"nome",
"matricula",
"perfil",
"perfis_ativos_display",
"ativo",
"criado_em",
)
@ -271,8 +232,6 @@ class UsuarioSistemaAdmin(admin.ModelAdmin):
"atualizado_em",
)
inlines = (UsuarioPerfilExtraInline,)
fieldsets = (
("Informações Básicas", {
"fields": ("matricula", "nome", "ativo")
@ -286,25 +245,6 @@ class UsuarioSistemaAdmin(admin.ModelAdmin):
}),
)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("perfis_extras")
@admin.display(description="Perfis ativos")
def perfis_ativos_display(self, obj):
codigos = obj.perfis_ativos()
labels = dict(UsuarioSistema.Perfil.choices)
resolved = [str(labels.get(c, c)) for c in codigos]
return ", ".join(resolved)
@admin.register(UsuarioPerfilExtra)
class UsuarioPerfilExtraAdmin(admin.ModelAdmin):
list_display = ("usuario", "perfil", "criado_em")
list_filter = ("perfil",)
search_fields = ("usuario__nome", "usuario__matricula")
autocomplete_fields = ("usuario",)
@admin.register(HeadGestor)
class HeadGestorAdmin(admin.ModelAdmin):
list_display = ("head", "gestor")

View File

@ -32,7 +32,7 @@ def usuario_sistema(request):
usuario_eh_gestor or usuario_eh_admin or usuario_eh_diretoria
),
"usuario_pode_ver_todas_solicitacoes": (
not usuario.eh_apenas_gestor()
usuario.perfil != UsuarioSistema.Perfil.GESTOR or usuario_eh_admin
),
"usuario_pode_gerenciar_permissoes": usuario_pode_gerenciar_permissoes(
usuario

View File

@ -18,7 +18,6 @@ def get_sqlserver_connection():
database=settings.SQLSERVER_CONFIG["DATABASE"],
)
def listar_para_selecionar_colaborador(apenas_desligados=False):
"""
Lista colaboradores do RM para seleção.

View File

@ -140,25 +140,6 @@ class UsuarioSistema(BaseModel):
# Garante que o principal venha primeiro e não seja duplicado
return [self.perfil] + [p for p in extras if p != self.perfil]
def eh_apenas_gestor(self) -> bool:
"""
Verdadeiro quando o usuário possui somente capacidades de gestor.
Útil para regras que restringem funcionalidades administrativas
(ex.: listagem "todas as solicitações") sem bloquear usuários que
acumulam gestor + outro perfil operacional.
"""
perfis_nao_gestor = [
self.Perfil.ADMIN,
self.Perfil.GG,
self.Perfil.CONTROLADORIA,
self.Perfil.HEAD,
self.Perfil.DIRETORIA,
]
return self.tem_perfil(self.Perfil.GESTOR) and not any(
self.tem_perfil(perfil) for perfil in perfis_nao_gestor
)
def __str__(self):
return f"{self.nome} ({self.matricula})"

View File

@ -3,7 +3,7 @@ from django.test import Client, TestCase
from django.urls import reverse
from ..acesso import usuario_pode_gerenciar_permissoes
from ..models import ConfiguracaoSGMP, UsuarioPerfilExtra, UsuarioSistema
from ..models import ConfiguracaoSGMP, UsuarioSistema
User = get_user_model()
@ -91,29 +91,3 @@ class GerenciarPermissoesViewTests(TestCase):
self.client.login(username="20", password="secret")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
class TodasSolicitacoesPerfilCombinadoTests(TestCase):
def setUp(self):
self.client = Client()
self.url = reverse("solicitacoes:todas_solicitacoes")
User.objects.create_user(username="30", password="secret")
self.usuario = UsuarioSistema.objects.create(
matricula="30",
nome="GG com perfil Gestor",
perfil=UsuarioSistema.Perfil.GG,
ativo=True,
)
UsuarioPerfilExtra.objects.create(
usuario=self.usuario,
perfil=UsuarioSistema.Perfil.GESTOR,
)
def test_usuario_com_gg_e_gestor_acessa_todas_solicitacoes(self):
self.client.login(username="30", password="secret")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_eh_apenas_gestor_false_quando_ha_perfil_operacional(self):
self.assertFalse(self.usuario.eh_apenas_gestor())

View File

@ -25,11 +25,6 @@ urlpatterns = [
views.todas_solicitacoes_view,
name="todas_solicitacoes",
),
path(
"solicitacoes/todas/exportar.xlsx",
views.exportar_todas_solicitacoes_xlsx,
name="exportar_todas_solicitacoes_xlsx",
),
# =========================
# DESLIGAMENTO

View File

@ -2,10 +2,7 @@
import json
import logging
import time
import uuid
from datetime import date
from io import BytesIO
from urllib.parse import urlencode
from django.contrib.auth import login, logout, get_user_model
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@ -13,9 +10,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.http import HttpResponse
from django.utils import timezone
from django.core.paginator import Paginator
from django.db.models import Q
from django.template.loader import render_to_string
from openpyxl import Workbook
from .models import (
HeadGestor,
PessoaRM,
@ -46,9 +41,8 @@ def _queryset_dashboard_solicitacoes(usuario: UsuarioSistema):
"""
Queryset visível no dashboard para o usuário (antes de order_by/paginação).
A seleção do ramo principal é baseada em ``usuario.perfil`` para preservar
precedência do perfil principal. Perfis extras são considerados apenas
como capacidades complementares dentro de cada ramo quando necessário.
Usa :meth:`UsuarioSistema.tem_perfil` (perfil principal + ``UsuarioPerfilExtra``),
não apenas ``usuario.perfil``, para alinhar com perfis secundários.
**Precedência** (primeiro ramo que se aplica; papéis operacionais antes de GESTOR):
1. ADMIN todas as solicitações
@ -61,36 +55,36 @@ def _queryset_dashboard_solicitacoes(usuario: UsuarioSistema):
Total e pendentes no dashboard devem usar ``.count()`` sobre este mesmo queryset
(pendentes = excluir FINALIZADA e REPROVADA).
"""
perfil_principal = usuario.perfil
if perfil_principal == UsuarioSistema.Perfil.ADMIN:
if usuario.tem_perfil(UsuarioSistema.Perfil.ADMIN):
return Solicitacao.objects.all()
if perfil_principal in (
UsuarioSistema.Perfil.GG,
UsuarioSistema.Perfil.CONTROLADORIA,
if usuario.tem_perfil(UsuarioSistema.Perfil.GG) or usuario.tem_perfil(
UsuarioSistema.Perfil.CONTROLADORIA
):
qs_fila = Solicitacao.objects.filter(status=StatusSolicitacao.ENVIADA)
if perfil_principal == UsuarioSistema.Perfil.GG:
if usuario.tem_perfil(UsuarioSistema.Perfil.GG) and not usuario.tem_perfil(
UsuarioSistema.Perfil.CONTROLADORIA
):
qs_fila = qs_fila.exclude(
pareceres__etapa=EtapaAprovacao.GG, pareceres__usuario=usuario
)
elif perfil_principal == UsuarioSistema.Perfil.CONTROLADORIA:
elif usuario.tem_perfil(UsuarioSistema.Perfil.CONTROLADORIA) and not usuario.tem_perfil(
UsuarioSistema.Perfil.GG
):
qs_fila = qs_fila.exclude(
pareceres__etapa=EtapaAprovacao.CONTROLADORIA,
pareceres__usuario=usuario,
)
return _qs_dashboard_unir_fila_com_minhas(qs_fila, usuario)
if perfil_principal == UsuarioSistema.Perfil.HEAD:
if usuario.tem_perfil(UsuarioSistema.Perfil.HEAD):
matriculas = matriculas_gestores_do_head(usuario)
qs_fila = Solicitacao.objects.filter(status=StatusSolicitacao.AGUARDANDO_HEAD)
if matriculas:
qs_fila = qs_fila.filter(solicitante__matricula__in=matriculas)
return _qs_dashboard_unir_fila_com_minhas(qs_fila, usuario)
if perfil_principal == UsuarioSistema.Perfil.DIRETORIA:
if usuario.tem_perfil(UsuarioSistema.Perfil.DIRETORIA):
qs_fila = Solicitacao.objects.filter(status=StatusSolicitacao.AGUARDANDO_DIRETORIA)
if usuario.perfis_extras.filter(perfil=UsuarioSistema.Perfil.HEAD).exists():
qs_fila = qs_fila | Solicitacao.objects.filter(status=StatusSolicitacao.AGUARDANDO_HEAD)
return _qs_dashboard_unir_fila_com_minhas(qs_fila, usuario)
if perfil_principal == UsuarioSistema.Perfil.GESTOR:
if usuario.tem_perfil(UsuarioSistema.Perfil.GESTOR):
return Solicitacao.objects.filter(solicitante=usuario)
return Solicitacao.objects.none()
@ -784,10 +778,23 @@ def dashboard_view(request):
def todas_solicitacoes_view(request):
"""Listagem de solicitações: Gestor não acessa; Head vê só dos gestores vinculados a ele; GG/Controladoria/Diretoria veem todas."""
usuario = get_usuario_sistema(request)
if usuario.eh_apenas_gestor():
if usuario.tem_perfil(UsuarioSistema.Perfil.GESTOR) and not usuario.tem_perfil(UsuarioSistema.Perfil.ADMIN):
return redirect("solicitacoes:dashboard")
qs_base, filtro_status, busca = _build_solicitacoes_queryset(request, usuario)
qs_base = Solicitacao.objects.all().order_by("-criado_em")
# Head: apenas solicitações dos gestores que ele aprova (subordinados imediatos)
if usuario.tem_perfil(UsuarioSistema.Perfil.HEAD) and not usuario.tem_perfil(UsuarioSistema.Perfil.ADMIN):
matriculas = matriculas_gestores_do_head(usuario)
if matriculas:
qs_base = qs_base.filter(solicitante__matricula__in=matriculas)
else:
qs_base = qs_base.none()
# Filtro por status (GET)
filtro_status = request.GET.get("status", "").strip()
if filtro_status:
qs_base = qs_base.filter(status=filtro_status)
total = qs_base.count()
@ -835,116 +842,15 @@ def todas_solicitacoes_view(request):
"is_solicitante": is_solicitante,
"dados_winthor_organizados": dados_winthor_organizados,
})
_exp_params = {}
if filtro_status:
_exp_params["status"] = filtro_status
if busca:
_exp_params["q"] = busca
export_query = ("?" + urlencode(_exp_params)) if _exp_params else ""
return render(request, "solicitacoes/todas_solicitacoes.html", {
"solicitacoes": solicitacoes_page,
"solicitacoes_com_acao": solicitacoes_com_acao,
"total": total,
"filtro_status": filtro_status,
"busca": busca,
"export_query": export_query,
"status_choices": StatusSolicitacao.choices,
})
def _build_solicitacoes_queryset(request, usuario: UsuarioSistema):
"""Queryset base para listagem/exportação da tela 'Todas as Solicitações'."""
qs_base = Solicitacao.objects.all().order_by("-criado_em")
# Head: apenas solicitações dos gestores que ele aprova (subordinados imediatos)
if usuario.tem_perfil(UsuarioSistema.Perfil.HEAD) and not usuario.tem_perfil(
UsuarioSistema.Perfil.ADMIN
):
matriculas = matriculas_gestores_do_head(usuario)
if matriculas:
qs_base = qs_base.filter(solicitante__matricula__in=matriculas)
else:
qs_base = qs_base.none()
filtro_status = request.GET.get("status", "").strip()
if filtro_status:
qs_base = qs_base.filter(status=filtro_status)
busca = request.GET.get("q", "").strip()
if busca:
q_filter = (
Q(funcionario__nome__icontains=busca)
| Q(solicitante__nome__icontains=busca)
| Q(tipo__icontains=busca)
)
if len(busca) == 36:
try:
q_filter |= Q(id=uuid.UUID(busca))
except ValueError:
pass
qs_base = qs_base.filter(q_filter)
return qs_base, filtro_status, busca
@login_required
def exportar_todas_solicitacoes_xlsx(request):
"""Exporta todas as solicitações visíveis ao usuário com os filtros atuais."""
usuario = get_usuario_sistema(request)
if usuario.eh_apenas_gestor():
return redirect("solicitacoes:dashboard")
qs_base, filtro_status, busca = _build_solicitacoes_queryset(request, usuario)
wb = Workbook()
ws = wb.active
ws.title = "Solicitacoes"
ws.append(
[
"ID",
"Tipo",
"Colaborador",
"Solicitante",
"Status",
"Criada em",
]
)
def excel_text(value):
if value is None:
return ""
return str(value)
for solicitacao in qs_base.iterator():
ws.append(
[
excel_text(solicitacao.id),
excel_text(solicitacao.get_tipo_display()),
excel_text(
solicitacao.funcionario.nome if solicitacao.funcionario else ""
),
excel_text(solicitacao.solicitante.nome),
excel_text(solicitacao.get_status_display_para_usuario(usuario)),
excel_text(
timezone.localtime(solicitacao.criado_em).strftime("%d/%m/%Y %H:%M")
),
]
)
output = BytesIO()
wb.save(output)
output.seek(0)
suffix = f"_{filtro_status}" if filtro_status else ""
filename = f"solicitacoes_todas{suffix}.xlsx"
response = HttpResponse(
output.getvalue(),
content_type=(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
),
)
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
@login_required
@pode_criar_solicitacao