diff --git a/sigi/apps/convenios/admin.py b/sigi/apps/convenios/admin.py index dcaa156..328f46f 100644 --- a/sigi/apps/convenios/admin.py +++ b/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 @@ -57,11 +58,14 @@ class ConvenioAdmin(BaseModelAdmin): {'fields': ('servico_gestao', 'servidor_gestao',)} ), (_(u'Datas'), - {'fields': ('data_retorno_assinatura', 'data_termino_vigencia', - 'data_pub_diario',)} + {'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,8 +175,13 @@ 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) admin.site.register(Convenio, ConvenioAdmin) -admin.site.register(EquipamentoPrevisto, EquipamentoPrevistoAdmin) +admin.site.register(EquipamentoPrevisto, EquipamentoPrevistoAdmin) \ No newline at end of file diff --git a/sigi/apps/convenios/migrations/0014_gescon.py b/sigi/apps/convenios/migrations/0014_gescon.py new file mode 100644 index 0000000..7fe216c --- /dev/null +++ b/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}.
Por exemplo: https://adm.senado.gov.br/gestao-contratos/api/contratos/busca?especie={s}', 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. ', 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,), + ), + ] diff --git a/sigi/apps/convenios/migrations/0015_remove_convenio_search_text.py b/sigi/apps/convenios/migrations/0015_remove_convenio_search_text.py new file mode 100644 index 0000000..c187381 --- /dev/null +++ b/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', + ), + ] diff --git a/sigi/apps/convenios/migrations/0016_auto_20210909_0732.py b/sigi/apps/convenios/migrations/0016_auto_20210909_0732.py new file mode 100644 index 0000000..9c98cbc --- /dev/null +++ b/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, + ), + ] diff --git a/sigi/apps/convenios/models.py b/sigi/apps/convenios/models.py index 59f00fa..b4dfc79 100644 --- a/sigi/apps/convenios/models.py +++ b/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}.
Por exemplo: " + u"https://adm.senado.gov.br/gestao-contratos/api/contratos" + u"/busca?especie={s}") + ) + 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"") + ) + 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 \ No newline at end of file diff --git a/sigi/apps/convenios/templates/convenios/importar_gescon.html b/sigi/apps/convenios/templates/convenios/importar_gescon.html new file mode 100644 index 0000000..49f068a --- /dev/null +++ b/sigi/apps/convenios/templates/convenios/importar_gescon.html @@ -0,0 +1,18 @@ +{% extends 'admin/base_site.html' %} +{% load i18n %} + +{% block content_title %}

{% trans 'Importar dados do Gescon' %}

{% endblock %} +{% block object-tools-items %} + + +{% endblock %} +{% block content %} + {% if gescon.ultima_importacao %} +
{{ gescon.ultima_importacao }}
+ {% else %} + {% blocktrans %} +

Nenhuma importação anterior foi realizada!

+

Configure a conexão com o Gescon para realizar a primeira importação.

+ {% endblocktrans %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/sigi/apps/convenios/urls.py b/sigi/apps/convenios/urls.py index d3b753a..1986ecc 100644 --- a/sigi/apps/convenios/urls.py +++ b/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\w+)/$', 'report_regiao', name='convenios-report_regiao_pdf'), + url(r'^importar/$', 'importar_gescon', name='importar-gescon'), ) diff --git a/sigi/apps/convenios/views.py b/sigi/apps/convenios/views.py index 6760828..dd21f0e 100644 --- a/sigi/apps/convenios/views.py +++ b/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}) \ No newline at end of file diff --git a/sigi/shortcuts.py b/sigi/shortcuts.py index f5f0fff..d27986c 100644 --- a/sigi/shortcuts.py +++ b/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['Content-Disposition'] = 'attachment; filename=' + filename - return response - return HttpResponse(_(u'We had some errors
%s
') % escape(html)) + + response = HttpResponse(content_type='application/pdf') + response['Content-Disposition'] = 'attachment; filename=' + filename + + pdf = pisa.CreatePDF(html, dest=response, + link_callback=fetch_resources) + + if pdf.err: + return HttpResponse(_(u'We had some errors
%s
') % escape(html)) + return response diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html index 6f61a15..f7d3120 100644 --- a/templates/admin/base_site.html +++ b/templates/admin/base_site.html @@ -41,6 +41,7 @@
  • {% trans 'Sites' %}
  • {% trans 'Diagnósticos' %}
  • {% trans 'Importar dados de Casas' %}
  • +
  • {% trans 'Importar convênios do Gescon' %}
  • {% endif %}