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 (
Checklist,
Cronograma,
LocalVisita,
ModeloDeclaracao,
Modulo,
Participante,
@ -48,6 +49,7 @@ from sigi.apps.eventos.models import (
ItemSolicitado,
Funcao,
Evento,
Visita,
Equipe,
Convite,
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):
model = Checklist
@ -905,6 +943,11 @@ class ModeloDeclaracaoAdmin(admin.ModelAdmin):
formfield_overrides = {HTMLField: {"widget": AdminTinyMCE}}
@admin.register(LocalVisita)
class LocalVisitaAdmin(admin.ModelAdmin):
list_display = ("nome", "descricao")
@admin.register(Evento)
class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
form = EventoAdminForm
@ -1109,7 +1152,11 @@ class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
def get_queryset(self, request):
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(
custo_total=Sum(
(F("equipe__qtde_diarias") * F("equipe__valor_diaria"))
@ -1739,3 +1786,170 @@ class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
)
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.safestring import mark_safe
from django.utils.translation import gettext as _
from django.utils.formats import localize
from sigi.apps.casas.models import Orgao, Servidor
from sigi.apps.contatos.models import UnidadeFederativa
from sigi.apps.espacos.models import Reserva
from sigi.apps.utils import get_sigad_url
from sigi.apps.utils.templatetags.model_fields import verbose_name
from sigi.apps.utils import get_sigad_url, periodo
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):
CATEGORIA_CURSO = "C"
CATEGORIA_ENCONTRO = "E"
@ -267,6 +276,18 @@ class AnexoSolicitacao(models.Model):
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):
STATUS_PREVISTO = "P"
STATUS_AUTORIZADO = "O"
@ -350,7 +371,7 @@ class Evento(models.Model):
casa_anfitria = models.ForeignKey(
Orgao,
on_delete=models.PROTECT,
verbose_name=_("Casa anfitriã"),
verbose_name=_("instituição / casa anfitriã"),
blank=True,
null=True,
)
@ -358,7 +379,15 @@ class Evento(models.Model):
Reserva, blank=True, null=True, on_delete=models.PROTECT
)
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)
pesquisa_previa = models.URLField(_("pesquisa prévia"), blank=True)
publico_alvo = models.TextField(_("Público alvo"), blank=True)
total_participantes = models.PositiveIntegerField(
_("total de participantes/aprovados"),
@ -489,9 +518,20 @@ class Evento(models.Model):
]
def __str__(self):
return _(
f"{self.nome} ({self.tipo_evento}): "
f"de {self.data_inicio} a {self.data_termino}"
inicio = (
datetime.datetime.combine(self.data_inicio, self.hora_inicio)
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):
@ -614,8 +654,12 @@ class Evento(models.Model):
def save(self, *args, **kwargs):
# Força que a casa anfitriã de todas as visitas seja Senado
# Gertik #165751
if self.tipo_evento.categoria == TipoEvento.CATEGORIA_VISITA:
self.casa_anfitria = Orgao.objects.get(tipo__sigla="SF")
# Mudado em 08/10/2025 porque as visitas foram destacadas dos eventos
# 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
# 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
@ -732,6 +776,15 @@ class Evento(models.Model):
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):
nome = models.CharField(_("Função na equipe de evento"), max_length=100)
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>
</a>
{% 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 %}

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
from threading import local
from unicodedata import normalize
from django.contrib import admin as django_admin
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from django.template.loader import render_to_string
from django.utils.encoding import force_str
from django.utils.formats import localize
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class SearchField(models.TextField):
@ -147,3 +151,33 @@ def mask_sigad(num_sigad):
return f"{orgao}.{sequencial}/{ano}-{dv}"
else:
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
querystr: tipo_evento__categoria__exact=S
- title: Visitas Interlegis
view_name: admin:eventos_evento_changelist
querystr: tipo_evento__categoria__exact=V
view_name: admin:eventos_visita_changelist
- title: Servidores
icon: badge
children:
@ -178,6 +177,8 @@ main_menu:
view_name: admin:parlamentares_partido_changelist
- title: Espaços para eventos
view_name: admin:espacos_espaco_changelist
- title: Locais para Visitas
view_name: admin:eventos_localvisita_changelist
- title: Recursos para eventos
view_name: admin:espacos_recurso_changelist
# contato_menu:

Loading…
Cancel
Save