Browse Source

Especializa eventos do tipo Visita

pull/202/head
Sesóstris Vieira 2 weeks ago
parent
commit
9f103b9d64
  1. 216
      sigi/apps/eventos/admin.py
  2. 82
      sigi/apps/eventos/migrations/0067_localvisita_visita_evento_pesquisa_previa_and_more.py
  3. 27
      sigi/apps/eventos/migrations/0068_acerta_casa_visitas.py
  4. 69
      sigi/apps/eventos/models.py
  5. 7
      sigi/apps/eventos/templates/admin/eventos/evento/change_form_object_tools.html
  6. 10
      sigi/apps/eventos/templates/admin/eventos/visita/change_form_object_tools.html
  7. 34
      sigi/apps/utils/__init__.py
  8. 5
      sigi/menu_conf.yaml

216
sigi/apps/eventos/admin.py

@ -39,6 +39,7 @@ from sigi.apps.contatos.models import UnidadeFederativa, Municipio
from sigi.apps.eventos.models import ( from sigi.apps.eventos.models import (
Checklist, Checklist,
Cronograma, Cronograma,
LocalVisita,
ModeloDeclaracao, ModeloDeclaracao,
Modulo, Modulo,
Participante, Participante,
@ -48,6 +49,7 @@ from sigi.apps.eventos.models import (
ItemSolicitado, ItemSolicitado,
Funcao, Funcao,
Evento, Evento,
Visita,
Equipe, Equipe,
Convite, Convite,
Anexo, Anexo,
@ -330,6 +332,42 @@ class EventoResource(ValueModelResource):
) )
class VisitaResource(resources.ModelResource):
class Meta:
model = Visita
fields = (
"id",
"nome",
"descricao",
"virtual",
"solicitante",
"num_processo",
"data_pedido",
"data_inicio",
"data_termino",
"casa_anfitria__nome",
"casa_anfitria__logradouro",
"casa_anfitria__bairro",
"casa_anfitria__municipio__nome",
"casa_anfitria__municipio__populacao",
"casa_anfitria__municipio__uf__sigla",
"casa_anfitria__municipio__uf__regiao",
"casa_anfitria__cep",
"casa_anfitria__email",
"local_visita__nome",
"observacao",
"status",
"data_cancelamento",
"motivo_cancelamento",
"participante__casa_legislativa",
"participante__cpf",
"participante__email",
"participante__nome",
"participante__local_trabalho",
)
export_order = fields
class ChecklistInline(admin.StackedInline): class ChecklistInline(admin.StackedInline):
model = Checklist model = Checklist
@ -905,6 +943,11 @@ class ModeloDeclaracaoAdmin(admin.ModelAdmin):
formfield_overrides = {HTMLField: {"widget": AdminTinyMCE}} formfield_overrides = {HTMLField: {"widget": AdminTinyMCE}}
@admin.register(LocalVisita)
class LocalVisitaAdmin(admin.ModelAdmin):
list_display = ("nome", "descricao")
@admin.register(Evento) @admin.register(Evento)
class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin): class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
form = EventoAdminForm form = EventoAdminForm
@ -1109,7 +1152,11 @@ class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
my_decimal_field = models.DecimalField(max_digits=14, decimal_places=2) my_decimal_field = models.DecimalField(max_digits=14, decimal_places=2)
queryset = super().get_queryset(request) queryset = (
super()
.get_queryset(request)
.exclude(tipo_evento__categoria=TipoEvento.CATEGORIA_VISITA)
)
return queryset.annotate( return queryset.annotate(
custo_total=Sum( custo_total=Sum(
(F("equipe__qtde_diarias") * F("equipe__valor_diaria")) (F("equipe__qtde_diarias") * F("equipe__valor_diaria"))
@ -1739,3 +1786,170 @@ class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
) )
return redirect(change_url) return redirect(change_url)
@admin.register(Visita)
class VisitaAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
resource_classes = [VisitaResource]
fieldsets = (
(
None,
{
"fields": (
"tipo_evento",
"casa_anfitria",
"nome",
"descricao",
"contato",
"telefone",
"data_pedido",
"data_recebido_coperi",
("data_inicio", "hora_inicio"),
("data_termino", "hora_termino"),
"local_visita",
"virtual",
"solicitante",
"status",
"data_cancelamento",
"motivo_cancelamento",
"pesquisa_previa",
"observacao",
"num_processo",
)
},
),
)
list_display = (
"nome",
"casa_anfitria",
"status",
"data_inicio",
"hora_inicio",
"hora_termino",
"pesquisa_previa",
"get_link_sigad",
)
list_filter = (
"status",
("num_processo", admin.EmptyFieldListFilter),
("tipo_evento", admin.RelatedOnlyFieldListFilter),
"participante__casa_legislativa__municipio__uf",
"participante__casa_legislativa__municipio__uf__regiao",
("data_inicio", DateRangeFilterBuilder()),
"virtual",
"solicitante",
)
date_hierarchy = "data_inicio"
autocomplete_fields = (
# "tipo_evento",
"casa_anfitria",
)
search_fields = (
"nome",
"tipo_evento__nome",
"participante__casa_legislativa__search_text",
"participante__casa_legislativa__municipio__search_text",
"solicitante",
"num_processo",
)
inlines = [ParticipanteInline]
def get_form(self, request, obj, change, **kwargs):
form = super().get_form(request, obj, change, **kwargs)
form.base_fields["tipo_evento"].queryset = form.base_fields[
"tipo_evento"
].queryset.filter(categoria=TipoEvento.CATEGORIA_VISITA)
form.base_fields["casa_anfitria"].required = True
form.base_fields["local_visita"].required = True
if obj is None:
form.base_fields["tipo_evento"].initial = (
form.base_fields["tipo_evento"].queryset.first().pk
)
return form
@admin.display(description=_("SIGAD"), ordering="num_processo")
def get_link_sigad(self, obj):
if obj.pk is None:
return ""
return mark_safe(obj.get_sigad_url())
def declaracao_report(self, request, object_id):
evento = get_object_or_404(Evento, id=object_id)
if request.method == "POST":
form = SelecionaModeloForm(request.POST)
if form.is_valid():
modelo = form.cleaned_data["modelo"]
membro = (
evento.equipe_set.filter(assina_oficio=True).first()
or evento.equipe_set.first()
)
if membro:
servidor = membro.membro
else:
servidor = None
template_string = (
"""
{% extends "eventos/declaracao_pdf.html" %}
{% block text_body %}"""
+ modelo.texto
+ """
{% endblock %}
"""
)
context = Context(
{
"pagesize": modelo.formato,
"pagemargin": modelo.margem,
"evento": evento,
"servidor": servidor,
"data": evento.data_inicio,
}
)
string = Template(template_string).render(context)
# return HttpResponse(string)
response = HttpResponse(
headers={
"Content-Type": "application/pdf",
"Content-Disposition": 'attachment; filename="declaração.pdf"',
}
)
pdf = HTML(
string=string,
url_fetcher=DjangoURLFetcher(),
encoding="utf-8",
base_url=request.build_absolute_uri("/"),
)
pdf.write_pdf(target=response)
return response
else:
form = SelecionaModeloForm(
initial={"modelo": ModeloDeclaracao.objects.first().id}
)
context = {
**self.admin_site.each_context(request),
"title": _("Emitir declaração para os participantes da visita"),
"subtitle": str(evento) if evento else None,
"form": form,
"object_id": object_id,
"original": evento,
"evento_id": object_id,
"opts": self.model._meta,
"preserved_filters": self.get_preserved_filters(request),
}
return render(
request, "admin/eventos/evento/seleciona_modelo.html", context
)
def get_urls(self):
urls = super().get_urls()
model_info = self.get_model_info()
my_urls = [
path(
"<path:object_id>/declaracao/",
self.admin_site.admin_view(self.declaracao_report),
name="%s_%s_declaracaoreport" % model_info,
),
]
return my_urls + urls

82
sigi/apps/eventos/migrations/0067_localvisita_visita_evento_pesquisa_previa_and_more.py

@ -0,0 +1,82 @@
# Generated by Django 6.0.5 on 2026-10-08 15:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("casas", "0027_alter_orgao_email"),
("eventos", "0066_viw_eventos_paricipante"),
]
operations = [
migrations.CreateModel(
name="LocalVisita",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"nome",
models.CharField(
max_length=100, verbose_name="nome do local"
),
),
(
"descricao",
models.TextField(verbose_name="descrição do local"),
),
],
options={
"verbose_name": "local de visita",
"verbose_name_plural": "locais de visita",
},
),
migrations.CreateModel(
name="Visita",
fields=[],
options={
"verbose_name": "Visita",
"verbose_name_plural": "Visitas",
"proxy": True,
"indexes": [],
"constraints": [],
},
bases=("eventos.evento",),
),
migrations.AddField(
model_name="evento",
name="pesquisa_previa",
field=models.URLField(blank=True, verbose_name="pesquisa prévia"),
),
migrations.AlterField(
model_name="evento",
name="casa_anfitria",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="casas.orgao",
verbose_name="instituição / casa anfitriã",
),
),
migrations.AddField(
model_name="evento",
name="local_visita",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="eventos.localvisita",
verbose_name="local da visita",
),
),
]

