Browse Source

Merge branch 'stable/2.2' of github.com:interlegis/sigi into new_sigi

pull/102/head
Lude Ribeiro 3 years ago
parent
commit
0864539052
  1. 18
      sigi/apps/convenios/admin.py
  2. 30
      sigi/apps/convenios/migrations/0014_gescon.py
  3. 18
      sigi/apps/convenios/migrations/0015_remove_convenio_search_text.py
  4. 26
      sigi/apps/convenios/migrations/0016_auto_20210909_0732.py
  5. 420
      sigi/apps/convenios/models.py
  6. 18
      sigi/apps/convenios/templates/convenios/importar_gescon.html
  7. 1
      sigi/apps/convenios/urls.py
  8. 16
      sigi/apps/convenios/views.py
  9. 14
      sigi/shortcuts.py
  10. 1
      templates/admin/base_site.html

18
sigi/apps/convenios/admin.py

@ -6,7 +6,8 @@ from geraldo.generators import PDFGenerator
from sigi.apps.convenios.models import (Projeto, StatusConvenio,
TipoSolicitacao, Convenio,
EquipamentoPrevisto, Anexo, Tramitacao)
EquipamentoPrevisto, Anexo, Tramitacao,
Gescon)
from sigi.apps.convenios.reports import ConvenioReport
from sigi.apps.convenios.views import adicionar_convenios_carrinho
from sigi.apps.utils import queryset_ascii
@ -60,8 +61,11 @@ class ConvenioAdmin(BaseModelAdmin):
{'fields': ('data_retorno_assinatura', 'data_termino_vigencia',
'data_pub_diario',)}
),
(_(u'Gescon'),
{'fields': ('atualizacao_gescon', 'observacao_gescon',)}
),
)
readonly_fields = ('data_sigi',)
readonly_fields = ('data_sigi', 'atualizacao_gescon', 'observacao_gescon',)
actions = ['adicionar_convenios']
inlines = (AnexosInline,)
list_display = ('num_convenio', 'casa_legislativa', 'get_uf',
@ -76,8 +80,9 @@ class ConvenioAdmin(BaseModelAdmin):
ordering = ('casa_legislativa', '-data_retorno_assinatura')
raw_id_fields = ('casa_legislativa',)
get_queryset = queryset_ascii
search_fields = ('id', 'search_text', 'casa_legislativa__sigla',
'num_processo_sf', 'num_convenio')
search_fields = ('id', 'casa_legislativa__search_text',
'casa_legislativa__sigla', 'num_processo_sf',
'num_convenio')
def get_uf(self, obj):
return obj.casa_legislativa.municipio.uf.sigla
@ -170,6 +175,11 @@ class EquipamentoPrevistoAdmin(BaseModelAdmin):
search_fields = ('convenio__id', 'equipamento__fabricante__nome',
'equipamento__modelo__modelo', 'equipamento__modelo__tipo__tipo')
@admin.register(Gescon)
class GesconAdmin(admin.ModelAdmin):
list_display = ('url_gescon', 'email', 'ultima_importacao')
readonly_fields = ('ultima_importacao',)
admin.site.register(Projeto)
admin.site.register(StatusConvenio)
admin.site.register(TipoSolicitacao)

30
sigi/apps/convenios/migrations/0014_gescon.py

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('convenios', '0013_remove_convenio_duracao'),
]
operations = [
migrations.CreateModel(
name='Gescon',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('url_gescon', models.URLField(default='https://adm.senado.gov.br/gestao-contratos/api/contratos/busca?especie={s}', help_text='Informe o ponto de consulta do webservice do Gescon, inclusive com a querystring. No ponto onde deve ser inserida a sigla da subespecie do contrato, use a marca\xe7\xe3o {s}.<br/><strong>Por exemplo:</strong> https://adm.senado.gov.br/gestao-contratos/api/contratos/busca?especie=<strong>{s}</strong>', verbose_name='Webservice Gescon')),
('subespecies', models.TextField(default='AC=ACT\nPI=PI\nCN=PML\nTA=PML', help_text='Informe as siglas das subesp\xe9cies de contratos que devem ser pesquisados no Gescon com a sigla correspondente do projeto no SIGI. Coloque um par de siglas por linha, no formato SIGLA_GESTON=SIGLA_SIGI. As siglas n\xe3o encontradas ser\xe3o ignoradas.', verbose_name='Subesp\xe9cies')),
('palavras', models.TextField(default='ILB\nINTERLEGIS', help_text='Palavras que devem aparecer no campo OBJETO dos dados do Gescon para identificar se o contrato pertence ao ILB. <ul><li>Informe uma palavra por linha.</li><li>Ocorrendo qualquer uma das palavras, o contrato ser\xe1 importado.</li></ul>', verbose_name='Palavras de filtro')),
('email', models.EmailField(help_text='Caixa de e-mail para onde o relat\xf3rio di\xe1rio de importa\xe7\xe3o ser\xe1 enviado.', max_length=75, verbose_name='E-mail')),
('ultima_importacao', models.TextField(verbose_name='Resultado da \xfaltima importa\xe7\xe3o', blank=True)),
],
options={
'verbose_name': 'Configura\xe7\xe3o do Gescon',
'verbose_name_plural': 'Configura\xe7\xf5es do Gescon',
},
bases=(models.Model,),
),
]

