diff --git a/sigi/apps/convenios/migrations/0020_gescon_orgaos_gestores.py b/sigi/apps/convenios/migrations/0020_gescon_orgaos_gestores.py
new file mode 100644
index 0000000..22adc43
--- /dev/null
+++ b/sigi/apps/convenios/migrations/0020_gescon_orgaos_gestores.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.0.3 on 2022-04-20 13:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('convenios', '0019_alter_anexo_arquivo_alter_anexo_descricao_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='gescon',
+ name='orgaos_gestores',
+ field=models.TextField(default='SCCO', help_text='Siglas de órgãos gestores que devem aparecer no campoORGAOSGESTORESTITULARES
- Informe um sigla por linha.
- Ocorrendo qualquer uma das siglas, o contrato será importado.
', verbose_name='Órgãos gestores'),
+ ),
+ ]
diff --git a/sigi/apps/convenios/models.py b/sigi/apps/convenios/models.py
index c3e5736..6e450d7 100644
--- a/sigi/apps/convenios/models.py
+++ b/sigi/apps/convenios/models.py
@@ -381,6 +381,15 @@ class Gescon(models.Model):
"Ocorrendo qualquer uma das palavras, o contrato será "
"importado.")
)
+ orgaos_gestores = models.TextField(
+ _("Órgãos gestores"),
+ default="SCCO",
+ help_text=_("Siglas de órgãos gestores que devem aparecer no campo"
+ "ORGAOSGESTORESTITULARES"
+ "- Informe um sigla por linha.
"
+ "- Ocorrendo qualquer uma das siglas, o contrato será "
+ "importado.
")
+ )
email = models.EmailField(
_("E-mail"),
help_text=_("Caixa de e-mail para onde o relatório diário de "
@@ -429,16 +438,13 @@ class Gescon(models.Model):
def importa_contratos(self):
self.ultima_importacao = ""
self.add_message(
- _("Importação iniciada em {:%d/%m/%Y %H:%M:%S}\n"
- "==========================================\n").format(
- datetime.now()
- )
+ _(f"Importação iniciada em {datetime.now():%d/%m/%Y %H:%M:%S}\n"
+ "==========================================================\n")
)
- if self.palavras == "":
- self.add_message(_("Nenhuma palavra de pesquisa definida - "
- "processo abortado."), True)
- return
+ if self.palavras == "" or self.orgaos_gestores == "":
+ self.add_message(_("Nenhuma palavra de pesquisa ou orgãos "
+ "gestores definidos - processo abortado."), True)
if self.subespecies == "":
self.add_message(_("Nenhuma subespécie definida - processo "
@@ -457,11 +463,14 @@ class Gescon(models.Model):
return
palavras = self.palavras.split()
+ orgaos = self.orgaos_gestores.split()
subespecies = {tuple(s.split("=")) for s in self.subespecies.split()}
+ lista_cnpj = {re.sub("[^\d]", "", o.cnpj).zfill(14): o
+ for o in Orgao.objects.exclude(cnpj="")
+ if re.sub("[^\d]", "", o.cnpj) != ''}
for sigla_gescon, sigla_sigi in subespecies:
- self.add_message(_("\nImportando subespécie {s}".format(
- s=sigla_gescon)))
+ self.add_message(_(f"\nImportando subespécie {sigla_gescon}"))
url = self.url_gescon.format(s=sigla_gescon)
projeto = Projeto.objects.get(sigla=sigla_sigi)
@@ -469,42 +478,34 @@ class Gescon(models.Model):
try:
response = requests.get(url, verify=False)
except Exception as e:
- self.add_message(
- _("\tErro ao acessar {url}: {errmsg}").format(
- url=url,
- errmsg=e.message.decode("utf8")
- )
- )
+ self.add_message(_(f"\tErro ao acessar {url}: {e.message}"))
continue
if not response.ok:
self.add_message(
- _("\tErro ao acessar {url}: {reason}").format(
- url=url,
- reason=response.reason
- )
+ _(f"\tErro ao acessar {url}: {response.reason}")
)
continue
if not 'application/json' in response.headers.get('Content-Type'):
- self.add_message(_("\tResultado da consulta à {url} não "
- "retornou dados em formato json").format(
- url=url
- )
- )
+ self.add_message(_(f"\tResultado da consulta à {url} não "
+ "retornou dados em formato json"))
continue
contratos = response.json()
# Pegar só os contratos que possuem alguma das palavras-chave
- nossos = [c for c in contratos
- if any(palavra in c['objeto'] for palavra in palavras)]
+ nossos = [
+ c for c in contratos
+ if any(palavra in c['objeto'] for palavra in palavras) or
+ any(orgao in c['orgaosGestoresTitulares']
+ for orgao in orgaos
+ if c['orgaosGestoresTitulares'] is not None)
+ ]
self.add_message(
- _("\t{count} contratos encontrados no Gescon").format(
- count=len(nossos)
- )
+ _(f"\t{len(nossos)} contratos encontrados no Gescon")
)
novos = 0
@@ -514,17 +515,14 @@ class Gescon(models.Model):
for contrato in nossos:
numero = contrato['numero'].zfill(8)
- numero = "{}/{}".format(numero[:4], numero[4:])
+ numero = f"{numero[:4]}/{numero[4:]}"
sigad = contrato['processo'].zfill(17)
- sigad = "{}.{}/{}-{}".format(sigad[:5], sigad[5:11],
- sigad[11:15], sigad[15:])
-
+ sigad = f"{sigad[:5]}.{sigad[5:11]}/{sigad[11:15]}-{sigad[15:]}"
if contrato['cnpjCpfFornecedor']:
cnpj = contrato['cnpjCpfFornecedor'].zfill(14)
- cnpj = "{}.{}.{}/{}-{}".format(cnpj[:2], cnpj[2:5],
- cnpj[5:8], cnpj[8:12],
- cnpj[12:])
+ cnpj_masked = (f"{cnpj[:2]}.{cnpj[2:5]}.{cnpj[5:8]}/"
+ f"{cnpj[8:12]}-{cnpj[12:]}")
else:
cnpj = None
@@ -541,8 +539,8 @@ class Gescon(models.Model):
if (cnpj is None) and (nome is None):
self.add_message(
- _("\tO contrato {numero} no Gescon não informa o CNPJ "
- "nem o nome do órgão.").format(numero=numero)
+ _(f"\tO contrato {numero} no Gescon não informa o CNPJ"
+ "nem o nome do órgão.")
)
erros += 1
continue
@@ -550,13 +548,16 @@ class Gescon(models.Model):
orgao = None
if cnpj is not None:
- try:
- orgao = Orgao.objects.get(cnpj=cnpj)
- except (
- Orgao.DoesNotExist,
- Orgao.MultipleObjectsReturned) as e:
- orgao = None
- pass
+ if cnpj in lista_cnpj:
+ orgao = lista_cnpj[cnpj]
+ else:
+ try:
+ orgao = Orgao.objects.get(cnpj=cnpj_masked)
+ except (
+ Orgao.DoesNotExist,
+ Orgao.MultipleObjectsReturned) as e:
+ orgao = None
+ pass
if (orgao is None) and (nome is not None):
try:
@@ -569,14 +570,11 @@ class Gescon(models.Model):
if orgao is None:
self.add_message(
- _("\tÓrgão não encontrado no SIGI ou mais de um órgão"
- "encontrado com o mesmo CNPJ ou nome. Favor "
- "regularizar o cadastro: CNPJ: {cnpj}, "
- "Nome: {nome}".format(
- cnpj=contrato['cnpjCpfFornecedor'],
- nome=contrato['nomeFornecedor']
- )
- )
+ _(f"\tÓrgão não encontrado no SIGI ou mais de um órgão"
+ f"encontrado com o mesmo CNPJ ou nome. Favor "
+ f"regularizar o cadastro: "
+ f"CNPJ: {contrato['cnpjCpfFornecedor']}, "
+ f"Nome: {contrato['nomeFornecedor']}")
)
erros += 1
continue
@@ -628,15 +626,9 @@ class Gescon(models.Model):
convenio.observacao_gescon = ''
if convenio.casa_legislativa != orgao:
self.add_message(
- _("\tO órgao no convênio {url} diverge do que "
- "consta no Gescon ({cnpj}, {nome})").format(
- url=reverse('admin:%s_%s_change' % (
- convenio._meta.app_label,
- convenio._meta.model_name),
- args=[convenio.id]),
- cnpj=cnpj,
- nome=contrato['nomeFornecedor']
- )
+ _(f"\tO órgao no convênio {convenio.id} diverge do "
+ f"que consta no Gescon ({cnpj}, "
+ f"{contrato['nomeFornecedor']})")
)
convenio.observacao_gescon = _(
'ERRO: Órgão diverge do Gescon. Não atualizado!'
@@ -647,19 +639,12 @@ class Gescon(models.Model):
if convenio.num_processo_sf != sigad:
self.add_message(
- _("\tO contrato Gescon nº {numero} corresponde"
- " ao convênio SIGI {url}, mas o NUP sigad "
- "diverge (Gescon: {sigad_gescon}, "
- "SIGI: {sigad_sigi}). CORRIGIDO!").format(
- numero=numero,
- url=reverse('admin:%s_%s_change' % (
- convenio._meta.app_label,
- convenio._meta.model_name),
- args=[convenio.id]),
- sigad_gescon=sigad,
- sigad_sigi=convenio.num_processo_sf
- )
- )
+ _(f"\tO contrato Gescon nº {numero} corresponde"
+ f" ao convênio SIGI {convenio.id}, mas o NUP "
+ f"sigad diverge (Gescon: {sigad}, "
+ f"SIGI: {convenio.num_processo_sf}). "
+ "CORRIGIDO!")
+ )
convenio.num_processo_sf = sigad
convenio.observacao_gescon += _(
"Número do SIGAD atualizado.\n"
@@ -668,19 +653,11 @@ class Gescon(models.Model):
if convenio.num_convenio != numero:
self.add_message(
- _("\tO contrato Gescon ID {id} corresponde ao "
- "convênio SIGI {url}, mas o número do convênio"
- " diverge (Gescon: {numero_gescon}, SIGI: "
- "{numero_sigi}). CORRIGIDO!").format(
- id=contrato['id'],
- url=reverse('admin:%s_%s_change' % (
- convenio._meta.app_label,
- convenio._meta.model_name),
- args=[convenio.id]
- ),
- numero_gescon=numero,
- numero_sigi=convenio.num_convenio
- )
+ _(f"\tO contrato Gescon ID {contrato['id']} "
+ f"corresponde ao convênio SIGI {convenio.id}, "
+ "mas o número do convênio diverge ("
+ f"Gescon: {numero}, SIGI: {convenio.num_convenio}"
+ "). CORRIGIDO!")
)
convenio.num_convenio = numero
convenio.observacao_gescon += _(
@@ -713,44 +690,28 @@ class Gescon(models.Model):
convenio.save()
except Exception as e:
self.add_message(
- _("Ocorreu um erro ao salvar o convênio {url} no "
- "SIGI. Alguma informação do Gescon pode ter "
- "quebrado o sistema. Informe ao suporte. Erro:"
- "{errmsg}").format(
- url=reverse('admin:%s_%s_change' % (
- convenio._meta.app_label,
- convenio._meta.model_name),
- args=[convenio.id]
- ),
- errmsg=e.message.decode("utf8")
- )
+ _("Ocorreu um erro ao salvar o convênio "
+ f"{convenio.id} no SIGI. Alguma informação do "
+ "Gescon pode ter quebrado o sistema. Informe ao "
+ f"suporte. Erro: {e.message}")
)
erros += 1
continue
atualizados += 1
else:
- self.add_message(_("\tExistem {count} convênios no SIGI "
- "que correspondem ao mesmo contrato no "
- "Gescon (contrato {numero}, sigad "
- "{sigad})").format(
- count=chk,
- numero=numero,
- sigad=sigad
- )
+ self.add_message(
+ _(f"\tExistem {chk} convênios no SIGI que "
+ "correspondem ao mesmo contrato no Gescon (contrato "
+ f"{numero}, sigad {sigad})")
)
erros += 1
continue
self.add_message(
- _("\t{novos} novos convenios adicionados ao SIGI, "
- "{atualizados} atualizados, sendo {alertas} com alertas, e "
- "{erros} reportados com erro.").format(
- novos=novos,
- atualizados=atualizados,
- alertas=alertas,
- erros=erros
- )
+ _(f"\t{novos} novos convenios adicionados ao SIGI, "
+ f"{atualizados} atualizados, sendo {alertas} com alertas, e "
+ f"{erros} reportados com erro.")
)
self.save()
diff --git a/sigi/apps/eventos/admin.py b/sigi/apps/eventos/admin.py
index 0656646..2099770 100644
--- a/sigi/apps/eventos/admin.py
+++ b/sigi/apps/eventos/admin.py
@@ -1,11 +1,54 @@
from django.contrib import admin
from django.http import HttpResponseRedirect
from django.utils.translation import gettext as _
+from import_export.fields import Field
from tinymce.models import HTMLField
from tinymce.widgets import AdminTinyMCE
from sigi.apps.eventos.models import (ModeloDeclaracao, Modulo, TipoEvento,
Funcao, Evento, Equipe, Convite, Anexo)
from sigi.apps.eventos.forms import EventoAdminForm
+from sigi.apps.utils.filters import EmptyFilter
+from sigi.apps.utils.mixins import CartExportMixin, ValueLabeledResource
+
+class EventoResource(ValueLabeledResource):
+ # categoria_evento = Field(column_name="tipo_evento__categoria")
+ # status = Field(column_name="status")
+ class Meta:
+ model = Evento
+ fields = (
+ 'id', 'tipo_evento__nome', 'tipo_evento__categoria', 'nome',
+ 'descricao', 'virtual', 'solicitante', 'num_processo',
+ 'data_pedido', 'data_inicio', 'data_termino', 'carga_horaria',
+ 'casa_anfitria__nome', 'casa_anfitria__logradouro',
+ 'casa_anfitria__bairro', 'casa_anfitria__municipio__nome',
+ 'casa_anfitria__municipio__uf__sigla', 'casa_anfitria__cep',
+ 'casa_anfitria__email', 'local', 'municipio__nome',
+ 'municipio__uf__sigla', 'observacao', 'publico_alvo',
+ 'total_participantes', 'status', 'data_cancelamento',
+ 'motivo_cancelamento', 'equipe__membro__nome_completo',
+ 'equipe__funcao__nome', 'convite__casa__nome',
+ 'convite__casa__municipio__nome',
+ 'convite__casa__municipio__uf__sigla', 'convite__casa__cep',
+ 'convite__casa__email', 'convite__aceite', 'convite__data_convite',
+ 'convite__participou', 'convite__qtde_participantes',
+ 'convite__nomes_participantes',
+ )
+ export_order = fields
+
+ def dehydrate_tipo_evento__categoria(self, obj):
+ return dict(TipoEvento.CATEGORIA_CHOICES)[obj['tipo_evento__categoria']]
+
+ def dehydrate_virtual(self, obj):
+ return "Sim" if obj['virtual'] else "Não"
+
+ def dehydrate_status(self, obj):
+ return dict(Evento.STATUS_CHOICES)[obj['status']]
+
+ def dehydrate_convite__aceite(self, obj):
+ return "Sim" if obj['convite__aceite'] else "Não"
+
+ def dehydrate_convite__participou(self, obj):
+ return "Sim" if obj['convite__participou'] else "Não"
@admin.register(TipoEvento)
class TipoEventAdmin(admin.ModelAdmin):
@@ -36,18 +79,28 @@ class AnexoInline(admin.StackedInline):
exclude = ('data_pub',)
@admin.register(Evento)
-class EventoAdmin(admin.ModelAdmin):
+class EventoAdmin(CartExportMixin, admin.ModelAdmin):
form = EventoAdminForm
+ resource_class = EventoResource
date_hierarchy = 'data_inicio'
- list_display = ('nome', 'tipo_evento', 'status', 'data_inicio',
- 'data_termino', 'municipio', 'solicitante',
+ list_display = ('nome', 'tipo_evento', 'status', 'link_sigad',
+ 'data_inicio', 'data_termino', 'municipio', 'solicitante',
'total_participantes',)
- list_filter = ('status', 'tipo_evento', 'tipo_evento__categoria', 'virtual',
- 'municipio__uf', 'solicitante')
+ list_filter = ('status', ('num_processo', EmptyFilter), 'tipo_evento',
+ 'tipo_evento__categoria', 'virtual', 'municipio__uf',
+ 'solicitante')
raw_id_fields = ('casa_anfitria', 'municipio',)
search_fields = ('nome', 'tipo_evento__nome', 'casa_anfitria__search_text',
'municipio__search_text', 'solicitante')
inlines = (EquipeInline, ConviteInline, ModuloInline, AnexoInline)
+ save_as = True
+
+ def link_sigad(self, obj):
+ if obj.pk is None:
+ return ""
+ return obj.get_sigad_url()
+ link_sigad.short_description = _(u"número do processo SIGAD")
+ link_sigad.allow_tags = True
def lookup_allowed(self, lookup, value):
return (super(EventoAdmin, self).lookup_allowed(lookup, value) or
diff --git a/sigi/apps/eventos/forms.py b/sigi/apps/eventos/forms.py
index b95d7d4..e100412 100644
--- a/sigi/apps/eventos/forms.py
+++ b/sigi/apps/eventos/forms.py
@@ -6,10 +6,10 @@ class EventoAdminForm(forms.ModelForm):
class Meta:
model = Evento
fields = ('tipo_evento', 'nome', 'descricao', 'virtual', 'solicitante',
- 'data_inicio', 'data_termino', 'carga_horaria',
- 'casa_anfitria', 'municipio', 'local', 'publico_alvo',
- 'total_participantes', 'status', 'data_cancelamento',
- 'motivo_cancelamento', )
+ 'num_processo', 'data_pedido', 'data_inicio', 'data_termino',
+ 'carga_horaria', 'casa_anfitria', 'municipio', 'observacao',
+ 'local', 'publico_alvo', 'total_participantes', 'status',
+ 'data_cancelamento', 'motivo_cancelamento', )
def clean(self):
cleaned_data = super(EventoAdminForm, self).clean()
diff --git a/sigi/apps/eventos/migrations/0018_evento_data_pedido_evento_num_processo_and_more.py b/sigi/apps/eventos/migrations/0018_evento_data_pedido_evento_num_processo_and_more.py
new file mode 100644
index 0000000..8a5751b
--- /dev/null
+++ b/sigi/apps/eventos/migrations/0018_evento_data_pedido_evento_num_processo_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.0.3 on 2022-04-19 12:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('eventos', '0017_alter_anexo_arquivo_alter_anexo_descricao_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='evento',
+ name='data_pedido',
+ field=models.DateField(blank=True, help_text='Data em que o pedido do Gabinete chegou à COPERI', null=True, verbose_name='Data do pedido'),
+ ),
+ migrations.AddField(
+ model_name='evento',
+ name='num_processo',
+ field=models.CharField(blank=True, help_text='Formato:XXXXX.XXXXXX/XXXX-XX', max_length=20, verbose_name='número do processo SIGAD'),
+ ),
+ migrations.AddField(
+ model_name='evento',
+ name='observacao',
+ field=models.TextField(blank=True, verbose_name='Observações e anotações'),
+ ),
+ migrations.AlterField(
+ model_name='evento',
+ name='status',
+ field=models.CharField(choices=[('E', 'Em planejamento'), ('G', 'Aguardando abertura SIGAD'), ('P', 'Previsão'), ('A', 'A confirmar'), ('O', 'Confirmado'), ('R', 'Realizado'), ('C', 'Cancelado')], max_length=1, verbose_name='Status'),
+ ),
+ ]
diff --git a/sigi/apps/eventos/migrations/0019_alter_evento_status.py b/sigi/apps/eventos/migrations/0019_alter_evento_status.py
new file mode 100644
index 0000000..dd870eb
--- /dev/null
+++ b/sigi/apps/eventos/migrations/0019_alter_evento_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.0.3 on 2022-04-20 13:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('eventos', '0018_evento_data_pedido_evento_num_processo_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='evento',
+ name='status',
+ field=models.CharField(choices=[('E', 'Em planejamento'), ('G', 'Aguardando abertura SIGAD'), ('P', 'Previsão'), ('A', 'A confirmar'), ('O', 'Confirmado'), ('R', 'Realizado'), ('C', 'Cancelado'), ('Q', 'Arquivado')], max_length=1, verbose_name='Status'),
+ ),
+ ]
diff --git a/sigi/apps/eventos/models.py b/sigi/apps/eventos/models.py
index aaddd83..85902d1 100644
--- a/sigi/apps/eventos/models.py
+++ b/sigi/apps/eventos/models.py
@@ -1,3 +1,4 @@
+import re
from datetime import datetime
from django.db import models
from django.db.models import Sum
@@ -33,11 +34,14 @@ class TipoEvento(models.Model):
class Evento(models.Model):
STATUS_CHOICES = (
+ ('E', _(u"Em planejamento")),
+ ('G', _(u"Aguardando abertura SIGAD")),
('P', _("Previsão")),
('A', _("A confirmar")),
('O', _("Confirmado")),
('R', _("Realizado")),
- ('C', _("Cancelado"))
+ ('C', _("Cancelado")),
+ ('Q', _("Arquivado")),
)
tipo_evento = models.ForeignKey(
@@ -48,6 +52,18 @@ class Evento(models.Model):
descricao = models.TextField(_("Descrição do evento"))
virtual = models.BooleanField(_("Virtual"), default=False)
solicitante = models.CharField(_("Solicitante"), max_length=100)
+ num_processo = models.CharField(
+ _(u'número do processo SIGAD'),
+ max_length=20,
+ blank=True,
+ help_text=_(u'Formato:XXXXX.XXXXXX/XXXX-XX')
+ )
+ data_pedido = models.DateField(
+ _(u"Data do pedido"),
+ null=True,
+ blank=True,
+ help_text=_(u"Data em que o pedido do Gabinete chegou à COPERI")
+ )
data_inicio = models.DateTimeField(
_("Data/hora do Início"),
null=True,
@@ -74,6 +90,7 @@ class Evento(models.Model):
on_delete=models.PROTECT
)
local = models.TextField(_("Local do evento"), blank=True)
+ observacao = models.TextField(_(u"Observações e anotações"), blank=True)
publico_alvo = models.TextField(_("Público alvo"), blank=True)
total_participantes = models.PositiveIntegerField(
_("Total de participantes"),
@@ -96,6 +113,20 @@ class Evento(models.Model):
f"de {self.data_inicio} a {self.data_termino}"
)
+ def get_sigad_url(self):
+ m = re.match('(?P00100|00200)\.(?P\d{6})/(?P'
+ '\d{4})-\d{2}', self.num_processo)
+ if m:
+ return ('{processo}').format(
+ processo=self.num_processo,**m.groupdict()
+ )
+ return self.num_processo
+
def save(self, *args, **kwargs):
if self.status != 'C':
self.data_cancelamento = None
@@ -105,7 +136,7 @@ class Evento(models.Model):
"data de início"))
total = self.convite_set.aggregate(total=Sum('qtde_participantes'))
total = total['total']
- if (total is not None) or (total > 0):
+ if total and total > 0:
self.total_participantes = total
return super(Evento, self).save(*args, **kwargs)
diff --git a/sigi/apps/eventos/templates/admin/eventos/evento/change_list.html b/sigi/apps/eventos/templates/admin/eventos/evento/change_list.html
deleted file mode 100644
index 83ebd55..0000000
--- a/sigi/apps/eventos/templates/admin/eventos/evento/change_list.html
+++ /dev/null
@@ -1 +0,0 @@
-{% extends "change_list_with_cart.html" %}
\ No newline at end of file
diff --git a/sigi/apps/utils/filters.py b/sigi/apps/utils/filters.py
index 1b1034b..777ce3d 100644
--- a/sigi/apps/utils/filters.py
+++ b/sigi/apps/utils/filters.py
@@ -1,11 +1,15 @@
import string
from math import log10
from django import forms
+from django.db.models import Q
from django.contrib import admin
from django.contrib.admin.options import IncorrectLookupParameters
from django.utils.translation import ngettext, gettext as _
from django.core.exceptions import ValidationError
+class NotEmptyableField(Exception):
+ pass
+
class AlphabeticFilter(admin.SimpleListFilter):
title = ''
@@ -20,6 +24,70 @@ class AlphabeticFilter(admin.SimpleListFilter):
(self.parameter_name + '__istartswith', self.value())
)
+class EmptyFilter(admin.FieldListFilter):
+ EMPTY_STRING = _("Em branco")
+ NOT_EMPTY_STRING = _("Preenchido")
+
+ def __init__(self, field, request, params, model, model_admin, field_path):
+ self.model = model
+ self.model_admin = model_admin
+ self.parameter_name = f'{field_path}__empty'
+
+ if (not field.null) and (not field.blank):
+ raise NotEmptyableField(
+ f"Field {field.name} cannot be empty nor null"
+ )
+
+ super().__init__(field, request, params, model, model_admin, field_path)
+
+ def lookups(self):
+ return (
+ ("1", self.EMPTY_STRING),
+ ("0", self.NOT_EMPTY_STRING),
+ )
+
+ def value(self):
+ return self.used_parameters.get(self.parameter_name)
+
+ def choices(self, changelist):
+ yield {
+ 'selected': self.value() is None,
+ 'query_string': changelist.get_query_string(remove=[
+ self.parameter_name]),
+ 'display': _('All'),
+ }
+
+ for value, display in self.lookups():
+ yield {
+ 'selected': self.value() == value,
+ 'query_string': changelist.get_query_string(
+ {self.parameter_name: value}),
+ 'display': display,
+ }
+
+ def expected_parameters(self):
+ return [self.parameter_name,]
+
+ def queryset(self, request, queryset):
+ val = self.value()
+
+ if val is None:
+ return queryset
+
+ val = bool(int(val))
+
+ filter = Q()
+
+ if self.field.null:
+ filter = filter | Q(**{f"{self.field_path}__isnull": val})
+ if self.field.blank:
+ if val:
+ filter = filter | Q(**{f"{self.field_path}__exact": ""})
+ else:
+ filter = filter | ~Q(**{f"{self.field_path}__exact": ""})
+
+ return queryset.filter(filter)
+
class RangeFilter(admin.FieldListFilter):
num_faixas = 4
parameter_name = None
diff --git a/sigi/apps/utils/mixins.py b/sigi/apps/utils/mixins.py
index bb60de1..7f9f19b 100644
--- a/sigi/apps/utils/mixins.py
+++ b/sigi/apps/utils/mixins.py
@@ -13,10 +13,17 @@ from django.urls import path
from django.utils.translation import gettext as _, ngettext
from import_export import resources
from import_export.admin import ExportMixin
+from import_export.fields import Field
from import_export.forms import ExportForm
from import_export.signals import post_export
from sigi.apps.utils import field_label
+class ValueField(Field):
+ def get_value(self, obj):
+ if self.attribute is None:
+ return None
+ return obj[self.attribute]
+
class ExportFormFields(ExportForm):
def __init__(self, formats, field_list, *args, **kwargs):
super().__init__(formats, *args, **kwargs)
@@ -51,6 +58,13 @@ class LabeledResourse(resources.ModelResource):
self.selected_fields = selected_fields
return super().export(queryset, *args, **kwargs)
+class ValueLabeledResource(LabeledResourse):
+ DEFAULT_RESOURCE_FIELD = ValueField
+
+ def export(self, queryset=None, selected_fields=None, *args, **kwargs):
+ queryset = queryset.values(*selected_fields)
+ return super().export(queryset, selected_fields, *args, **kwargs)
+
class CartExportMixin(ExportMixin):
to_encoding = 'utf-8'
change_list_template = 'admin/cart/change_list_cart_export.html'