Compare commits
1 Commits
main
...
feat/sgmp-
| Author | SHA1 | Date |
|---|---|---|
|
|
3ae6856463 |
|
|
@ -1,13 +1,21 @@
|
|||
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, UsuarioSistema
|
||||
from .models import (
|
||||
ConfiguracaoSGMP,
|
||||
HeadGestor,
|
||||
PessoaRM,
|
||||
Solicitacao,
|
||||
UsuarioPerfilExtra,
|
||||
UsuarioSistema,
|
||||
)
|
||||
|
||||
|
||||
class ConfiguracaoSGMPForm(forms.ModelForm):
|
||||
|
|
@ -207,12 +215,43 @@ 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",
|
||||
)
|
||||
|
|
@ -232,6 +271,8 @@ class UsuarioSistemaAdmin(admin.ModelAdmin):
|
|||
"atualizado_em",
|
||||
)
|
||||
|
||||
inlines = (UsuarioPerfilExtraInline,)
|
||||
|
||||
fieldsets = (
|
||||
("Informações Básicas", {
|
||||
"fields": ("matricula", "nome", "ativo")
|
||||
|
|
@ -245,6 +286,25 @@ 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")
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def usuario_sistema(request):
|
|||
usuario_eh_gestor or usuario_eh_admin or usuario_eh_diretoria
|
||||
),
|
||||
"usuario_pode_ver_todas_solicitacoes": (
|
||||
usuario.perfil != UsuarioSistema.Perfil.GESTOR or usuario_eh_admin
|
||||
not usuario.eh_apenas_gestor()
|
||||
),
|
||||
"usuario_pode_gerenciar_permissoes": usuario_pode_gerenciar_permissoes(
|
||||
usuario
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ def get_sqlserver_connection():
|
|||
database=settings.SQLSERVER_CONFIG["DATABASE"],
|
||||
)
|
||||
|
||||
|
||||
def listar_para_selecionar_colaborador(apenas_desligados=False):
|
||||
"""
|
||||
Lista colaboradores do RM para seleção.
|
||||
|
|
|
|||
|
|
@ -140,6 +140,25 @@ 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})"
|
||||
|
||||
|
|
|
|||
|
|
@ -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, UsuarioSistema
|
||||
from ..models import ConfiguracaoSGMP, UsuarioPerfilExtra, UsuarioSistema
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
|
@ -91,3 +91,29 @@ 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())
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ urlpatterns = [
|
|||
views.todas_solicitacoes_view,
|
||||
name="todas_solicitacoes",
|
||||
),
|
||||
path(
|
||||
"solicitacoes/todas/exportar.xlsx",
|
||||
views.exportar_todas_solicitacoes_xlsx,
|
||||
name="exportar_todas_solicitacoes_xlsx",
|
||||
),
|
||||
|
||||
# =========================
|
||||
# DESLIGAMENTO
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
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
|
||||
|
|
@ -10,7 +13,9 @@ 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,
|
||||
|
|
@ -41,8 +46,9 @@ def _queryset_dashboard_solicitacoes(usuario: UsuarioSistema):
|
|||
"""
|
||||
Queryset visível no dashboard para o usuário (antes de order_by/paginação).
|
||||
|
||||
Usa :meth:`UsuarioSistema.tem_perfil` (perfil principal + ``UsuarioPerfilExtra``),
|
||||
não apenas ``usuario.perfil``, para alinhar com perfis secundários.
|
||||
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.
|
||||
|
||||
**Precedência** (primeiro ramo que se aplica; papéis operacionais antes de GESTOR):
|
||||
1. ADMIN — todas as solicitações
|
||||
|
|
@ -55,36 +61,36 @@ def _queryset_dashboard_solicitacoes(usuario: UsuarioSistema):
|
|||
Total e pendentes no dashboard devem usar ``.count()`` sobre este mesmo queryset
|
||||
(pendentes = excluir FINALIZADA e REPROVADA).
|
||||
"""
|
||||
if usuario.tem_perfil(UsuarioSistema.Perfil.ADMIN):
|
||||
perfil_principal = usuario.perfil
|
||||
if perfil_principal == UsuarioSistema.Perfil.ADMIN:
|
||||
return Solicitacao.objects.all()
|
||||
if usuario.tem_perfil(UsuarioSistema.Perfil.GG) or usuario.tem_perfil(
|
||||
UsuarioSistema.Perfil.CONTROLADORIA
|
||||
if perfil_principal in (
|
||||
UsuarioSistema.Perfil.GG,
|
||||
UsuarioSistema.Perfil.CONTROLADORIA,
|
||||
):
|
||||
qs_fila = Solicitacao.objects.filter(status=StatusSolicitacao.ENVIADA)
|
||||
if usuario.tem_perfil(UsuarioSistema.Perfil.GG) and not usuario.tem_perfil(
|
||||
UsuarioSistema.Perfil.CONTROLADORIA
|
||||
):
|
||||
if perfil_principal == UsuarioSistema.Perfil.GG:
|
||||
qs_fila = qs_fila.exclude(
|
||||
pareceres__etapa=EtapaAprovacao.GG, pareceres__usuario=usuario
|
||||
)
|
||||
elif usuario.tem_perfil(UsuarioSistema.Perfil.CONTROLADORIA) and not usuario.tem_perfil(
|
||||
UsuarioSistema.Perfil.GG
|
||||
):
|
||||
elif perfil_principal == UsuarioSistema.Perfil.CONTROLADORIA:
|
||||
qs_fila = qs_fila.exclude(
|
||||
pareceres__etapa=EtapaAprovacao.CONTROLADORIA,
|
||||
pareceres__usuario=usuario,
|
||||
)
|
||||
return _qs_dashboard_unir_fila_com_minhas(qs_fila, usuario)
|
||||
if usuario.tem_perfil(UsuarioSistema.Perfil.HEAD):
|
||||
if perfil_principal == 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 usuario.tem_perfil(UsuarioSistema.Perfil.DIRETORIA):
|
||||
if perfil_principal == 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 usuario.tem_perfil(UsuarioSistema.Perfil.GESTOR):
|
||||
if perfil_principal == UsuarioSistema.Perfil.GESTOR:
|
||||
return Solicitacao.objects.filter(solicitante=usuario)
|
||||
return Solicitacao.objects.none()
|
||||
|
||||
|
|
@ -778,23 +784,10 @@ 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.tem_perfil(UsuarioSistema.Perfil.GESTOR) and not usuario.tem_perfil(UsuarioSistema.Perfil.ADMIN):
|
||||
if usuario.eh_apenas_gestor():
|
||||
return redirect("solicitacoes:dashboard")
|
||||
|
||||
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)
|
||||
qs_base, filtro_status, busca = _build_solicitacoes_queryset(request, usuario)
|
||||
|
||||
total = qs_base.count()
|
||||
|
||||
|
|
@ -842,15 +835,116 @@ 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue