diff --git a/sigi/apps/casas/views.py b/sigi/apps/casas/views.py index 0572a3d..e18bfb8 100644 --- a/sigi/apps/casas/views.py +++ b/sigi/apps/casas/views.py @@ -434,7 +434,7 @@ class CnpjErradoReport( orgaos.append(orgao) return orgaos - def get_dataset(self): + def get_dataset(self, context): return ( [ {f: getattr(o, f) for f in self.list_fields} diff --git a/sigi/apps/eventos/admin.py b/sigi/apps/eventos/admin.py index d16b8bf..6e1b0a7 100644 --- a/sigi/apps/eventos/admin.py +++ b/sigi/apps/eventos/admin.py @@ -1,5 +1,4 @@ import datetime -import pandas as pd import time from admin_auto_filters.filters import AutocompleteFilter from moodle import Moodle @@ -12,9 +11,6 @@ from django.db.models import ( Q, Sum, Avg, - Min, - Max, - Prefetch, Case, When, ) @@ -54,7 +50,10 @@ from sigi.apps.eventos.models import ( ParticipantesEvento, ) from sigi.apps.eventos.forms import EventoAdminForm, SelecionaModeloForm -from sigi.apps.servidores.models import Servidor +from sigi.apps.eventos.views import ( + context_custos_eventos, + context_custos_servidor, +) from sigi.apps.utils import abreviatura from sigi.apps.utils.filters import DateRangeFilter from sigi.apps.utils.mixins import ( @@ -1481,172 +1480,23 @@ class EventoAdmin(AsciifyQParameter, CartExportReportMixin, admin.ModelAdmin): ) def custos_eventos_report(self, request): - my_decimal_field = models.DecimalField(max_digits=14, decimal_places=2) - equipe_qs = Equipe.objects.annotate( - total_diarias=(F("qtde_diarias") * F("valor_diaria")), - antecedencia=ExtractDay( - F("evento__data_inicio") - F("emissao_passagens") - ), - ) - eventos = ( - self.get_queryset(request) - .annotate( - duracao_dias=( - ExtractDay(F("data_termino") - F("data_inicio")) + 1 - ), - qtde_diarias=Sum("equipe__qtde_diarias"), - vlr_tot_diarias=Sum( - F("equipe__qtde_diarias") * F("equipe__valor_diaria"), - output_field=my_decimal_field, - ), - vlr_tot_passagens=Sum("equipe__total_passagens"), - custo_total=F("vlr_tot_diarias") + F("vlr_tot_passagens"), - custo_medio_participante=Cast( - Case( - When(total_participantes__lte=0, then=0), - default=F("custo_total") / F("total_participantes"), - output_field=my_decimal_field, - ), - output_field=my_decimal_field, - ), - custo_medio_membro=Cast( - F("custo_total") / Count("equipe__membro"), - output_field=my_decimal_field, - ), - tot_membros=Count("equipe"), - ) - .prefetch_related( - Prefetch( - "equipe_set", queryset=equipe_qs, to_attr="equipe_ext" - ) - ) - ) - resumo = eventos.aggregate( - qtde_oficinas=Count("id"), - tot_participantes=Sum("total_participantes"), - media_participantes=Cast( - 1.0 * F("tot_participantes") / F("qtde_oficinas"), - output_field=my_decimal_field, - ), - min_participantes=Min("total_participantes"), - max_participantes=Max("total_participantes"), - tot_servidores=Sum("tot_membros"), - media_membros=Cast( - 1.0 * Sum("tot_membros") / F("qtde_oficinas"), - output_field=my_decimal_field, - ), - min_membros=Min("tot_membros"), - max_membros=Max("tot_membros"), - tot_dias=Sum("duracao_dias"), - media_dias=Cast( - 1.0 * F("tot_dias") / F("qtde_oficinas"), - output_field=my_decimal_field, - ), - tot_diarias=Sum("qtde_diarias"), - media_diarias=Cast( - 1.0 * F("tot_diarias") / F("qtde_oficinas"), - output_field=my_decimal_field, - ), - tot_custo_total=Sum("custo_total"), - tot_custo_diarias=Sum("vlr_tot_diarias"), - tot_custo_passagens=Sum("vlr_tot_passagens"), - media_custo_total=Cast( - F("tot_custo_total") / F("qtde_oficinas"), - output_field=my_decimal_field, - ), - media_custo_diarias=Cast( - F("tot_custo_diarias") / F("qtde_oficinas"), - output_field=my_decimal_field, - ), - media_custo_passagens=Cast( - F("tot_custo_passagens") / F("qtde_oficinas"), - output_field=my_decimal_field, - ), - media_custo_participantes=Cast( - F("tot_custo_total") / F("tot_participantes"), - output_field=my_decimal_field, - ), - media_custo_membro=Cast( - F("tot_custo_total") / Sum("tot_membros"), - output_field=my_decimal_field, - ), - ) - resumo.update( - eventos.aggregate( - media_antecedencia=Avg( - ExtractDay( - F("data_inicio") - F("equipe__emissao_passagens") - ) - ), - min_antecedencia=Min( - ExtractDay( - F("data_inicio") - F("equipe__emissao_passagens") - ) - ), - max_antecedencia=Max( - ExtractDay( - F("data_inicio") - F("equipe__emissao_passagens") - ) - ), - ) - ) - - f_valor_diarias = F("equipe__qtde_diarias") * F("equipe__valor_diaria") - f_custo_total = (f_valor_diarias) + F("equipe__total_passagens") - - extrato = ( - self.get_queryset(request) - .order_by("casa_anfitria__municipio__uf__regiao") - .annotate( - regiao=F("casa_anfitria__municipio__uf__regiao"), - tot_diarias=Sum(f_valor_diarias), - tot_passagens=Sum("equipe__total_passagens"), - tot_custo=Sum(f_custo_total), - ) - .values("regiao", "tot_diarias", "tot_passagens", "tot_custo") - ) - - df = ( - pd.DataFrame(extrato) - .set_index("regiao") - .groupby("regiao") - .aggregate(["sum", "min", "max", "mean"]) - .fillna(0) - ) - - custos_regiao = [ - { - "nome": nome, - "extrato": df.loc[sigla] if sigla in df.index else None, - } - for sigla, nome in UnidadeFederativa.REGIAO_CHOICES - ] - - data_inicio = ( + context = context_custos_eventos(self.get_queryset(request)) + context["data_inicio"] = ( self.get_queryset(request) .order_by("data_inicio") .first() .data_inicio ) - data_fim = ( + context["data_fim"] = ( self.get_queryset(request) .order_by("data_termino") .last() .data_termino ) - - context = { - "eventos": eventos.order_by("data_inicio"), - "resumo": resumo, - "custos_regiao": custos_regiao, - "title": _("Custos por eventos"), - "data_inicio": data_inicio, - "data_fim": data_fim, - } return WeasyTemplateResponse( filename=f"custos_eventos-{timezone.localdate()}.pdf", request=request, - template="admin/eventos/custos_eventos_report.html", + template="admin/eventos/custos_eventos_report_pdf.html", context=context, content_type="application/pdf", ) @@ -1654,83 +1504,25 @@ class EventoAdmin(AsciifyQParameter, CartExportReportMixin, admin.ModelAdmin): custos_eventos_report.title = _("Custos por eventos") def custos_servidor_report(self, request): - equipe_qs = Equipe.objects.filter( - evento__in=self.get_queryset(request) - ) - f_total_diarias = F("equipe_evento__qtde_diarias") * F( - "equipe_evento__valor_diaria" - ) - my_decimal_field = models.DecimalField(max_digits=14, decimal_places=2) - - servidores = ( - Servidor.objects.distinct() - .filter(equipe_evento__evento__in=self.get_queryset(request)) - .prefetch_related( - Prefetch( - "equipe_evento", queryset=equipe_qs, to_attr="equipe_ext" - ) - ) - .annotate( - qtde_eventos=Count("equipe_evento"), - qtde_diarias=Sum("equipe_evento__qtde_diarias"), - media_diarias=Cast( - Sum(f_total_diarias / F("equipe_evento__qtde_diarias")), - output_field=my_decimal_field, - ), - total_diarias=Sum(f_total_diarias), - total_passagens=Sum("equipe_evento__total_passagens"), - total_custo=Sum( - F("equipe_evento__total_passagens") + f_total_diarias - ), - ) - ) - totais = ( - Servidor.objects.distinct() - .filter(equipe_evento__evento__in=self.get_queryset(request)) - .prefetch_related( - Prefetch( - "equipe_evento", queryset=equipe_qs, to_attr="equipe_ext" - ) - ) - .aggregate( - qtde_eventos=Count("equipe_evento"), - qtde_diarias=Sum("equipe_evento__qtde_diarias"), - media_diarias=Cast( - Avg(f_total_diarias / F("equipe_evento__qtde_diarias")), - output_field=my_decimal_field, - ), - total_diarias=Sum(f_total_diarias), - total_passagens=Sum("equipe_evento__total_passagens"), - total_custo=Sum( - F("equipe_evento__total_passagens") + f_total_diarias - ), - ) - ) - - data_inicio = ( + context = context_custos_servidor(self.get_queryset(request)) + context["data_inicio"] = ( self.get_queryset(request) .order_by("data_inicio") .first() .data_inicio ) - data_fim = ( + context["data_fim"] = ( self.get_queryset(request) + .exclude(data_termino=None) .order_by("data_termino") .last() .data_termino ) - context = { - "servidores": servidores.order_by("nome_completo"), - "totais": totais, - "title": _("Custos por servidor"), - "data_inicio": data_inicio, - "data_fim": data_fim, - } return WeasyTemplateResponse( filename=f"custos_servidor-{timezone.localdate()}.pdf", request=request, - template="admin/eventos/custos_servidor_report.html", + template="admin/eventos/custos_servidor_report_pdf.html", context=context, content_type="application/pdf", ) diff --git a/sigi/apps/eventos/admin_urls.py b/sigi/apps/eventos/admin_urls.py index d00601e..d55e0b3 100644 --- a/sigi/apps/eventos/admin_urls.py +++ b/sigi/apps/eventos/admin_urls.py @@ -17,4 +17,14 @@ urlpatterns = [ views.solicitacoes_por_periodo, name="eventos_solicitacoesporperiodo", ), + path( + "custoseventos/", + views.CustosEventosReport.as_view(), + name="eventos_custoseventos", + ), + path( + "custosservidor/", + views.CustosServidorReport.as_view(), + name="eventos_custosservidor", + ), ] diff --git a/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report.html b/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report.html index d22f72f..20a5466 100644 --- a/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report.html +++ b/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report.html @@ -1,357 +1,22 @@ -{% extends 'pdf/base_report.html' %} -{% load static i18n sigi_tags %} - -{% block page_size %}A4 landscape{% endblock page_size %} -{% block page_margin %}3cm 1cm 2cm 1cm{% endblock page_margin %}; - -{% block extra_style %} - {{ block.super }} - aside { - margin-left: 8px; - font-size: 0.8em; - color: #666; - } - blockquote { - margin: 12px 0 12px; - padding-left: 1.5rem; - border-left: 5px solid #ee6e73; - font-size: 1.4em; - font-weight: bold; - } - tr:nth-child(even) { - background-color: initial; - } - .even-row { - background-color: #d2d2d2 !important; - } - - .sessao-resumo { - align-items: stretch; - display: flex; - flex-wrap: wrap; - width: 100%; - margin-top: 24px; - } - .card-resumo { - background-color: #eeeeef; - border-radius: 2px; - box-sizing: border-box; - margin: 6px; - flex-basis: 49%; - padding: 0 6px 6px 6px; - position: relative; - width: 100%; - } - .card-resumo.full { - flex-basis: 98%; - } - - .index-cell { - width: 2em; - text-align: center; - } - .label-resumo { - min-width: 30em; - } - .timestamp-container { - width: 100%; - margin: 24px 10px; - border-left: 5px solid #ee6e73; - font-size: 1.3em; - } - .timestamp-row { - display: flex; - flex-wrap: wrap; - margin-bottom: 6px; - } - .timestamp-col { - position: relative; - padding-left: 15px; - padding-right: 15px; - } - .timestamp-label { - flex: 0 0 12%; - max-width: 12%; - font-weight: bold; - } +{% extends 'utils/report/report.html' %} +{% load i18n %} + +{% block extrastyle %} +{{ block.super }} + {% endblock %} -{% block main_content %} -
-
-
- {% trans "Data inicial" %}: -
-
- {{ data_inicio|date:"SHORT_DATE_FORMAT" }}
-
-
-
-
- {% trans "Data final" %}: -
-
- {{ data_fim|date:"SHORT_DATE_FORMAT" }} -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - {% for evento in eventos %} - {% with equipe_count=evento.equipe_ext|length|default:1 %} - - - - - - - - {% for membro in evento.equipe_ext %} - {% if not forloop.first %}{% endif %} - - - - - - - - {% if forloop.first %} - - - - {% endif %} - - {% empty %} - - - - - - {% endfor %} - {% endwith %} - {% endfor %} - -
{% trans "Início / término" %}{% trans "SIGAD" %}{% trans "Evento" %}{% trans "Casa anfitriã" %}{% trans "Duração do evento (dias)" %}{% trans "Total de participantes" %}{% trans "Equipe" %}{% trans "Custos" %}
{% trans "Nome" %}{% trans "Função" %}{% trans "Qtde de diárias" %}{% trans "Valor total diárias" %}{% trans "Valor total passagens" %}{% trans "Emissão das passagens" %}{% trans "Antecedência das passagens (dias)" %}{% trans "Custo total" %}{% trans "Custo médio participante" %}{% trans "Custo médio equipe" %}
- {% blocktranslate with inicio=evento.data_inicio|date:"SHORT_DATE_FORMAT" termino=evento.data_termino|date:"SHORT_DATE_FORMAT" %} - {{ inicio }} a {{ termino }} - {% endblocktranslate %} - {{ evento.num_processo }} - {% blocktranslate with nome=evento.nome turma=evento.turma %} - {{ nome }} - turma {{ turma }} - {% endblocktranslate %} - {{ evento.casa_anfitria }}{{ evento.duracao_dias|default:"-" }}{{ evento.total_participantes|default:"-" }}
{{ membro.membro.get_apelido }}{{ membro.funcao }}{{ membro.qtde_diarias|floatformat:2|default:"-" }}{{ membro.total_diarias|floatformat:2|default:"-" }}{{ membro.total_passagens|floatformat:2|default:"-" }}{{ membro.emissao_passagens|default:"-" }}{{ membro.antecedencia|default:"-" }}{{ evento.custo_total|floatformat:2|default:"-" }}{{ evento.custo_medio_participante|floatformat:2|default:"-" }}{{ evento.custo_medio_membro|floatformat:2|default:"-" }}
{% trans "Equipe não definida" %}{{ evento.custo_total|floatformat:2|default:"-" }}{{ evento.custo_medio_participante|floatformat:2|default:"-" }}{{ evento.custo_medio_membro|floatformat:2|default:"-" }}
- - {# Resumo do relatório #} - -
-
-
{% trans "Dados gerais" %}
- - - - - - - - - - - - - - - - - - - - - - - - - - -
{% cycle "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" as letra %}{% trans "Quantidade de eventos" %}{{ resumo.qtde_oficinas|default:"-" }}
{% cycle letra %}{% trans "Total de participantes" %}{{ resumo.tot_participantes|default:"-" }}
{% cycle letra %}{% trans "Média de participantes por evento" %} [B / A]{{ resumo.media_participantes|default:"-" }}
{% cycle letra %}{% trans "Mínimo de participantes" %}{{ resumo.min_participantes|default:"-" }}
{% cycle letra %}{% trans "Máximo de participantes" %}{{ resumo.max_participantes|default:"-" }}
-
-
-
{% trans "Equipes" %}
- - - - - - - - - - - - - - - - - - - - - -
{% cycle letra %}{% trans "Total de servidores em missão" %}{{ resumo.tot_servidores|default:"-" }}
{% cycle letra %}{% trans "Tamanho médio das equipes [F / A]" %}{{ resumo.media_membros|default:"-" }}
{% cycle letra %}{% trans "Menor equipe" %}{{ resumo.min_membros|default:"-" }}
{% cycle letra %}{% trans "Maior equipe" %}{{ resumo.max_membros|default:"-" }}
-
-
-
{% trans "Tempo" %}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% cycle letra %}{% trans "Total de dias de evento" %}{{ resumo.tot_dias|default:"-" }}
{% cycle letra %}{% trans "Duração média dos eventos (dias) [J / A]" %}{{ resumo.media_dias|default:"-" }}
{% cycle letra %}{% trans "Total de diárias" %}{{ resumo.tot_diarias|default:"-" }}
{% cycle letra %}{% trans "Média de diárias por evento [L / A]" %}{{ resumo.media_diarias|default:"-" }}
{% cycle letra %}{% trans "Antecedência média na emissão de passagens" %}{{ resumo.media_antecedencia|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Menor antecedência" %}{{ resumo.min_antecedencia|default:"-" }}
{% cycle letra %}{% trans "Maior antecedência" %}{{ resumo.max_antecedencia|default:"-" }}
-
-
-
{% trans "Custos" %}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% cycle letra %}{% trans "Custo total" %}{{ resumo.tot_custo_total|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Total com diárias" %}{{ resumo.tot_custo_diarias|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Total com passagens" %}{{ resumo.tot_custo_passagens|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Custo médio dos eventos [Q / A]" %}{{ resumo.media_custo_total|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Custo médio de diárias por evento [R / A]" %}{{ resumo.media_custo_diarias|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Custo médio de passagens por evento [S / A]" %}{{ resumo.media_custo_passagens|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Custo médio por participante [Q / B]" %}{{ resumo.media_custo_participantes|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Gasto médio por membro da equipe" %}{{ resumo.media_custo_membro|floatformat:2|default:"-" }}
-
-
-
{% trans "Custos por região" %}
- - - - - - - - - - - - - - - - - - - - - - - - {% for data in custos_regiao %} - - - - - - - - - - - - - - - - {% endfor %} - -
{% trans "Região" %}{% trans "Custos com diárias" %}{% trans "Custos com passagens" %}{% trans "Custo total" %}
{% trans "Mínimo" %}{% trans "Médio" %}{% trans "Máximo" %}{% trans "Total" %}{% trans "Mínimo" %}{% trans "Médio" %}{% trans "Máximo" %}{% trans "Total" %}{% trans "Mínimo" %}{% trans "Médio" %}{% trans "Máximo" %}{% trans "Total" %}
{{ data.nome }}{{ data.extrato.tot_diarias.min|floatformat:2|default:"-" }}{{ data.extrato.tot_diarias.mean|floatformat:2|default:"-" }}{{ data.extrato.tot_diarias.max|floatformat:2|default:"-" }}{{ data.extrato.tot_diarias.sum|floatformat:2|default:"-" }}{{ data.extrato.tot_passagens.min|floatformat:2|default:"-" }}{{ data.extrato.tot_passagens.mean|floatformat:2|default:"-" }}{{ data.extrato.tot_passagens.max|floatformat:2|default:"-" }}{{ data.extrato.tot_passagens.sum|floatformat:2|default:"-" }}{{ data.extrato.tot_custo.min|floatformat:2|default:"-" }}{{ data.extrato.tot_custo.mean|floatformat:2|default:"-" }}{{ data.extrato.tot_custo.max|floatformat:2|default:"-" }}{{ data.extrato.tot_custo.sum|floatformat:2|default:"-" }}
-
-
-{% endblock %} \ No newline at end of file +{% block data %} + {% if eventos %} + {% include "admin/eventos/custos_eventos_report_snippet.html" %} + {% endif %} +{% endblock data %} diff --git a/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report_pdf.html b/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report_pdf.html new file mode 100644 index 0000000..ee58dd3 --- /dev/null +++ b/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report_pdf.html @@ -0,0 +1,101 @@ +{% extends 'pdf/base_report.html' %} +{% load static i18n sigi_tags %} + +{% block page_size %}A4 landscape{% endblock page_size %} +{% block page_margin %}3cm 1cm 2cm 1cm{% endblock page_margin %}; + +{% block extra_style %} + {{ block.super }} + aside { + margin-left: 8px; + font-size: 0.8em; + color: #666; + } + blockquote { + margin: 12px 0 12px; + padding-left: 1.5rem; + border-left: 5px solid #ee6e73; + font-size: 1.4em; + font-weight: bold; + } + tr:nth-child(even) { + background-color: initial; + } + .even-row { + background-color: #d2d2d2 !important; + } + + .sessao-resumo { + align-items: stretch; + display: flex; + flex-wrap: wrap; + width: 100%; + margin-top: 24px; + } + .card-resumo { + background-color: #eeeeef; + border-radius: 2px; + box-sizing: border-box; + margin: 6px; + flex-basis: 49%; + padding: 0 6px 6px 6px; + position: relative; + width: 100%; + } + .card-resumo.full { + flex-basis: 98%; + } + + .index-cell { + width: 2em; + text-align: center; + } + .label-resumo { + min-width: 30em; + } + .timestamp-container { + width: 100%; + margin: 24px 10px; + border-left: 5px solid #ee6e73; + font-size: 1.3em; + } + .timestamp-row { + display: flex; + flex-wrap: wrap; + margin-bottom: 6px; + } + .timestamp-col { + position: relative; + padding-left: 15px; + padding-right: 15px; + } + .timestamp-label { + flex: 0 0 12%; + max-width: 12%; + font-weight: bold; + } +{% endblock %} + +{% block main_content %} +
+
+
+ {% trans "Data inicial" %}: +
+
+ {{ data_inicio|date:"SHORT_DATE_FORMAT" }}
+
+
+
+
+ {% trans "Data final" %}: +
+
+ {{ data_fim|date:"SHORT_DATE_FORMAT" }} +
+
+
+ +{% include "admin/eventos/custos_eventos_report_snippet.html" %} + +{% endblock %} \ No newline at end of file diff --git a/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report_snippet.html b/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report_snippet.html new file mode 100644 index 0000000..24a364b --- /dev/null +++ b/sigi/apps/eventos/templates/admin/eventos/custos_eventos_report_snippet.html @@ -0,0 +1,263 @@ +{% load static i18n sigi_tags %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% for evento in eventos %} + {% with equipe_count=evento.equipe_ext|length|default:1 %} + + + + + + + + {% for membro in evento.equipe_ext %} + {% if not forloop.first %}{% endif %} + + + + + + + + {% if forloop.first %} + + + + {% endif %} + + {% empty %} + + + + + + {% endfor %} + {% endwith %} + {% endfor %} + +
{% trans "Início / término" %}{% trans "SIGAD" %}{% trans "Evento" %}{% trans "Casa anfitriã" %}{% trans "Dur. (dias)" %}{% trans "Tot part." %}{% trans "Equipe" %}{% trans "Custo" %}
{% trans "Nome" %}{% trans "Função" %}{% trans "Diárias" %}{% trans "Passagens" %}
{% trans "Qtde." %}{% trans "Valor total" %}{% trans "Valor total" %}{% trans "Emissão" %}{% trans "Antec. (dias)" %}{% trans "Total" %}{% trans "Médio partic." %}{% trans "Médio equipe" %}
+ {% blocktranslate with inicio=evento.data_inicio|date:"SHORT_DATE_FORMAT" termino=evento.data_termino|date:"SHORT_DATE_FORMAT" %} + {{ inicio }} a {{ termino }} + {% endblocktranslate %} + {{ evento.num_processo }} + {% blocktranslate with nome=evento.nome turma=evento.turma %} + {{ nome }} - turma {{ turma }} + {% endblocktranslate %} + {{ evento.casa_anfitria|default:"" }}{{ evento.duracao_dias|default:"-" }}{{ evento.total_participantes|default:"-" }}
{{ membro.membro.get_apelido }}{{ membro.funcao }}{{ membro.qtde_diarias|floatformat:2|default:"-" }}{{ membro.total_diarias|floatformat:2|default:"-" }}{{ membro.total_passagens|floatformat:2|default:"-" }}{{ membro.emissao_passagens|default:"-" }}{{ membro.antecedencia|default:"-" }}{{ evento.custo_total|floatformat:2|default:"-" }}{{ evento.custo_medio_participante|floatformat:2|default:"-" }}{{ evento.custo_medio_membro|floatformat:2|default:"-" }}
{% trans "Equipe não definida" %}{{ evento.custo_total|floatformat:2|default:"-" }}{{ evento.custo_medio_participante|floatformat:2|default:"-" }}{{ evento.custo_medio_membro|floatformat:2|default:"-" }}
+ +{# Resumo do relatório #} + +
+
+
{% trans "Dados gerais" %}
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
{% cycle "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" as letra %}{% trans "Quantidade de eventos" %}{{ resumo.qtde_oficinas|default:"-" }}
{% cycle letra %}{% trans "Total de participantes" %}{{ resumo.tot_participantes|default:"-" }}
{% cycle letra %}{% trans "Média de participantes por evento" %} [B / A]{{ resumo.media_participantes|default:"-" }}
{% cycle letra %}{% trans "Mínimo de participantes" %}{{ resumo.min_participantes|default:"-" }}
{% cycle letra %}{% trans "Máximo de participantes" %}{{ resumo.max_participantes|default:"-" }}
+
+
+
{% trans "Equipes" %}
+ + + + + + + + + + + + + + + + + + + + + +
{% cycle letra %}{% trans "Total de servidores em missão" %}{{ resumo.tot_servidores|default:"-" }}
{% cycle letra %}{% trans "Tamanho médio das equipes [F / A]" %}{{ resumo.media_membros|default:"-" }}
{% cycle letra %}{% trans "Menor equipe" %}{{ resumo.min_membros|default:"-" }}
{% cycle letra %}{% trans "Maior equipe" %}{{ resumo.max_membros|default:"-" }}
+
+
+
{% trans "Tempo" %}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% cycle letra %}{% trans "Total de dias de evento" %}{{ resumo.tot_dias|default:"-" }}
{% cycle letra %}{% trans "Duração média dos eventos (dias) [J / A]" %}{{ resumo.media_dias|default:"-" }}
{% cycle letra %}{% trans "Total de diárias" %}{{ resumo.tot_diarias|default:"-" }}
{% cycle letra %}{% trans "Média de diárias por evento [L / A]" %}{{ resumo.media_diarias|default:"-" }}
{% cycle letra %}{% trans "Antecedência média na emissão de passagens" %}{{ resumo.media_antecedencia|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Menor antecedência" %}{{ resumo.min_antecedencia|default:"-" }}
{% cycle letra %}{% trans "Maior antecedência" %}{{ resumo.max_antecedencia|default:"-" }}
+
+
+
{% trans "Custos" %}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% cycle letra %}{% trans "Custo total" %}{{ resumo.tot_custo_total|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Total com diárias" %}{{ resumo.tot_custo_diarias|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Total com passagens" %}{{ resumo.tot_custo_passagens|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Custo médio dos eventos [Q / A]" %}{{ resumo.media_custo_total|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Custo médio de diárias por evento [R / A]" %}{{ resumo.media_custo_diarias|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Custo médio de passagens por evento [S / A]" %}{{ resumo.media_custo_passagens|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Custo médio por participante [Q / B]" %}{{ resumo.media_custo_participantes|floatformat:2|default:"-" }}
{% cycle letra %}{% trans "Gasto médio por membro da equipe" %}{{ resumo.media_custo_membro|floatformat:2|default:"-" }}
+
+
+
{% trans "Custos por região" %}
+ + + + + + + + + + + + + + + + + + + + + + + + {% for data in custos_regiao %} + + + + + + + + + + + + + + + + {% endfor %} + +
{% trans "Região" %}{% trans "Custos com diárias" %}{% trans "Custos com passagens" %}{% trans "Custo total" %}
{% trans "Mínimo" %}{% trans "Médio" %}{% trans "Máximo" %}{% trans "Total" %}{% trans "Mínimo" %}{% trans "Médio" %}{% trans "Máximo" %}{% trans "Total" %}{% trans "Mínimo" %}{% trans "Médio" %}{% trans "Máximo" %}{% trans "Total" %}
{{ data.nome }}{{ data.extrato.tot_diarias.min|floatformat:2|default:"-" }}{{ data.extrato.tot_diarias.mean|floatformat:2|default:"-" }}{{ data.extrato.tot_diarias.max|floatformat:2|default:"-" }}{{ data.extrato.tot_diarias.sum|floatformat:2|default:"-" }}{{ data.extrato.tot_passagens.min|floatformat:2|default:"-" }}{{ data.extrato.tot_passagens.mean|floatformat:2|default:"-" }}{{ data.extrato.tot_passagens.max|floatformat:2|default:"-" }}{{ data.extrato.tot_passagens.sum|floatformat:2|default:"-" }}{{ data.extrato.tot_custo.min|floatformat:2|default:"-" }}{{ data.extrato.tot_custo.mean|floatformat:2|default:"-" }}{{ data.extrato.tot_custo.max|floatformat:2|default:"-" }}{{ data.extrato.tot_custo.sum|floatformat:2|default:"-" }}
+
+
\ No newline at end of file diff --git a/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report.html b/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report.html index 179d7e0..610b619 100644 --- a/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report.html +++ b/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report.html @@ -1,133 +1,28 @@ -{% extends 'pdf/base_report.html' %} -{% load static i18n sigi_tags %} +{% extends 'utils/report/report.html' %} +{% load i18n %} -{% block page_size %}A4 landscape{% endblock page_size %} -{% block page_margin %}3cm 1cm 2cm 1cm{% endblock page_margin %}; - -{% block extra_style %} - {{ block.super }} - aside { - margin-left: 8px; - font-size: 0.8em; - color: #666; - } - blockquote { - margin: 12px 0 12px; - padding-left: 1.5rem; - border-left: 5px solid #ee6e73; - font-size: 1.4em; - font-weight: bold; - } - tr:nth-child(even) { - background-color: initial; - } - .even-row { - background-color: #d2d2d2 !important; - } - .sessao-resumo { - align-items: stretch; - display: flex; - flex-wrap: wrap; - width: 100%; - margin-top: 24px; - } - .card-resumo { - background-color: #eeeeef; - border-radius: 2px; - box-sizing: border-box; - margin: 6px; - flex-basis: 49%; - padding: 0 6px 6px 6px; - position: relative; - width: 100%; - } - .card-resumo.full { - flex-basis: 98%; - } - - .index-cell { - width: 2em; - text-align: center; - } - .label-resumo { - min-width: 30em; - } - .timestamp-container { - width: 100%; - margin: 24px 10px; - border-left: 5px solid #ee6e73; - font-size: 1.3em; - } - .timestamp-row { - display: flex; - flex-wrap: wrap; - margin-bottom: 6px; - } - .timestamp-col { - position: relative; - padding-left: 15px; - padding-right: 15px; - } - .timestamp-label { - flex: 0 0 12%; - max-width: 12%; - font-weight: bold; - } +{% block extrastyle %} +{{ block.super }} + {% endblock %} -{% block main_content %} -
-
-
- {% trans "Data inicial" %}: -
-
- {{ data_inicio|date:"SHORT_DATE_FORMAT" }}
-
-
-
-
- {% trans "Data final" %}: -
-
- {{ data_fim|date:"SHORT_DATE_FORMAT" }} +{% block data %} + {% if not servidores is None %} +
+
+
+ {% include "admin/eventos/custos_servidor_report_snippet.html" %}
- - - - - - - - - - - - - - - {% for servidor in servidores %} - - - - - - - - - - {% endfor %} - - - - - - - - - - -
{% trans "Membro da equipe" %}{% trans "Qtde eventos" %}{% trans "Qtde diárias" %}{% trans "Valor médio diária" %}{% trans "Total diárias" %}{% trans "Total passagens" %}{% trans "Total" %}
{{ servidor.nome_completo }}{{ servidor.qtde_eventos|default:"-" }}{{ servidor.qtde_diarias|floatformat:2|default:"-" }}{{ servidor.media_diarias|floatformat:2|default:"-" }}{{ servidor.total_diarias|floatformat:2|default:"-" }}{{ servidor.total_passagens|floatformat:2|default:"-" }}{{ servidor.total_custo|floatformat:2|default:"-" }}
{% trans "Totais" %}{{ totais.qtde_eventos|default:"-" }}{{ totais.qtde_diarias|floatformat:2|default:"-" }}{{ totais.media_diarias|floatformat:2|default:"-" }}{{ totais.total_diarias|floatformat:2|default:"-" }}{{ totais.total_passagens|floatformat:2|default:"-" }}{{ totais.total_custo|floatformat:2|default:"-" }}
-{% endblock %} \ No newline at end of file + {% endif %} +{% endblock data %} diff --git a/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report_pdf.html b/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report_pdf.html new file mode 100644 index 0000000..812d118 --- /dev/null +++ b/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report_pdf.html @@ -0,0 +1,98 @@ +{% extends 'pdf/base_report.html' %} +{% load i18n %} + +{% block page_size %}A4 landscape{% endblock page_size %} +{% block page_margin %}3cm 1cm 2cm 1cm{% endblock page_margin %}; + +{% block extra_style %} + {{ block.super }} + aside { + margin-left: 8px; + font-size: 0.8em; + color: #666; + } + blockquote { + margin: 12px 0 12px; + padding-left: 1.5rem; + border-left: 5px solid #ee6e73; + font-size: 1.4em; + font-weight: bold; + } + tr:nth-child(even) { + background-color: initial; + } + .even-row { + background-color: #d2d2d2 !important; + } + .sessao-resumo { + align-items: stretch; + display: flex; + flex-wrap: wrap; + width: 100%; + margin-top: 24px; + } + .card-resumo { + background-color: #eeeeef; + border-radius: 2px; + box-sizing: border-box; + margin: 6px; + flex-basis: 49%; + padding: 0 6px 6px 6px; + position: relative; + width: 100%; + } + .card-resumo.full { + flex-basis: 98%; + } + + .index-cell { + width: 2em; + text-align: center; + } + .label-resumo { + min-width: 30em; + } + .timestamp-container { + width: 100%; + margin: 24px 10px; + border-left: 5px solid #ee6e73; + font-size: 1.3em; + } + .timestamp-row { + display: flex; + flex-wrap: wrap; + margin-bottom: 6px; + } + .timestamp-col { + position: relative; + padding-left: 15px; + padding-right: 15px; + } + .timestamp-label { + flex: 0 0 12%; + max-width: 12%; + font-weight: bold; + } +{% endblock %} + +{% block main_content %} +
+
+
+ {% trans "Data inicial" %}: +
+
+ {{ data_inicio|date:"SHORT_DATE_FORMAT" }}
+
+
+
+
+ {% trans "Data final" %}: +
+
+ {{ data_fim|date:"SHORT_DATE_FORMAT" }} +
+
+
+ {% include 'admin/eventos/custos_servidor_report_snippet.html' %} +{% endblock %} \ No newline at end of file diff --git a/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report_snippet.html b/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report_snippet.html new file mode 100644 index 0000000..4d06f4f --- /dev/null +++ b/sigi/apps/eventos/templates/admin/eventos/custos_servidor_report_snippet.html @@ -0,0 +1,36 @@ +{% load i18n %} + + + + + + + + + + + + + + {% for servidor in servidores.itertuples %} + + + + + + + + + + {% endfor %} + + + + + + + + + + +
{% trans "Membro da equipe" %}{% trans "Qtde eventos" %}{% trans "Qtde diárias" %}{% trans "Valor médio diária" %}{% trans "Total diárias" %}{% trans "Total passagens" %}{% trans "Total" %}
{{ servidor.nome_completo }}{{ servidor.qtde_eventos|default:"-" }}{{ servidor.qtde_diarias|floatformat:2|default:"-" }}{{ servidor.media_diarias|floatformat:2|default:"-" }}{{ servidor.total_diarias|floatformat:2|default:"-" }}{{ servidor.total_passagens|floatformat:2|default:"-" }}{{ servidor.total_custo|floatformat:2|default:"-" }}
{% trans "Totais" %}{{ totais.qtde_eventos|default:"-" }}{{ totais.qtde_diarias|floatformat:2|default:"-" }}{{ totais.media_diarias|floatformat:2|default:"-" }}{{ totais.total_diarias|floatformat:2|default:"-" }}{{ totais.total_passagens|floatformat:2|default:"-" }}{{ totais.total_custo|floatformat:2|default:"-" }}
\ No newline at end of file diff --git a/sigi/apps/eventos/views.py b/sigi/apps/eventos/views.py index a643f88..38e11ac 100644 --- a/sigi/apps/eventos/views.py +++ b/sigi/apps/eventos/views.py @@ -11,16 +11,21 @@ from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.db import models from django.db.models import ( + Avg, + Case, Count, - Sum, - Q, F, + Max, + Min, OuterRef, + Prefetch, + Q, Subquery, - Case, - When, + Sum, Value, + When, ) from django.http import HttpResponse from django.shortcuts import render @@ -51,6 +56,7 @@ from sigi.apps.eventos.serializers import ( EventoSerializer, EventoListSerializer, ) +from sigi.apps.servidores.models import Servidor from sigi.apps.utils.views import ReportListView @@ -89,7 +95,7 @@ class AlunosPorUfReportView( queryset = queryset.filter(evento__virtual=False) return queryset - def get_dataset(self): + def get_dataset(self, context): queryset = self.get_queryset() fieldnames = [ "evento__nome", @@ -1006,3 +1012,337 @@ class ApiEventoRetrieve(ApiEventoAbstract, generics.RetrieveAPIView): """ pass + + +class CustosEventosReport( + LoginRequiredMixin, UserPassesTestMixin, ReportListView +): + title = _("Custos por eventos") + template_name = "admin/eventos/custos_eventos_report.html" + template_name_pdf = "admin/eventos/custos_eventos_report_pdf.html" + filter_form = EventosPorUfForm + queryset = Evento.objects.filter(status=Evento.STATUS_REALIZADO) + list_fields = [ + "nome", + "data_inicio", + "data_termino", + "turma", + "descricao", + "virtual", + "solicitante", + "num_processo", + "casa_anfitria__nome", + "casa_anfitria__municipio__nome", + "casa_anfitria__municipio__uf__sigla", + "duracao_dias", + "qtde_diarias", + "vlr_tot_diarias", + "custo_total", + "custo_medio_participante", + "custo_medio_membro", + "tot_membros", + ] + + def test_func(self): + return self.request.user.is_staff + + def filter_queryset(self, queryset): + form = self.get_filter_form_instance() + if form.is_valid(): + data_inicio = form.cleaned_data.get("data_inicio") + data_fim = form.cleaned_data.get("data_fim") + categorias = form.cleaned_data.get( + "categoria", [c[0] for c in TipoEvento.CATEGORIA_CHOICES] + ) + modo = form.cleaned_data.get("virtual", ["V", "P"]) + queryset = queryset.filter( + status=Evento.STATUS_REALIZADO, + data_inicio__gte=data_inicio, + data_termino__lte=data_fim, + tipo_evento__categoria__in=categorias, + ) + if len(modo) == 1: + if "V" in modo: + queryset = queryset.filter(virtual=True) + else: + queryset = queryset.filter(virtual=False) + else: + queryset = queryset.none() + return queryset + + def get_context_data(self, **kwargs): + queryset = self.get_queryset() + form = self.get_filter_form_instance() + if queryset.exists(): + context = context_custos_eventos(queryset) + form.is_valid() + context["data_inicio"] = form.cleaned_data["data_inicio"] + context["data_fim"] = form.cleaned_data["data_fim"] + else: + context = {} + context["form"] = form + return context + + def get_dataset(self, context): + dataset = context["eventos"] + return dataset.values(*self.list_fields), self.list_fields + + +class CustosServidorReport( + LoginRequiredMixin, UserPassesTestMixin, ReportListView +): + title = _("Custos por servidor") + template_name = "admin/eventos/custos_servidor_report.html" + template_name_pdf = "admin/eventos/custos_servidor_report_pdf.html" + filter_form = EventosPorUfForm + queryset = Evento.objects.filter(status=Evento.STATUS_REALIZADO) + + def test_func(self): + return self.request.user.is_staff + + def filter_queryset(self, queryset): + form = self.get_filter_form_instance() + if form.is_valid(): + data_inicio = form.cleaned_data.get("data_inicio") + data_fim = form.cleaned_data.get("data_fim") + categorias = form.cleaned_data.get( + "categoria", [c[0] for c in TipoEvento.CATEGORIA_CHOICES] + ) + modo = form.cleaned_data.get("virtual", ["V", "P"]) + queryset = queryset.filter( + status=Evento.STATUS_REALIZADO, + data_inicio__gte=data_inicio, + data_termino__lte=data_fim, + tipo_evento__categoria__in=categorias, + ) + if len(modo) == 1: + if "V" in modo: + queryset = queryset.filter(virtual=True) + else: + queryset = queryset.filter(virtual=False) + else: + queryset = queryset.none() + return queryset + + def get_context_data(self, **kwargs): + queryset = self.get_queryset() + form = self.get_filter_form_instance() + if queryset.exists(): + context = context_custos_servidor(queryset) + form.is_valid() + context["data_inicio"] = form.cleaned_data["data_inicio"] + context["data_fim"] = form.cleaned_data["data_fim"] + else: + context = {} + context["form"] = form + return context + + def render_to_response(self, context, **response_kwargs): + if self._is_csv(): + dataset = context["servidores"] + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = ( + f'attachment; filename="{self.get_filename()}.csv"' + ) + dataset.to_csv(response, index=False, encoding="utf8") + return response + return super().render_to_response(context, **response_kwargs) + + +def context_custos_eventos(queryset): + my_decimal_field = models.DecimalField(max_digits=14, decimal_places=2) + equipe_qs = Equipe.objects.annotate( + total_diarias=(F("qtde_diarias") * F("valor_diaria")), + antecedencia=models.functions.ExtractDay( + F("evento__data_inicio") - F("emissao_passagens") + ), + ) + eventos = queryset.annotate( + duracao_dias=( + models.functions.ExtractDay(F("data_termino") - F("data_inicio")) + + 1 + ), + qtde_diarias=Sum("equipe__qtde_diarias"), + vlr_tot_diarias=Sum( + F("equipe__qtde_diarias") * F("equipe__valor_diaria"), + output_field=my_decimal_field, + ), + vlr_tot_passagens=Sum("equipe__total_passagens"), + custo_total=F("vlr_tot_diarias") + F("vlr_tot_passagens"), + custo_medio_participante=models.functions.Cast( + Case( + When(total_participantes__lte=0, then=0), + default=F("custo_total") / F("total_participantes"), + output_field=my_decimal_field, + ), + output_field=my_decimal_field, + ), + custo_medio_membro=models.functions.Cast( + F("custo_total") / Count("equipe__membro"), + output_field=my_decimal_field, + ), + tot_membros=Count("equipe"), + ).prefetch_related( + Prefetch("equipe_set", queryset=equipe_qs, to_attr="equipe_ext") + ) + resumo = eventos.aggregate( + qtde_oficinas=Count("id"), + tot_participantes=Sum("total_participantes"), + media_participantes=models.functions.Cast( + 1.0 * F("tot_participantes") / F("qtde_oficinas"), + output_field=my_decimal_field, + ), + min_participantes=Min("total_participantes"), + max_participantes=Max("total_participantes"), + tot_servidores=Sum("tot_membros"), + media_membros=models.functions.Cast( + 1.0 * Sum("tot_membros") / F("qtde_oficinas"), + output_field=my_decimal_field, + ), + min_membros=Min("tot_membros"), + max_membros=Max("tot_membros"), + tot_dias=Sum("duracao_dias"), + media_dias=models.functions.Cast( + 1.0 * F("tot_dias") / F("qtde_oficinas"), + output_field=my_decimal_field, + ), + tot_diarias=Sum("qtde_diarias"), + media_diarias=models.functions.Cast( + 1.0 * F("tot_diarias") / F("qtde_oficinas"), + output_field=my_decimal_field, + ), + tot_custo_total=Sum("custo_total"), + tot_custo_diarias=Sum("vlr_tot_diarias"), + tot_custo_passagens=Sum("vlr_tot_passagens"), + media_custo_total=models.functions.Cast( + F("tot_custo_total") / F("qtde_oficinas"), + output_field=my_decimal_field, + ), + media_custo_diarias=models.functions.Cast( + F("tot_custo_diarias") / F("qtde_oficinas"), + output_field=my_decimal_field, + ), + media_custo_passagens=models.functions.Cast( + F("tot_custo_passagens") / F("qtde_oficinas"), + output_field=my_decimal_field, + ), + media_custo_participantes=models.functions.Cast( + F("tot_custo_total") / F("tot_participantes"), + output_field=my_decimal_field, + ), + media_custo_membro=models.functions.Cast( + F("tot_custo_total") / Sum("tot_membros"), + output_field=my_decimal_field, + ), + ) + resumo.update( + eventos.aggregate( + media_antecedencia=Avg( + models.functions.ExtractDay( + F("data_inicio") - F("equipe__emissao_passagens") + ) + ), + min_antecedencia=Min( + models.functions.ExtractDay( + F("data_inicio") - F("equipe__emissao_passagens") + ) + ), + max_antecedencia=Max( + models.functions.ExtractDay( + F("data_inicio") - F("equipe__emissao_passagens") + ) + ), + ) + ) + + f_valor_diarias = F("equipe__qtde_diarias") * F("equipe__valor_diaria") + f_custo_total = (f_valor_diarias) + F("equipe__total_passagens") + + extrato = ( + queryset.order_by("casa_anfitria__municipio__uf__regiao") + .annotate( + regiao=F("casa_anfitria__municipio__uf__regiao"), + tot_diarias=Sum(f_valor_diarias), + tot_passagens=Sum("equipe__total_passagens"), + tot_custo=Sum(f_custo_total), + ) + .values("regiao", "tot_diarias", "tot_passagens", "tot_custo") + ) + + df = ( + pd.DataFrame(extrato) + .set_index("regiao") + .groupby("regiao") + .aggregate(["sum", "min", "max", "mean"]) + .fillna(0) + ) + + custos_regiao = [ + { + "nome": nome, + "extrato": df.loc[sigla] if sigla in df.index else None, + } + for sigla, nome in UnidadeFederativa.REGIAO_CHOICES + ] + + return { + "eventos": eventos.order_by("data_inicio"), + "resumo": resumo, + "custos_regiao": custos_regiao, + "title": _("Custos por eventos"), + } + + +def context_custos_servidor(queryset): + equipe_qs = Equipe.objects.filter(evento__in=queryset) + f_total_diarias = F("equipe_evento__qtde_diarias") * F( + "equipe_evento__valor_diaria" + ) + servidores = ( + ( + Servidor.objects.distinct() + .filter(equipe_evento__evento__in=queryset) + .prefetch_related( + Prefetch( + "equipe_evento", queryset=equipe_qs, to_attr="equipe_ext" + ) + ) + .annotate( + qtde_eventos=Count("equipe_evento"), + qtde_diarias=Sum("equipe_evento__qtde_diarias"), + total_diarias=Sum(f_total_diarias), + total_passagens=Sum("equipe_evento__total_passagens"), + total_custo=Sum( + F("equipe_evento__total_passagens") + f_total_diarias + ), + ) + ) + .order_by("nome_completo") + .values( + "nome_completo", + "qtde_eventos", + "qtde_diarias", + "total_diarias", + "total_passagens", + "total_custo", + ) + ) + servidores = pd.DataFrame(servidores) + totais = servidores[ + [ + "qtde_eventos", + "qtde_diarias", + "total_diarias", + "total_passagens", + "total_custo", + ] + ].sum() + servidores["media_diarias"] = ( + servidores["total_diarias"] / servidores["qtde_diarias"] + ) + totais["media_diarias"] = totais["total_diarias"] / totais["qtde_diarias"] + return { + "servidores": servidores.fillna(0), + "totais": totais.fillna(0), + "title": _("Custos por servidor"), + } diff --git a/sigi/apps/utils/views.py b/sigi/apps/utils/views.py index 016d52a..460dd98 100644 --- a/sigi/apps/utils/views.py +++ b/sigi/apps/utils/views.py @@ -85,7 +85,7 @@ class ReportListView(ListView): def render_to_response(self, context, **response_kwargs): if self._is_csv(): - dataset, fieldnames = self.get_dataset() + dataset, fieldnames = self.get_dataset(context) response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = ( f'attachment; filename="{self.get_filename()}.csv"' @@ -119,7 +119,7 @@ class ReportListView(ListView): form = self.filter_form(self.request.GET) return form - def get_dataset(self): + def get_dataset(self, context): return ( self.get_queryset().values(*self.list_fields), self.list_fields, diff --git a/sigi/menu_conf.yaml b/sigi/menu_conf.yaml index bc62f50..d4fd24e 100644 --- a/sigi/menu_conf.yaml +++ b/sigi/menu_conf.yaml @@ -40,6 +40,10 @@ main_menu: view_name: eventos_eventosporuf - title: Alunos por UF view_name: eventos_alunosporuf + - title: Custos por evento + view_name: eventos_custoseventos + - title: Custos por servidor + view_name: eventos_custosservidor - title: Solicitações de eventos por período view_name: eventos_solicitacoesporperiodo - title: Calendário de eventos