diff --git a/sigi/apps/eventos/admin.py b/sigi/apps/eventos/admin.py index fb65a57..f56c99d 100644 --- a/sigi/apps/eventos/admin.py +++ b/sigi/apps/eventos/admin.py @@ -5,6 +5,7 @@ from moodle import Moodle from django.db.models import Q from django.conf import settings from django.contrib import admin, messages +from django.core.exceptions import ValidationError from django.http import HttpResponse from django.shortcuts import get_object_or_404, render, redirect from django.template import Template, Context @@ -25,6 +26,7 @@ from sigi.apps.eventos.models import ( Modulo, TipoEvento, Solicitacao, + AnexoSolicitacao, ItemSolicitado, Funcao, Evento, @@ -42,54 +44,7 @@ from sigi.apps.utils.mixins import ( ) -class SolicitacaoStatusFilter(admin.SimpleListFilter): - title = _("status") - parameter_name = "status" - - def lookups(self, request, model_admin): - return ( - ("aberto", _("Aberto")), - ("analise", _("Análise")), - ("inconcluso", _("Inconcluso (aberto + análise)")), - ("concluido", _("Concluído")), - ) - - def queryset(self, request, queryset): - if self.value() == "aberto": - return queryset.exclude( - itemsolicitado__status__in=[ - ItemSolicitado.STATUS_AUTORIZADO, - ItemSolicitado.STATUS_REJEITADO, - ] - ).distinct() - elif self.value() == "analise": - return ( - queryset.filter( - itemsolicitado__status=ItemSolicitado.STATUS_SOLICITADO - ) - .filter( - itemsolicitado__status__in=[ - ItemSolicitado.STATUS_AUTORIZADO, - ItemSolicitado.STATUS_REJEITADO, - ] - ) - .distinct() - ) - elif self.value() == "inconcluso": - return queryset.exclude( - id__in=Solicitacao.objects.exclude( - itemsolicitado__status=ItemSolicitado.STATUS_SOLICITADO - ).only("id") - ) - elif self.value() == "concluido": - return queryset.exclude( - itemsolicitado__status=ItemSolicitado.STATUS_SOLICITADO - ).distinct() - return queryset - - class SolicitacaoResource(LabeledResourse): - status = Field(column_name="status") oficinas = Field(column_name="oficinas solicitadas") oficinas_uf = Field(column_name="número de oficinas realizadas na UF") @@ -114,7 +69,7 @@ class SolicitacaoResource(LabeledResourse): export_order = fields def dehydrate_status(self, obj): - return obj.get_status() + return obj.get_status_display() def dehydrate_oficinas(self, obj): return ", ".join( @@ -234,6 +189,11 @@ class ItemSolicitadoInline(admin.StackedInline): autocomplete_fields = ("tipo_evento",) +class AnexoSolicitacaoInline(admin.TabularInline): + model = AnexoSolicitacao + readonly_fields = ("data_pub",) + + @admin.register(TipoEvento) class TipoEventoAdmin(admin.ModelAdmin): list_display = ["nome", "categoria"] @@ -248,7 +208,7 @@ class SolicitacaoAdmin(CartExportMixin, admin.ModelAdmin): list_display = ( "casa", "get_sigad_url", - "get_status", + "status", "senador", "data_pedido", "data_recebido_coperi", @@ -266,7 +226,7 @@ class SolicitacaoAdmin(CartExportMixin, admin.ModelAdmin): "casa__municipio__uf__regiao", "senador", "itemsolicitado__tipo_evento", - SolicitacaoStatusFilter, + "status", ) list_select_related = ["casa", "casa__municipio", "casa__municipio__uf"] list_display_links = ("casa",) @@ -277,95 +237,129 @@ class SolicitacaoAdmin(CartExportMixin, admin.ModelAdmin): "senador", ) date_hierarchy = "data_pedido" - inlines = (ItemSolicitadoInline,) + fieldsets = ( + ( + None, + { + "fields": [ + "casa", + "senador", + "num_processo", + "descricao", + "data_pedido", + "data_recebido_coperi", + ] + }, + ), + ( + _("Autorização"), + { + "fields": [ + "status", + "servidor", + "data_analise", + "justificativa", + ] + }, + ), + ( + _("Contato da Casa"), + { + "fields": [ + "contato", + "email_contato", + "telefone_contato", + "whatsapp_contato", + ] + }, + ), + ( + _("Participação esperada"), + {"fields": ["estimativa_casas", "estimativa_servidores"]}, + ), + ) + readonly_fields = ("servidor", "data_analise") + inlines = (ItemSolicitadoInline, AnexoSolicitacaoInline) autocomplete_fields = ("casa",) - def save_formset(self, request, form, formset, change): - instances = formset.save(commit=False) - - if hasattr(request.user, "servidor"): - servidor = request.user.servidor + def save_model(self, request, obj, form, change): + if change: + old_obj = Solicitacao.objects.get(id=obj.id) else: - servidor = None - - agora = timezone.localtime() + old_obj = obj + if ( + obj.status != Solicitacao.STATUS_SOLICITADO + and obj.status != old_obj.status + ): + obj.servidor = ( + request.user.servidor + if hasattr(request.user, "servidor") + else None + ) + obj.data_analise = timezone.localtime() + return super().save_model(request, obj, form, change) - for item in instances: - if ( - item.status == ItemSolicitado.STATUS_SOLICITADO - and item.evento is not None - ): - item.evento.status = Evento.STATUS_ACONFIRMAR - self.message_user( - request, - _( - f"Status do evento {item.evento} alterado para " - f"{item.evento.get_status_display()}" - ), - messages.INFO, - ) - elif item.status == ItemSolicitado.STATUS_AUTORIZADO: - item.servidor = servidor - item.data_analise = agora - if item.evento is None: - item.evento = Evento( - tipo_evento=item.tipo_evento, - nome=_( - f"{item.tipo_evento} em {item.solicitacao.casa}"[ - :100 - ] - ), - descricao=_( - f"{item.tipo_evento} em {item.solicitacao.casa}" - ), - virtual=item.virtual, - solicitante=item.solicitacao.senador, - num_processo=item.solicitacao.num_processo, - data_pedido=item.solicitacao.data_pedido, - data_recebido_coperi=item.solicitacao.data_recebido_coperi, - data_inicio=item.inicio_desejado, - data_termino=item.inicio_desejado - + datetime.timedelta(days=item.tipo_evento.duracao), - casa_anfitria=item.solicitacao.casa, - observacao=_( - f"Autorizado por {servidor} com a justificativa '{item.justificativa}" + def save_formset(self, request, form, formset, change): + if formset.model == ItemSolicitado: + obj = form.instance + instances = formset.save(commit=False) + + if hasattr(request.user, "servidor"): + servidor = request.user.servidor + else: + servidor = None + + agora = timezone.localtime() + + for item in instances: + if ( + obj.status == Solicitacao.STATUS_SOLICITADO + and item.status != ItemSolicitado.STATUS_SOLICITADO + ): + item.status = ItemSolicitado.STATUS_SOLICITADO + self.message_user( + request, + _( + f"O item {item} teve o status mudado para " + "SOLICITADO porque a solicitação ainda não foi " + "autorizada" ), - status=Evento.STATUS_CONFIRMADO, - contato=item.solicitacao.contato, - telefone=item.solicitacao.telefone_contato, + messages.WARNING, ) + if ( + obj.status == Solicitacao.STATUS_REJEITADO + and item.status != ItemSolicitado.STATUS_REJEITADO + ): + item.status = ItemSolicitado.STATUS_REJEITADO self.message_user( request, - _(f"Evento {item.evento} criado automaticamente."), - messages.INFO, - ) - else: - item.evento.status = Evento.STATUS_CONFIRMADO - item.evento.observacao += _( - f"\nConfirmado por {servidor} com a justificativa: {item.justificativa}" + _( + f"O item {item} teve o status mudado para " + "REJEITADO porque a solicitação inteira foi " + "rejeitada" + ), + messages.WARNING, ) - item.evento.data_cancelamento = None - item.evento.motivo_cancelamento = "" + if ( + obj.status == Solicitacao.STATUS_CONCLUIDO + and item.status == ItemSolicitado.STATUS_SOLICITADO + ): + item.status = ItemSolicitado.STATUS_REJEITADO self.message_user( request, _( - f"Status do evento {item.evento} alterado para " - f"{item.evento.get_status_display()}" + f"O item {item} teve o status mudado para " + "REJEITADO porque a solicitação foi concluída e " + "ele ainda estava em aberto" ), - messages.INFO, - ) - elif item.status == ItemSolicitado.STATUS_REJEITADO: - item.servidor = servidor - item.data_analise = agora - if item.evento is not None: - item.evento.status = Evento.STATUS_CANCELADO - item.evento.observacao += _( - f"\nCancelado por {servidor} com a justificativa: {item.justificativa}" - ) - item.evento.data_cancelamento = timezone.localdate() - item.evento.motivo_cancelamento = _( - f"\nCancelado por {servidor} com a justificativa: {item.justificativa}" + messages.WARNING, ) + + if ( + item.status == ItemSolicitado.STATUS_SOLICITADO + and item.evento is not None + ): + item.evento.status = Evento.STATUS_ACONFIRMAR self.message_user( request, _( @@ -374,31 +368,121 @@ class SolicitacaoAdmin(CartExportMixin, admin.ModelAdmin): ), messages.INFO, ) - if item.evento: - item.evento.tipo_evento = item.tipo_evento - item.evento.nome = _( - f"{item.tipo_evento} em {item.solicitacao.casa}"[:100] - ) - item.evento.descricao = _( - f"{item.tipo_evento} em {item.solicitacao.casa}" - ) - item.evento.virtual = item.virtual - item.evento.solicitante = item.solicitacao.senador - item.evento.num_processo = item.solicitacao.num_processo - item.evento.data_pedido = item.solicitacao.data_pedido - item.evento.data_recebido_coperi = ( - item.solicitacao.data_recebido_coperi - ) - item.evento.data_inicio = item.inicio_desejado - item.evento.data_termino = ( - item.inicio_desejado - + datetime.timedelta(days=item.tipo_evento.duracao) + elif item.status == ItemSolicitado.STATUS_AUTORIZADO: + item.servidor = servidor + item.data_analise = agora + if item.evento is None: + item.evento = Evento( + tipo_evento=item.tipo_evento, + nome=_( + f"{item.tipo_evento} em {item.solicitacao.casa}"[ + :100 + ] + ), + descricao=_( + f"{item.tipo_evento} em {item.solicitacao.casa}" + ), + virtual=item.virtual, + solicitante=item.solicitacao.senador, + num_processo=item.solicitacao.num_processo, + data_pedido=item.solicitacao.data_pedido, + data_recebido_coperi=item.solicitacao.data_recebido_coperi, + data_inicio=item.inicio_desejado, + data_termino=item.inicio_desejado + + datetime.timedelta( + days=item.tipo_evento.duracao + ), + casa_anfitria=item.solicitacao.casa, + observacao=_( + f"Autorizado por {servidor} com a justificativa '{item.justificativa}" + ), + status=Evento.STATUS_CONFIRMADO, + contato=item.solicitacao.contato, + telefone=item.solicitacao.telefone_contato, + ) + self.message_user( + request, + _(f"Evento {item.evento} criado automaticamente."), + messages.INFO, + ) + else: + item.evento.status = Evento.STATUS_CONFIRMADO + item.evento.observacao += _( + f"\nConfirmado por {servidor} com a justificativa: {item.justificativa}" + ) + item.evento.data_cancelamento = None + item.evento.motivo_cancelamento = "" + self.message_user( + request, + _( + f"Status do evento {item.evento} alterado para " + f"{item.evento.get_status_display()}" + ), + messages.INFO, + ) + elif item.status == ItemSolicitado.STATUS_REJEITADO: + item.servidor = servidor + item.data_analise = agora + if item.evento is not None: + item.evento.status = Evento.STATUS_CANCELADO + item.evento.observacao += _( + f"\nCancelado por {servidor} com a justificativa: {item.justificativa}" + ) + item.evento.data_cancelamento = timezone.localdate() + item.evento.motivo_cancelamento = _( + f"\nCancelado por {servidor} com a justificativa: {item.justificativa}" + ) + self.message_user( + request, + _( + f"Status do evento {item.evento} alterado para " + f"{item.evento.get_status_display()}" + ), + messages.INFO, + ) + if item.evento: + item.evento.tipo_evento = item.tipo_evento + item.evento.nome = _( + f"{item.tipo_evento} em {item.solicitacao.casa}"[:100] + ) + item.evento.descricao = _( + f"{item.tipo_evento} em {item.solicitacao.casa}" + ) + item.evento.virtual = item.virtual + item.evento.solicitante = item.solicitacao.senador + item.evento.num_processo = item.solicitacao.num_processo + item.evento.data_pedido = item.solicitacao.data_pedido + item.evento.data_recebido_coperi = ( + item.solicitacao.data_recebido_coperi + ) + item.evento.data_inicio = item.inicio_desejado + item.evento.data_termino = ( + item.inicio_desejado + + datetime.timedelta(days=item.tipo_evento.duracao) + ) + item.evento.casa_anfitria = item.solicitacao.casa + item.evento.contato = item.solicitacao.contato + item.evento.telefone = item.solicitacao.telefone_contato + item.evento.save() + item.save() + + if ( + obj.status == Solicitacao.STATUS_AUTORIZADO + and not obj.itemsolicitado_set.filter( + status=ItemSolicitado.STATUS_SOLICITADO + ).exists() + ): + obj.status = Solicitacao.STATUS_CONCLUIDO + obj.save() + self.message_user( + request, + _( + "Status da solicitação alterado automaticamente para " + "Concluído pois não há mais itens a serem analisados" + ), + messages.INFO, ) - item.evento.casa_anfitria = item.solicitacao.casa - item.evento.contato = item.solicitacao.contato - item.evento.telefone = item.solicitacao.telefone_contato - item.evento.save() - item.save() + return super().save_formset(request, form, formset, change) @admin.display(description=_("Oficinas solicitadas")) diff --git a/sigi/apps/eventos/migrations/0041_solicitacao_data_analise_solicitacao_justificativa_and_more.py b/sigi/apps/eventos/migrations/0041_solicitacao_data_analise_solicitacao_justificativa_and_more.py new file mode 100644 index 0000000..f66ba72 --- /dev/null +++ b/sigi/apps/eventos/migrations/0041_solicitacao_data_analise_solicitacao_justificativa_and_more.py @@ -0,0 +1,104 @@ +# Generated by Django 4.2.4 on 2023-09-13 22:15 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + dependencies = [ + ("servidores", "0013_servidor_moodle_userid"), + ("eventos", "0040_alter_itemsolicitado_data_analise_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="solicitacao", + name="data_analise", + field=models.DateTimeField( + blank=True, + editable=False, + null=True, + verbose_name="data de autorização/rejeição", + ), + ), + migrations.AddField( + model_name="solicitacao", + name="justificativa", + field=models.TextField(blank=True, verbose_name="Justificativa"), + ), + migrations.AddField( + model_name="solicitacao", + name="servidor", + field=models.ForeignKey( + blank=True, + editable=False, + help_text="Servidor que autorizou ou rejeitou a realização do evento", + limit_choices_to={"externo": False}, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="servidores.servidor", + verbose_name="servidor analisador", + ), + ), + migrations.AddField( + model_name="solicitacao", + name="status", + field=models.CharField( + choices=[ + ("S", "Solicitado"), + ("A", "Autorizado"), + ("R", "Rejeitado"), + ("C", "Concluído"), + ], + default="S", + max_length=1, + verbose_name="Status", + ), + ), + migrations.CreateModel( + name="AnexoSolicitacao", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "arquivo", + models.FileField( + max_length=500, + upload_to="apps/eventos/solicitacao/anexo/arquivo", + ), + ), + ( + "descricao", + models.CharField(max_length=70, verbose_name="descrição"), + ), + ( + "data_pub", + models.DateTimeField( + default=django.utils.timezone.localtime, + verbose_name="data da publicação do anexo", + ), + ), + ( + "solicitacao", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="eventos.solicitacao", + verbose_name="evento", + ), + ), + ], + options={ + "verbose_name": "Anexo", + "verbose_name_plural": "Anexos", + "ordering": ("-data_pub",), + }, + ), + ] diff --git a/sigi/apps/eventos/migrations/0042_atualiza_status_solicitacao.py b/sigi/apps/eventos/migrations/0042_atualiza_status_solicitacao.py new file mode 100644 index 0000000..a62763e --- /dev/null +++ b/sigi/apps/eventos/migrations/0042_atualiza_status_solicitacao.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.4 on 2023-09-13 22:20 + +from django.db import migrations + + +def forwards(apps, schema_editor): + Solicitacao = apps.get_model("eventos", "Solicitacao") + + for s in Solicitacao.objects.all(): + statuses = list( + s.itemsolicitado_set.values_list("status", flat=True).distinct( + "status" + ) + ) + if statuses == ["S"]: + s.status = "S" + elif statuses == ["A"]: + s.status = "C" + elif statuses == ["R"]: + s.status = "R" + elif "S" in statuses and ("A" in statuses or "R" in statuses): + s.status = "A" + s.save() + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eventos", + "0041_solicitacao_data_analise_solicitacao_justificativa_and_more", + ), + ] + + operations = [ + migrations.RunPython( + forwards, + ) + ] diff --git a/sigi/apps/eventos/models.py b/sigi/apps/eventos/models.py index b225d9a..4419974 100644 --- a/sigi/apps/eventos/models.py +++ b/sigi/apps/eventos/models.py @@ -1,3 +1,4 @@ +from collections.abc import Iterable import datetime import re from tinymce.models import HTMLField @@ -71,6 +72,16 @@ class TipoEvento(models.Model): class Solicitacao(models.Model): + STATUS_SOLICITADO = "S" + STATUS_AUTORIZADO = "A" + STATUS_REJEITADO = "R" + STATUS_CONCLUIDO = "C" + STATUS_CHOICES = ( + (STATUS_SOLICITADO, _("Solicitado")), + (STATUS_AUTORIZADO, _("Autorizado")), + (STATUS_REJEITADO, _("Rejeitado")), + (STATUS_CONCLUIDO, _("Concluído")), + ) casa = models.ForeignKey( Orgao, verbose_name=_("casa solicitante"), on_delete=models.PROTECT ) @@ -92,6 +103,33 @@ class Solicitacao(models.Model): blank=True, help_text=_("Data em que o pedido chegou na COPERI"), ) + status = models.CharField( + _("Status"), + max_length=1, + choices=STATUS_CHOICES, + default=STATUS_SOLICITADO, + ) + servidor = models.ForeignKey( + Servidor, + verbose_name=_("servidor analisador"), + help_text=_( + "Servidor que autorizou ou rejeitou a realização do evento" + ), + on_delete=models.PROTECT, + limit_choices_to={"externo": False}, + blank=True, + null=True, + editable=False, + ) + data_analise = models.DateTimeField( + _("data de autorização/rejeição"), + blank=True, + null=True, + editable=False, + ) + justificativa = models.TextField( + verbose_name=_("Justificativa"), blank=True + ) contato = models.CharField( _("pessoa de contato na Casa"), max_length=100, blank=True ) @@ -121,28 +159,6 @@ class Solicitacao(models.Model): def __str__(self): return _(f"{self.num_processo}: {self.casa} / Senador {self.senador}") - @admin.display(description="Status") - def get_status(self): - # TODO: Definir status do pedido com base no status de seus ítens: - # Aberto: Todos os itens estão em estado Solicitado - # Análise: Parte dos pedidos estão Autorizados/Rejeitados e o restante - # está Solicitado - # Concluído: Nenhum pedido está em estado Solicitado - item_status = set( - self.itemsolicitado_set.distinct("status").values_list( - "status", flat=True - ) - ) - if {ItemSolicitado.STATUS_SOLICITADO} == item_status: - return _("Aberto") - elif ItemSolicitado.STATUS_SOLICITADO in item_status and ( - ItemSolicitado.STATUS_AUTORIZADO in item_status - or ItemSolicitado.STATUS_REJEITADO in item_status - ): - return _("Análise") - else: - return _("Concluído") - @admin.display(description=_("SIGAD"), ordering="num_processo") def get_sigad_url(self): m = re.match( @@ -256,6 +272,27 @@ class ItemSolicitado(models.Model): return _(f"{self.tipo_evento}: {self.get_status_display()}") +class AnexoSolicitacao(models.Model): + solicitacao = models.ForeignKey( + Solicitacao, on_delete=models.CASCADE, verbose_name=_("evento") + ) + arquivo = models.FileField( + upload_to="apps/eventos/solicitacao/anexo/arquivo", max_length=500 + ) + descricao = models.CharField(_("descrição"), max_length=70) + data_pub = models.DateTimeField( + _("data da publicação do anexo"), default=timezone.localtime + ) + + class Meta: + ordering = ("-data_pub",) + verbose_name = _("Anexo") + verbose_name_plural = _("Anexos") + + def __str__(self): + return _(f"{self.descricao} publicado em {self.data_pub}") + + class Evento(models.Model): STATUS_PLANEJAMENTO = "E" STATUS_AGUARDANDOSIGAD = "G" diff --git a/sigi/menu_conf.yaml b/sigi/menu_conf.yaml index 00b479a..e3721a3 100644 --- a/sigi/menu_conf.yaml +++ b/sigi/menu_conf.yaml @@ -76,7 +76,7 @@ main_menu: children: - title: Solicitações view_name: admin:eventos_solicitacao_changelist - querystr: status=inconcluso + querystr: status__exact=S - title: Todos os eventos view_name: admin:eventos_evento_changelist - title: Cursos