18
sigi/apps/convenios/migrations/0015_remove_convenio_search_text.py

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('convenios', '0014_gescon'),
]
operations = [
migrations.RemoveField(
model_name='convenio',
name='search_text',
),
]

26
sigi/apps/convenios/migrations/0016_auto_20210909_0732.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('convenios', '0015_remove_convenio_search_text'),
]
operations = [
migrations.AddField(
model_name='convenio',
name='atualizacao_gescon',
field=models.DateTimeField(null=True, verbose_name='Data de atualiza\xe7\xe3o pelo Gescon', blank=True),
preserve_default=True,
),
migrations.AddField(
model_name='convenio',
name='observacao_gescon',
field=models.TextField(verbose_name='Observa\xe7\xf5es da atualiza\xe7\xe3o do Gescon', blank=True),
preserve_default=True,
),
]

420
sigi/apps/convenios/models.py

@ -1,9 +1,14 @@
#-*- coding: utf-8 -*-
import re
import requests
from datetime import datetime, date
from django.db import models
from django.db.models import Q
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from sigi.apps.utils import SearchField
from sigi.apps.utils import SearchField, to_ascii
from sigi.apps.casas.models import Orgao
from sigi.apps.servidores.models import Servidor, Servico
class Projeto(models.Model):
@ -49,7 +54,6 @@ class Convenio(models.Model):
verbose_name=_(u'órgão conveniado')
)
# campo de busca em caixa baixa e sem acentos
search_text = SearchField(field_names=['casa_legislativa'])
projeto = models.ForeignKey(
Projeto,
on_delete=models.PROTECT,
@ -177,6 +181,15 @@ class Convenio(models.Model):
)
conveniada = models.BooleanField(default=False)
equipada = models.BooleanField(default=False)
atualizacao_gescon = models.DateTimeField(
_(u"Data de atualização pelo Gescon"),
blank=True,
null=True
)
observacao_gescon = models.TextField(
_(u"Observações da atualização do Gescon"),
blank=True
)
def get_status(self):
if self.status and self.status.cancela:
@ -360,3 +373,406 @@ class Tramitacao(models.Model):
if self.observacao:
result = result + u" (%s)" % (self.observacao)
return unicode(result) # XXX is this unicode(...) really necessary???
class Gescon(models.Model):
url_gescon = models.URLField(
_(u"Webservice Gescon"),
default=(u"https://adm.senado.gov.br/gestao-contratos/api/contratos"
u"/busca?especie={s}"),
help_text=_(u"Informe o ponto de consulta do webservice do Gescon, "
u"inclusive com a querystring. No ponto onde deve ser "
u"inserida a sigla da subespecie do contrato, use a "
u"marcação {s}.<br/><strong>Por exemplo:</strong> "
u"https://adm.senado.gov.br/gestao-contratos/api/contratos"
u"/busca?especie=<strong>{s}</strong>")
)
subespecies = models.TextField(
_(u"Subespécies"),
default=u"AC=ACT\nPI=PI\nCN=PML\nTA=PML",
help_text=_(u"Informe as siglas das subespécies de contratos que "
u"devem ser pesquisados no Gescon com a sigla "
u"correspondente do projeto no SIGI. Coloque um par de "
u"siglas por linha, no formato SIGLA_GESTON=SIGLA_SIGI. "
u"As siglas não encontradas serão ignoradas.")
)
palavras = models.TextField(
_(u"Palavras de filtro"),
default=u"ILB\nINTERLEGIS",
help_text=_(u"Palavras que devem aparecer no campo OBJETO dos dados do "
u"Gescon para identificar se o contrato pertence ao ILB. "
u"<ul><li>Informe uma palavra por linha.</li>"
u"<li>Ocorrendo qualquer uma das palavras, o contrato será "
u"importado.</li></ul>")
)
email = models.EmailField(
_(u"E-mail"),
help_text=_(u"Caixa de e-mail para onde o relatório diário de "
u"importação será enviado.")
)
ultima_importacao = models.TextField(
_(u"Resultado da última importação"),
blank=True
)
class Meta:
verbose_name = _(u"Configuração do Gescon")
verbose_name_plural = _(u"Configurações do Gescon")
def __unicode__(self):
return self.url_gescon
def save(self, *args, **kwargs):
self.pk = 1 # Highlander (singleton pattern)
return super(Gescon, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
pass # Highlander is immortal
def add_message(self, msg, save=False):
self.ultima_importacao += msg + "\n"
if save:
self.save()
self.email_report()
def email_report(self):
if self.email:
send_mail(
subject=_(u"Relatório de importação GESCON"),
message=self.ultima_importacao,
recipient_list=self.email,
fail_silently=True
)
else:
self.ultima_importacao += _(
u"\n\n*Não foi definida uma caixa de e-mail nas configurações "
u"do Gescon*"
)
self.save()
def importa_contratos(self):
self.ultima_importacao = ""
self.add_message(
_(u"Importação iniciada em {:%d/%m/%Y %H:%M:%S}\n"
u"==========================================\n").format(
datetime.now()
)
)
if self.palavras == "":
self.add_message(_(u"Nenhuma palavra de pesquisa definida - "
u"processo abortado."), True)
return
if self.subespecies == "":
self.add_message(_(u"Nenhuma subespécie definida - processo "
u"abortado."), True)
return
if "{s}" not in self.url_gescon:
self.add_message(
_(
u"Falta a marcação {s} na URL para indicar o local onde "
u"inserir a sigla da subespécia na consulta ao webservice "
u"- processo abortado."
),
True
)
return
palavras = self.palavras.split()
subespecies = {tuple(s.split("=")) for s in self.subespecies.split()}
for sigla_gescon, sigla_sigi in subespecies:
self.add_message(_(u"\nImportando subespécie {s}".format(
s=sigla_gescon)))
url = self.url_gescon.format(s=sigla_gescon)
projeto = Projeto.objects.get(sigla=sigla_sigi)
try:
response = requests.get(url)
except Exception as e:
self.add_message(
_(u"\tErro ao acessar {url}: {errmsg}").format(
url=url,
errmsg=str(e)
)
)
continue
if not response.ok:
self.add_message(
_(u"\tErro ao acessar {url}: {reason}").format(
url=url,
reason=response.reason
)
)
continue
if not 'application/json' in response.headers.get('Content-Type'):
self.add_message(_(u"\tResultado da consulta à {url} não "
u"retornou dados em formato json").format(
url=url
)
)
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)]
self.add_message(
_(u"\t{count} contratos encontrados no Gescon").format(
count=len(nossos)
)
)
novos = 0
erros = 0
alertas = 0
atualizados = 0
for contrato in nossos:
numero = contrato['numero'].zfill(8)
numero = "{}/{}".format(numero[:4], numero[4:])
sigad = contrato['processo'].zfill(17)
sigad = "{}.{}/{}-{}".format(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:])
else:
cnpj = None
if contrato['nomeFornecedor']:
nome = contrato['nomeFornecedor']
nome = nome.replace(u'VEREADORES DE', '')
nome = nome.split('-')[0]
nome = nome.split('/')[0]
nome = nome.strip()
nome = nome.replace(" ", " ")
nome = to_ascii(nome)
else:
nome = None
if (cnpj is None) and (nome is None):
self.add_message(
_(u"\tO contrato {numero} no Gescon não informa o CNPJ "
u"nem o nome do órgão.").format(numero=numero)
)
erros += 1
continue
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 (orgao is None) and (nome is not None):
try:
orgao = Orgao.objects.get(search_text__iexact=nome)
except (
Orgao.DoesNotExist,
Orgao.MultipleObjectsReturned) as e:
orgao = None
pass
if orgao is None:
self.add_message(
_(u"\tÓrgão não encontrado no SIGI ou mais de um órgão"
u"encontrado com o mesmo CNPJ ou nome. Favor "
u"regularizar o cadastro: CNPJ: {cnpj}, "
u"Nome: {nome}".format(
cnpj=contrato['cnpjCpfFornecedor'],
nome=contrato['nomeFornecedor']
)
)
)
erros += 1
continue
# O mais seguro é o NUP sigad
convenios = Convenio.objects.filter(num_processo_sf=sigad)
chk = convenios.count()
if chk == 0:
# NUP não encontrado, talvez exista apenas com o número
# do GESCON
convenios = Convenio.objects.filter(
Q(num_convenio=numero) |
Q(num_processo_sf=numero)
)
chk = convenios.count()
if chk > 1:
# Pode ser que existam vários contratos de subespécies
# diferentes com o mesmo número Gescon. Neste caso, o
# ideal é filtrar pelo tipo de projeto. Existindo, é
# ele mesmo. Se não existir, então segue com os
# múltiplos para registrar o problema mais adiante
if convenios.filter(projeto=projeto).count() == 1:
convenios = convenios.filter(projeto=projeto)
chk = 1
if chk == 0:
convenio = Convenio(
casa_legislativa=orgao,
projeto=projeto,
num_processo_sf=sigad,
num_convenio=numero,
data_sigi=date.today(),
data_sigad=contrato['assinatura'],
observacao=contrato['objeto'],
data_retorno_assinatura=contrato['inicioVigencia'],
data_termino_vigencia=contrato['terminoVigencia'],
data_pub_diario=contrato['publicacao'],
atualizacao_gescon=datetime.now(),
observacao_gescon=_(u"Importado integralmente do"
u"Gescon")
)
convenio.save()
novos += 1
continue
elif chk == 1:
convenio = convenios.get()
convenio.atualizacao_gescon = datetime.now()
convenio.observacao_gescon = ''
if convenio.casa_legislativa != orgao:
self.add_message(
_(u"\tO órgao no convênio {url} diverge do que "
u"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']
)
)
convenio.observacao_gescon = _(
u'ERRO: Órgão diverge do Gescon. Não atualizado!'
)
convenio.save()
erros += 1
continue
if convenio.num_processo_sf != sigad:
self.add_message(
_(u"\tO contrato Gescon nº {numero} corresponde"
u" ao convênio SIGI {url}, mas o NUP sigad "
u"diverge (Gescon: {sigad_gescon}, "
u"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
)
)
convenio.num_processo_sf = sigad
convenio.observacao_gescon += _(
u"Número do SIGAD atualizado.\n"
)
alertas += 1
if convenio.num_convenio != numero:
self.add_message(
_(u"\tO contrato Gescon ID {id} corresponde ao "
u"convênio SIGI {url}, mas o número do convênio"
u" diverge (Gescon: {numero_gescon}, SIGI: "
u"{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
)
)
convenio.num_convenio = numero
convenio.observacao_gescon += _(
u"Número do convênio atualizado.\n"
)
alertas += 1
if contrato['objeto'] not in convenio.observacao:
convenio.observacao += "\n" + contrato['objeto']
convenio.observacao_gescon += _(
u"Observação atualizada.\n"
)
convenio.data_sigad = contrato['assinatura']
convenio.data_retorno_assinatura = contrato[
'inicioVigencia'
]
convenio.data_termino_vigencia = contrato[
'terminoVigencia'
]
convenio.data_pub_diario = contrato['publicacao']
try:
convenio.save()
except Exception as e:
self.add_message(
_(u"Ocorreu um erro ao salvar o convênio {url} no "
u"SIGI. Alguma informação do Gescon pode ter "
u"quebrado o sistema. Informe ao suporte. Erro:"
u"{errmsg}").format(
url=reverse('admin:%s_%s_change' % (
convenio._meta.app_label,
convenio._meta.model_name),
args=[convenio.id]
),
errmsg=str(e)
)
)
erros += 1
continue
atualizados += 1
else:
self.add_message(_(u"\tExistem {count} convênios no SIGI "
u"que correspondem ao mesmo contrato no "
u"Gescon (contrato {numero}, sigad "
u"{sigad})").format(
count=chk,
numero=numero,
sigad=sigad
)
)
erros += 1
continue
self.add_message(
_(u"\t{novos} novos convenios adicionados ao SIGI, "
u"{atualizados} atualizados, sendo {alertas} com alertas, e "
u"{erros} reportados com erro.").format(
novos=novos,
atualizados=atualizados,
alertas=alertas,
erros=erros
)
)
self.save()
@classmethod
def load(cls):
obj, created = cls.objects.get_or_create(pk=1)
return obj

