diff --git a/sigi/apps/espacos/admin.py b/sigi/apps/espacos/admin.py index aea8763..e408d31 100644 --- a/sigi/apps/espacos/admin.py +++ b/sigi/apps/espacos/admin.py @@ -1,4 +1,6 @@ +from typing import Any from django.contrib import admin, messages +from django.http.request import HttpRequest from django.urls import path, reverse from django.utils.safestring import mark_safe from django.utils.translation import gettext as _, ngettext @@ -10,6 +12,7 @@ from sigi.apps.espacos.models import ( Reserva, RecursoSolicitado, ) +from sigi.apps.espacos.forms import ReservaAdminForm from sigi.apps.utils.mixins import CartExportMixin, LabeledResourse @@ -65,6 +68,7 @@ class RecursoAdmin(admin.ModelAdmin): @admin.register(Reserva) class ReservaAdmin(CartExportMixin, admin.ModelAdmin): + form = ReservaAdminForm resource_classes = [ReservaResource] list_display = [ "get_status", @@ -87,7 +91,6 @@ class ReservaAdmin(CartExportMixin, admin.ModelAdmin): "num_processo", ] date_hierarchy = "inicio" - actions = ["cancelar_action", "reativar_action"] fieldsets = [ (None, {"fields": ("status",)}), ( @@ -95,6 +98,7 @@ class ReservaAdmin(CartExportMixin, admin.ModelAdmin): { "fields": ( "espaco", + "evento", "proposito", "num_processo", "virtual", @@ -119,20 +123,24 @@ class ReservaAdmin(CartExportMixin, admin.ModelAdmin): ), ] autocomplete_fields = ["espaco"] - readonly_fields = ("status",) + readonly_fields = ("evento",) inlines = [RecursoSolicitadoInline] - def get_urls(self): - urls = super().get_urls() - model_info = self.get_model_info() - my_urls = [ - path( - "/cancel/", - self.admin_site.admin_view(self.cancelar_reserva), - name="%s_%s_cancel" % model_info, - ) - ] - return my_urls + urls + def get_readonly_fields(self, request, obj=None): + if obj and hasattr(obj, "evento"): + if not hasattr(self, "_readonly_evento_alerted"): + self.message_user( + request, + _( + f"Esta reserva está vinculada ao evento '{obj.evento}'. " + "Apenas os recursos solicitados podem ser editados. " + "Os demais campos devem ser alterados no evento." + ), + level=messages.ERROR, + ) + self._readonly_evento_alerted = True + return self.get_fields(request) + return super().get_readonly_fields(request, obj) @admin.display(description=_("Status"), ordering="status", boolean=True) def get_status(self, obj): @@ -147,37 +155,3 @@ class ReservaAdmin(CartExportMixin, admin.ModelAdmin): if obj.pk is None: return "" return mark_safe(obj.get_sigad_url()) - - def cancelar_reserva(self, request, object_id): - reserva = get_object_or_404(Reserva, id=object_id) - reserva.status = Reserva.STATUS_CANCELADO - reserva.save() - return redirect( - reverse( - "admin:%s_%s_change" % self.get_model_info(), args=[object_id] - ) - + "?" - + self.get_preserved_filters(request) - ) - - @admin.action(description=_("Cancelar as reservas selecionadas")) - def cancelar_action(self, request, queryset): - count = queryset.update(status=Reserva.STATUS_CANCELADO) - self.message_user( - request, - ngettext( - "Uma reserva cancelada", f"{count} reservas canceladas", count - ), - messages.SUCCESS, - ) - - @admin.action(description=_("Reativar as reservas selecionadas")) - def reativar_action(self, request, queryset): - count = queryset.update(status=Reserva.STATUS_ATIVO) - self.message_user( - request, - ngettext( - "Uma reserva reativada", f"{count} reservas reativadas", count - ), - messages.SUCCESS, - ) diff --git a/sigi/apps/espacos/forms.py b/sigi/apps/espacos/forms.py index b09af58..ba6e6d0 100644 --- a/sigi/apps/espacos/forms.py +++ b/sigi/apps/espacos/forms.py @@ -4,7 +4,14 @@ from django import forms from material.admin.widgets import MaterialAdminDateWidget from django.forms.widgets import CheckboxSelectMultiple from django.utils.translation import gettext as _ -from sigi.apps.espacos.models import Espaco +from sigi.apps.espacos.models import Espaco, Reserva + + +class ReservaAdminForm(forms.ModelForm): + class Meta: + model = Reserva + widgets = {"status": forms.RadioSelect} + fields = "__all__" class UsoEspacoReportForm(forms.Form): diff --git a/sigi/apps/espacos/migrations/0004_alter_reserva_status.py b/sigi/apps/espacos/migrations/0004_alter_reserva_status.py new file mode 100644 index 0000000..d39c3d7 --- /dev/null +++ b/sigi/apps/espacos/migrations/0004_alter_reserva_status.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.4 on 2023-12-05 13:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("espacos", "0003_reserva_data_pedido_reserva_num_processo_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="reserva", + name="status", + field=models.CharField( + choices=[("A", "Ativo"), ("C", "Cancelado")], + default="A", + max_length=1, + verbose_name="status", + ), + ), + ] diff --git a/sigi/apps/espacos/models.py b/sigi/apps/espacos/models.py index 36600f5..ff35952 100644 --- a/sigi/apps/espacos/models.py +++ b/sigi/apps/espacos/models.py @@ -1,6 +1,8 @@ import re from django.core.exceptions import ValidationError from django.db import models +from django.urls import reverse +from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ @@ -53,7 +55,6 @@ class Reserva(models.Model): max_length=1, choices=STATUS_CHOICES, default=STATUS_ATIVO, - editable=False, ) espaco = models.ForeignKey( Espaco, verbose_name=_("espaço"), on_delete=models.PROTECT @@ -126,23 +127,36 @@ class Reserva(models.Model): raise ValidationError( _("Data de início deve ser anterior à data de término") ) - if ( - Reserva.objects.exclude(id=self.pk) - .filter( - espaco=self.espaco, - inicio__lte=self.termino, - termino__gte=self.inicio, + reservas_conflitantes = Reserva.objects.exclude(id=self.pk).filter( + espaco=self.espaco, + inicio__lte=self.termino, + termino__gte=self.inicio, + status=Reserva.STATUS_ATIVO, + ) + + if reservas_conflitantes.exists(): + link_list = ", ".join( + [ + f"" + f"{ reserva }" + for reserva in reservas_conflitantes + ] ) - .exists() - ): raise ValidationError( - _( - "Já existe um evento neste mesmo espaço que conflita com " - "as datas solicitadas" + mark_safe( + _( + "Existe(m) reserva(s) que conflita(m) com essas datas: " + f"{ link_list }" + ) ) ) return super().clean() + def save(self, *args, **kwargs): + self.clean() + return super().save(*args, **kwargs) + def get_sigad_url(self): m = re.match( "(?P00100|00200)\.(?P\d{6})/(?P" diff --git a/sigi/apps/espacos/templates/admin/espacos/reserva/change_form.html b/sigi/apps/espacos/templates/admin/espacos/reserva/change_form.html deleted file mode 100644 index ce08861..0000000 --- a/sigi/apps/espacos/templates/admin/espacos/reserva/change_form.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "admin/change_form.html" %} -{% load i18n admin_urls %} - -{% block object-tools-items %} - {% if object_id %} -
  • - {% url opts|admin_urlname:'cancel' object_id|admin_urlquote as tool_url %} - - - {% trans "Cancelar reserva" %} - -
  • - {% endif %} - {{ block.super }} -{% endblock %} diff --git a/sigi/apps/eventos/admin.py b/sigi/apps/eventos/admin.py index 1419014..016d703 100644 --- a/sigi/apps/eventos/admin.py +++ b/sigi/apps/eventos/admin.py @@ -873,6 +873,7 @@ class EventoAdmin(AsciifyQParameter, CartExportReportMixin, admin.ModelAdmin): "data_termino", "carga_horaria", "casa_anfitria", + "espaco", "contato", "telefone", "observacao", @@ -1134,6 +1135,40 @@ class EventoAdmin(AsciifyQParameter, CartExportReportMixin, admin.ModelAdmin): ] return my_urls + urls + def save_model(self, request, obj, form, change): + if change: + reserva_original = self.get_object(request, obj.pk).reserva + super().save_model(request, obj, form, change) + if change: + if reserva_original is None and obj.reserva is not None: + self.message_user( + request, + _( + f"Reserva do espaço '{obj.reserva.espaco}' criada para " + "este evento.", + ), + level=messages.SUCCESS, + ) + if reserva_original is not None: + if obj.reserva is None: + self.message_user( + request, + _( + f"Reserva do espaço '{reserva_original.espaco}' " + "excluída.", + ), + level=messages.SUCCESS, + ) + else: + self.message_user( + request, + _( + f"Reserva do espaço '{obj.reserva.espaco}' " + "atualizada.", + ), + level=messages.SUCCESS, + ) + def declaracao_report(self, request, object_id): if request.method == "POST": form = SelecionaModeloForm(request.POST) diff --git a/sigi/apps/eventos/forms.py b/sigi/apps/eventos/forms.py index 31eb822..3047b4e 100644 --- a/sigi/apps/eventos/forms.py +++ b/sigi/apps/eventos/forms.py @@ -1,12 +1,24 @@ +from collections.abc import Mapping +from typing import Any from django import forms +from django.core.files.base import File +from django.db.models.base import Model +from django.forms.utils import ErrorList from django.utils.translation import gettext as _ from material.admin.widgets import MaterialAdminTextareaWidget from sigi.apps.casas.models import Funcionario, Orgao +from sigi.apps.espacos.models import Espaco, Reserva from sigi.apps.eventos.models import Convite, ModeloDeclaracao, Evento from sigi.apps.parlamentares.models import Parlamentar class EventoAdminForm(forms.ModelForm): + espaco = forms.ModelChoiceField( + label=_("Reservar espaço"), + required=False, + queryset=Espaco.objects.all(), + ) + class Meta: model = Evento fields = ( @@ -23,6 +35,7 @@ class EventoAdminForm(forms.ModelForm): "data_termino", "carga_horaria", "casa_anfitria", + "espaco", "observacao", "local", "publico_alvo", @@ -42,8 +55,13 @@ class EventoAdminForm(forms.ModelForm): "motivo_cancelamento", ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.reserva: + self.initial["espaco"] = self.instance.reserva.espaco + def clean(self): - cleaned_data = super(EventoAdminForm, self).clean() + cleaned_data = super().clean() data_inicio = cleaned_data.get("data_inicio") data_termino = cleaned_data.get("data_termino") publicar = cleaned_data.get("publicar") @@ -57,11 +75,27 @@ class EventoAdminForm(forms.ModelForm): if publicar and (data_inicio is None or data_termino is None): raise forms.ValidationError( _( - "Para publicar no site é preciso ter data início e data término" + "Para publicar no site é preciso ter data início e " + "data término" ), code="cannot_publish", ) + espaco = cleaned_data["espaco"] + if (self.instance.reserva is None) and (espaco is None): + return + if self.instance.reserva is None: + self.instance.reserva = Reserva(espaco=espaco) + elif espaco is None: + self.instance.reserva = None + else: + self.instance.reserva.espaco = espaco + + if self.instance.reserva: + self.instance.update_reserva() + + return cleaned_data + class SelecionaModeloForm(forms.Form): modelo = forms.ModelChoiceField( diff --git a/sigi/apps/eventos/migrations/0055_evento_reserva.py b/sigi/apps/eventos/migrations/0055_evento_reserva.py new file mode 100644 index 0000000..bcd334d --- /dev/null +++ b/sigi/apps/eventos/migrations/0055_evento_reserva.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.4 on 2023-12-01 12:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("espacos", "0003_reserva_data_pedido_reserva_num_processo_and_more"), + ( + "eventos", + "0054_equipe_emissao_passagens_equipe_qtde_diarias_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="evento", + name="reserva", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="espacos.reserva", + ), + ), + ] diff --git a/sigi/apps/eventos/models.py b/sigi/apps/eventos/models.py index e3d408c..eef3194 100644 --- a/sigi/apps/eventos/models.py +++ b/sigi/apps/eventos/models.py @@ -15,6 +15,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ from sigi.apps.casas.models import Orgao, Servidor from sigi.apps.contatos.models import Municipio +from sigi.apps.espacos.models import Reserva from sigi.apps.servidores.models import Servidor @@ -374,6 +375,9 @@ class Evento(models.Model): blank=True, null=True, ) + reserva = models.OneToOneField( + Reserva, blank=True, null=True, on_delete=models.PROTECT + ) local = models.TextField(_("Local do evento"), blank=True) observacao = models.TextField(_("Observações e anotações"), blank=True) publico_alvo = models.TextField(_("Público alvo"), blank=True) @@ -606,6 +610,39 @@ class Evento(models.Model): self.save() + def clean(self): + super().clean() + if ( + self.data_inicio + and self.data_termino + and self.data_inicio > self.data_termino + ): + raise ValidationError( + _("Data de término deve ser posterior à data de início") + ) + if self.reserva: + self.update_reserva() + self.reserva.clean() + + def update_reserva(self): + # Prepara e valida a reserva de espaço para ser salva + # Gertiq #167321 + if self.reserva is not None: + self.reserva.proposito = self.nome + self.reserva.virtual = self.virtual + self.reserva.data_pedido = self.data_pedido + self.reserva.inicio = self.data_inicio + self.reserva.termino = self.data_termino + self.reserva.num_processo = self.num_processo + self.reserva.informacoes = self.observacao + self.reserva.solicitante = self.solicitante + self.reserva.contato = self.contato + self.reserva.telefone_contato = self.telefone + if self.status in (self.STATUS_CANCELADO, self.STATUS_SOBRESTADO): + self.reserva.status = Reserva.STATUS_CANCELADO + else: + self.reserva.status = Reserva.STATUS_ATIVO + def save(self, *args, **kwargs): # Força que a casa anfitriã de todas as visitas seja Senado # Gertik #165751 @@ -622,15 +659,6 @@ class Evento(models.Model): if self.status != Evento.STATUS_CANCELADO: self.data_cancelamento = None self.motivo_cancelamento = "" - if ( - self.data_inicio - and self.data_termino - and self.data_inicio > self.data_termino - ): - raise ValidationError( - _("Data de término deve ser posterior à data de início") - ) - if ( self.turma == "" and self.data_inicio @@ -653,7 +681,15 @@ class Evento(models.Model): self.turma = f"{proximo:02}/{ano:04}" # É preciso salvar para poder usar o relacionamento com convites + if self.reserva is None: + reservas_remover = list(Reserva.objects.filter(evento=self)) + else: + self.update_reserva() + self.reserva.save() + reservas_remover = Reserva.objects.none() super().save(*args, **kwargs) + for reserva in reservas_remover: + reserva.delete() if self.total_participantes == 0 and self.moodle_courseid is None: # Só calcula total_participantes se não tem curso relacionado diff --git a/sigi/menu_conf.yaml b/sigi/menu_conf.yaml index 308e5e4..132184c 100644 --- a/sigi/menu_conf.yaml +++ b/sigi/menu_conf.yaml @@ -76,7 +76,7 @@ main_menu: children: - title: Reservas view_name: admin:espacos_reserva_changelist - querystr: status=A + querystr: status__exact=A - title: Agenda de reservas view_name: espacos_agenda - title: Uso dos espaços