27
sigi/apps/eventos/migrations/0068_acerta_casa_visitas.py

@ -0,0 +1,27 @@
# Generated by Django 6.0.5 on 2026-10-08 15:22
from django.db import migrations
def forward(apps, schema_editor):
Evento = apps.get_model("eventos", "Evento")
for visita in Evento.objects.filter(tipo_evento__categoria="V"):
if visita.convite_set.count() == 1:
visita.casa_anfitria = visita.convite_set.first().casa
visita.save()
def backward(apps, schema_editor):
Evento = apps.get_model("eventos", "Evento")
Orgao = apps.get_model("casas", "Orgao")
senado = Orgao.objects.get(tipo__sigla="SF")
Evento.objects.filter(tipo_evento__categoria="V").update(
casa_anfitria=senado
)
class Migration(migrations.Migration):
dependencies = [
("eventos", "0067_localvisita_visita_evento_pesquisa_previa_and_more"),
]
operations = [migrations.RunPython(forward, backward)]

69
sigi/apps/eventos/models.py

@ -15,14 +15,23 @@ from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.utils.formats import localize
from sigi.apps.casas.models import Orgao, Servidor from sigi.apps.casas.models import Orgao, Servidor
from sigi.apps.contatos.models import UnidadeFederativa from sigi.apps.contatos.models import UnidadeFederativa
from sigi.apps.espacos.models import Reserva from sigi.apps.espacos.models import Reserva
from sigi.apps.utils import get_sigad_url from sigi.apps.utils import get_sigad_url, periodo
from sigi.apps.utils.templatetags.model_fields import verbose_name
from sigi.apps.eventos.saberes import EventoSaberes from sigi.apps.eventos.saberes import EventoSaberes
class VisitaManager(models.Manager):
def get_queryset(self):
return (
super()
.get_queryset()
.filter(tipo_evento__categoria=TipoEvento.CATEGORIA_VISITA)
)
class TipoEvento(models.Model): class TipoEvento(models.Model):
CATEGORIA_CURSO = "C" CATEGORIA_CURSO = "C"
CATEGORIA_ENCONTRO = "E" CATEGORIA_ENCONTRO = "E"
@ -267,6 +276,18 @@ class AnexoSolicitacao(models.Model):
return _(f"{self.descricao} publicado em {self.data_pub}") return _(f"{self.descricao} publicado em {self.data_pub}")
class LocalVisita(models.Model):
nome = models.CharField(_("nome do local"), max_length=100)
descricao = models.TextField(_("descrição do local"))
class Meta:
verbose_name = _("local de visita")
verbose_name_plural = _("locais de visita")
def __str__(self):
return self.nome
class Evento(models.Model): class Evento(models.Model):
STATUS_PREVISTO = "P" STATUS_PREVISTO = "P"
STATUS_AUTORIZADO = "O" STATUS_AUTORIZADO = "O"
@ -350,7 +371,7 @@ class Evento(models.Model):
casa_anfitria = models.ForeignKey( casa_anfitria = models.ForeignKey(
Orgao, Orgao,
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_("Casa anfitriã"), verbose_name=_("instituição / casa anfitriã"),
blank=True, blank=True,
null=True, null=True,
) )
@ -358,7 +379,15 @@ class Evento(models.Model):
Reserva, blank=True, null=True, on_delete=models.PROTECT Reserva, blank=True, null=True, on_delete=models.PROTECT
) )
local = models.TextField(_("Local do evento"), blank=True) local = models.TextField(_("Local do evento"), blank=True)
local_visita = models.ForeignKey(
LocalVisita,
verbose_name=_("local da visita"),
on_delete=models.PROTECT,
blank=True,
null=True,
)
observacao = models.TextField(_("Observações e anotações"), blank=True) observacao = models.TextField(_("Observações e anotações"), blank=True)
pesquisa_previa = models.URLField(_("pesquisa prévia"), blank=True)
publico_alvo = models.TextField(_("Público alvo"), blank=True) publico_alvo = models.TextField(_("Público alvo"), blank=True)
total_participantes = models.PositiveIntegerField( total_participantes = models.PositiveIntegerField(
_("total de participantes/aprovados"), _("total de participantes/aprovados"),
@ -489,9 +518,20 @@ class Evento(models.Model):
] ]
def __str__(self): def __str__(self):
return _( inicio = (
f"{self.nome} ({self.tipo_evento}): " datetime.datetime.combine(self.data_inicio, self.hora_inicio)
f"de {self.data_inicio} a {self.data_termino}" if self.data_inicio and self.hora_inicio
else self.data_inicio
)
termino = (
datetime.datetime.combine(self.data_termino, self.hora_termino)
if self.data_termino and self.hora_termino
else self.data_termino
)
return _("{nome} ({tipo_evento}): {periodo}").format(
nome=self.nome,
tipo_evento=self.tipo_evento,
periodo=periodo(inicio, termino),
) )
def get_absolute_url(self): def get_absolute_url(self):
@ -614,8 +654,12 @@ class Evento(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Força que a casa anfitriã de todas as visitas seja Senado # Força que a casa anfitriã de todas as visitas seja Senado
# Gertik #165751 # Gertik #165751
if self.tipo_evento.categoria == TipoEvento.CATEGORIA_VISITA: # Mudado em 08/10/2025 porque as visitas foram destacadas dos eventos
self.casa_anfitria = Orgao.objects.get(tipo__sigla="SF") # e agora o campo casa_anfitria servirá para indicar a Casa que fará
# a visita
# if self.tipo_evento.categoria == TipoEvento.CATEGORIA_VISITA:
# self.casa_anfitria = Orgao.objects.get(tipo__sigla="SF")
# Limpa casas convidadas se a categoria do evento for Oficina # Limpa casas convidadas se a categoria do evento for Oficina
# e está vinculado com um curso no Saberes. # e está vinculado com um curso no Saberes.
# Gertik #165984: https://gertiq.senado.leg.br/redmine/issues/165984#3em-seguida-DELETAR-as-Casas-convidadas-DOS-registros-de-OFICINAS # Gertik #165984: https://gertiq.senado.leg.br/redmine/issues/165984#3em-seguida-DELETAR-as-Casas-convidadas-DOS-registros-de-OFICINAS
@ -732,6 +776,15 @@ class Evento(models.Model):
ajusta_data(item, self.data_termino) ajusta_data(item, self.data_termino)
class Visita(Evento):
objects = VisitaManager()
class Meta:
proxy = True
verbose_name = _("Visita")
verbose_name_plural = _("Visitas")
class Funcao(models.Model): class Funcao(models.Model):
nome = models.CharField(_("Função na equipe de evento"), max_length=100) nome = models.CharField(_("Função na equipe de evento"), max_length=100)
descricao = models.TextField(_("Descrição da função")) descricao = models.TextField(_("Descrição da função"))

7
sigi/apps/eventos/templates/admin/eventos/evento/change_form_object_tools.html

@ -41,11 +41,4 @@
{% icon "refresh" %} <span id="text-tool-updateparticipantes" class="d-lg-none">{% translate "Atualizar lista de participantes (Saberes)" %}</span> {% icon "refresh" %} <span id="text-tool-updateparticipantes" class="d-lg-none">{% translate "Atualizar lista de participantes (Saberes)" %}</span>
</a> </a>
{% endif %} {% endif %}
{% if original.tipo_evento.categoria == "V" %}
{% url opts|admin_urlname:'declaracaoreport' original.pk|admin_urlquote as declaracao_url %}
<a class="addlink nav-link declaracao" href="{% add_preserved_filters declaracao_url %}" aria-labelledby="text-tool-declaracao" title="{% translate 'Declaração' %}">
{% icon "pdf" %} <span id="text-tool-declaracao" class="d-lg-none">{% translate "Declaração" %}</span>
</a>
{% endif %}
{% endblock %} {% endblock %}

10
sigi/apps/eventos/templates/admin/eventos/visita/change_form_object_tools.html

@ -0,0 +1,10 @@
{% extends "admin/change_form_object_tools.html" %}
{% load i18n admin_urls djbs_extras %}
{% block object-tools-items %}
{{ block.super }}
{% url opts|admin_urlname:'declaracaoreport' original.pk|admin_urlquote as declaracao_url %}
<a class="addlink nav-link declaracao" href="{% add_preserved_filters declaracao_url %}" aria-labelledby="text-tool-declaracao" title="{% translate 'Declaração' %}">
{% icon "pdf" %} <span id="text-tool-declaracao" class="d-lg-none">{% translate "Declaração" %}</span>
</a>
{% endblock %}

34
sigi/apps/utils/__init__.py

@ -1,11 +1,15 @@
import datetime
import re import re
from threading import local
from unicodedata import normalize from unicodedata import normalize
from django.contrib import admin as django_admin from django.contrib import admin as django_admin
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.db import models from django.db import models
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.formats import localize
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class SearchField(models.TextField): class SearchField(models.TextField):
@ -147,3 +151,33 @@ def mask_sigad(num_sigad):
return f"{orgao}.{sequencial}/{ano}-{dv}" return f"{orgao}.{sequencial}/{ano}-{dv}"
else: else:
return num_sigad return num_sigad
def periodo(inicio, termino):
if isinstance(inicio, datetime.datetime) and isinstance(
termino, datetime.datetime
):
data_inicio = inicio.date()
hora_inicio = inicio.time()
data_termino = termino.date()
hora_termino = termino.time()
if data_inicio == data_termino:
if hora_inicio == hora_termino:
result = _("em {data_inicio}, às {hora_inicio}")
else:
result = _(
"em {data_inicio}, de {hora_inicio} até {hora_termino}"
)
else:
result = _("de {inicio} até {termino}")
else:
data_inicio = hora_inicio = data_termino = hora_termino = None
result = _("de {inicio} até {termino}")
return result.format(
inicio=localize(inicio),
termino=localize(termino),
data_inicio=localize(data_inicio),
hora_inicio=localize(hora_inicio),
data_termino=localize(data_termino),
hora_termino=localize(hora_termino),
)

5
sigi/menu_conf.yaml

@ -144,8 +144,7 @@ main_menu:
view_name: admin:eventos_evento_changelist view_name: admin:eventos_evento_changelist
querystr: tipo_evento__categoria__exact=S querystr: tipo_evento__categoria__exact=S
- title: Visitas Interlegis - title: Visitas Interlegis
view_name: admin:eventos_evento_changelist view_name: admin:eventos_visita_changelist
querystr: tipo_evento__categoria__exact=V
- title: Servidores - title: Servidores
icon: badge icon: badge
children: children:
@ -178,6 +177,8 @@ main_menu:
view_name: admin:parlamentares_partido_changelist view_name: admin:parlamentares_partido_changelist
- title: Espaços para eventos - title: Espaços para eventos
view_name: admin:espacos_espaco_changelist view_name: admin:espacos_espaco_changelist
- title: Locais para Visitas
view_name: admin:eventos_localvisita_changelist
- title: Recursos para eventos - title: Recursos para eventos
view_name: admin:espacos_recurso_changelist view_name: admin:espacos_recurso_changelist
# contato_menu: # contato_menu:

Loading…
Cancel
Save