18
sigi/apps/convenios/templates/convenios/importar_gescon.html

@ -0,0 +1,18 @@
{% extends 'admin/base_site.html' %}
{% load i18n %}
{% block content_title %}<h1>{% trans 'Importar dados do Gescon' %}</h1>{% endblock %}
{% block object-tools-items %}
<li class="nav-item"><a class="nav-link active" href="{% url 'importar-gescon' %}?action=importar">Importar</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'admin:convenios_gescon_change' gescon.id %}">Configurações</a></li>
{% endblock %}
{% block content %}
{% if gescon.ultima_importacao %}
<pre><code>{{ gescon.ultima_importacao }}</code></pre>
{% else %}
{% blocktrans %}
<p class="alert alert-danger"><strong>Nenhuma importação anterior foi realizada!</strong></p>
<p class="">Configure a conexão com o Gescon para realizar a primeira importação.</p>
{% endblocktrans %}
{% endif %}
{% endblock %}

1
sigi/apps/convenios/urls.py

@ -11,4 +11,5 @@ urlpatterns = patterns(
url(r'^convenio/carrinho/deleta_itens_carrinho$', 'deleta_itens_carrinho', name='deleta-itens-carrinho'), # tagerror
url(r'^convenio/csv/$', 'export_csv', name='convenios-csv'),
url(r'^reportsRegiao/(?P<regiao>\w+)/$', 'report_regiao', name='convenios-report_regiao_pdf'),
url(r'^importar/$', 'importar_gescon', name='importar-gescon'),
)

16
sigi/apps/convenios/views.py

@ -2,6 +2,7 @@
import csv
import datetime
from django.http.response import HttpResponseForbidden
import ho.pisa as pisa
from django.conf import settings
from django.core.paginator import Paginator, InvalidPage, EmptyPage
@ -13,7 +14,7 @@ from geraldo.generators import PDFGenerator
from sigi.apps.casas.models import Orgao
from sigi.apps.contatos.models import UnidadeFederativa
from sigi.apps.convenios.models import Convenio, Projeto
from sigi.apps.convenios.models import Convenio, Gescon, Projeto
from sigi.apps.convenios.reports import (ConvenioReport,
ConvenioReportSemAceite,
ConvenioPorCMReport,
@ -372,3 +373,16 @@ def export_csv(request):
csv_writer.writerow(lista)
return response
@login_required
def importar_gescon(request):
if not request.user.is_superuser:
return HttpResponseForbidden()
action = request.GET.get('action', "")
gescon = Gescon.load()
if action == 'importar':
gescon.importa_contratos()
return render(request, "convenios/importar_gescon.html", {'gescon': gescon})

14
sigi/shortcuts.py

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from cgi import escape
from datetime import datetime
import os
from django.conf import settings
@ -28,12 +29,15 @@ def render_to_pdf(template_src, context_dict):
filename = template_src.replace('.html', '').replace('_pdf', '.pdf')
template = get_template(template_src)
context = Context(context_dict)
html = template.render(context)
result = StringIO.StringIO()
pdf = pisa.pisaDocument(StringIO.StringIO(html.encode('utf-8')), result, link_callback=fetch_resources)
if not pdf.err:
response = HttpResponse(result.getvalue(), content_type='application/pdf')
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename=' + filename
return response
pdf = pisa.CreatePDF(html, dest=response,
link_callback=fetch_resources)
if pdf.err:
return HttpResponse(_(u'We had some errors<pre>%s</pre>') % escape(html))
return response

1
templates/admin/base_site.html

@ -41,6 +41,7 @@
<li><a href="/sites/site/">{% trans 'Sites' %}</a></li>
<li><a href="/diagnosticos/">{% trans 'Diagnósticos' %}</a></li>
<li><a href="{% url 'importar-casas' %}">{% trans 'Importar dados de Casas' %}</a></li>
<li><a href="{% url 'importar-gescon' %}">{% trans 'Importar convênios do Gescon' %}</a></li>
</ul>
</li>
{% endif %}

Loading…
Cancel
Save