Browse Source

Melhora algoritmo de importação do Gescon

pull/174/head
Sesóstris Vieira 8 months ago
parent
commit
6ff10d1427
  1. 4
      sigi/apps/convenios/admin.py
  2. 392
      sigi/apps/convenios/models.py

4
sigi/apps/convenios/admin.py

@ -318,9 +318,7 @@ class GesconAdmin(admin.ModelAdmin):
"url_gescon", "url_gescon",
"email", "email",
) )
exclude = [ exclude = ["ultima_importacao", "checksums"]
"ultima_importacao",
]
admin.site.register(StatusConvenio) admin.site.register(StatusConvenio)

392
sigi/apps/convenios/models.py

@ -1,9 +1,10 @@
import re import re
import requests import requests
from difflib import SequenceMatcher
from hashlib import md5 from hashlib import md5
from pathlib import Path from pathlib import Path
from django.db import models from django.db import models
from django.db.models import Q, fields from django.db.models import Q, F
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
from django.template import Template, Context from django.template import Template, Context
@ -17,12 +18,11 @@ from docx import Document
from tinymce.models import HTMLField from tinymce.models import HTMLField
from weasyprint import HTML from weasyprint import HTML
from sigi.apps.contatos.models import Municipio, UnidadeFederativa from sigi.apps.contatos.models import Municipio, UnidadeFederativa
from sigi.apps.eventos.models import Evento
from sigi.apps.parlamentares.models import Parlamentar from sigi.apps.parlamentares.models import Parlamentar
from sigi.apps.utils import to_ascii from sigi.apps.utils import to_ascii
from sigi.apps.casas.models import Funcionario, Orgao from sigi.apps.casas.models import Funcionario, Orgao, TipoOrgao
from sigi.apps.servidores.models import Servidor, Servico from sigi.apps.servidores.models import Servidor, Servico
from sigi.apps.utils import editor_help from sigi.apps.utils import editor_help, mask_cnpj
class Projeto(models.Model): class Projeto(models.Model):
@ -608,7 +608,7 @@ class Gescon(models.Model):
pass # Highlander is immortal pass # Highlander is immortal
def add_message(self, msg, save=False): def add_message(self, msg, save=False):
self.ultima_importacao += msg + "\n" self.ultima_importacao += msg + "\n\n"
if save: if save:
self.save() self.save()
self.email_report() self.email_report()
@ -635,6 +635,21 @@ class Gescon(models.Model):
boolean: Indica se o que reportar ao usuário (erro ou dados boolean: Indica se o que reportar ao usuário (erro ou dados
importados/atualizados) importados/atualizados)
""" """
def mathnames(nome, orgaos):
for o, nome_canonico in orgaos:
ratio = SequenceMatcher(
None, to_ascii(nome).lower(), nome_canonico
).ratio()
if ratio > 0.9:
yield (o, ratio)
def get_semelhantes(nome, orgaos):
return sorted(
mathnames(nome, orgaos),
key=lambda m: m[1],
)
self.ultima_importacao = "" self.ultima_importacao = ""
if self.checksums is None: if self.checksums is None:
self.checksums = {} self.checksums = {}
@ -657,7 +672,7 @@ class Gescon(models.Model):
if self.subespecies == "": if self.subespecies == "":
self.add_message( self.add_message(
_("Nenhuma subespécie definida - processo " "abortado."), True _("Nenhuma subespécie definida - processo abortado."), True
) )
return True return True
@ -676,11 +691,12 @@ class Gescon(models.Model):
excludentes = self.palavras_excluir.splitlines() excludentes = self.palavras_excluir.splitlines()
orgaos = self.orgaos_gestores.split() orgaos = self.orgaos_gestores.split()
subespecies = {tuple(s.split("=")) for s in self.subespecies.split()} subespecies = {tuple(s.split("=")) for s in self.subespecies.split()}
lista_cnpj = { todos_orgaos = [
re.sub("[^\d]", "", o.cnpj).zfill(14): o (o, f"{to_ascii(o.nome)} - {o.uf_sigla}".lower())
for o in Orgao.objects.exclude(cnpj="") for o in Orgao.objects.all()
if re.sub("[^\d]", "", o.cnpj) != "" .order_by()
} .annotate(uf_sigla=F("municipio__uf__sigla"))
]
requests.packages.urllib3.disable_warnings() requests.packages.urllib3.disable_warnings()
report_user = False report_user = False
@ -705,7 +721,7 @@ class Gescon(models.Model):
report_user = True report_user = True
continue continue
if not "application/json" in response.headers.get("Content-Type"): if "application/json" not in response.headers.get("Content-Type"):
self.add_message( self.add_message(
_( _(
f"\tResultado da consulta à {url} não " f"\tResultado da consulta à {url} não "
@ -721,8 +737,8 @@ class Gescon(models.Model):
and self.checksums[sigla_gescon] == md5sum and self.checksums[sigla_gescon] == md5sum
): ):
self.add_message( self.add_message(
f"Dados da subespécie {sigla_gescon} inalterados no Gescon." f"\tDados da subespécie {sigla_gescon} inalterados no "
" Processamento desnecessário." "Gescon. Processamento desnecessário."
) )
continue continue
@ -754,103 +770,218 @@ class Gescon(models.Model):
atualizados = 0 atualizados = 0
for contrato in nossos: for contrato in nossos:
numero = contrato["numero"].zfill(8) numero = re.sub(
numero = f"{numero[:4]}/{numero[4:]}" r"(\d{4})(\d{4})", r"\1/\2", contrato["numero"].zfill(8)
sigad = contrato["processo"].zfill(17) )
sigad = ( sigad = re.sub(
f"{sigad[:5]}.{sigad[5:11]}/{sigad[11:15]}-{sigad[15:]}" r"(\d{5})(\d{6})(\d{4})(\d{2})",
r"\1.\2/\3-\4",
contrato["processo"].zfill(17),
) )
if contrato["cnpjCpfFornecedor"]: if contrato["cnpjCpfFornecedor"]:
cnpj = contrato["cnpjCpfFornecedor"].zfill(14) cnpj = contrato["cnpjCpfFornecedor"].zfill(14)
cnpj_masked = ( cnpj_masked = mask_cnpj(cnpj)
f"{cnpj[:2]}.{cnpj[2:5]}.{cnpj[5:8]}/"
f"{cnpj[8:12]}-{cnpj[12:]}"
)
else: else:
cnpj = None cnpj = None
if contrato["nomeFornecedor"]: if contrato["nomeFornecedor"]:
nome = contrato["nomeFornecedor"] nome = to_ascii(
nome = nome.replace("VEREADORES DE", "") contrato["nomeFornecedor"]
nome = nome.split("-")[0] .replace("VEREADORES DE", "")
nome = nome.split("/")[0] .replace("DO ESTADO", "")
nome = nome.strip() .split("-")[0]
nome = nome.replace(" ", " ") .split("/")[0]
nome = to_ascii(nome) .strip()
.replace(" ", " ")
)
else: else:
nome = None nome = None
if (cnpj is None) and (nome is None): # Buscar o Convenio pelo NUP #
try:
convenio = Convenio.objects.get(
projeto=projeto, num_processo_sf=sigad
)
except Convenio.DoesNotExist:
# Encontrou 0: Pode ser que só exista com o código Gescon
try:
convenio = Convenio.objects.get(
Q(projeto=projeto)
& Q(
Q(num_convenio=numero)
| Q(num_processo_sf=numero)
)
)
except Convenio.DoesNotExist:
# Encontrou 0: Não existe mesmo. Precisa ser criado.
# Para não esticar muito a profundidade do código,
# vou setar convenio para None e tratar o caso lá na
# frente.
convenio = None
except Convenio.MultipleObjectsReturned:
# Encontrou N: Reportar erro
self.add_message(
_(
f"\t* O contrato {numero} no Gescon pode ser "
"relacionado aos seguintes convênios do SIGI:"
+ ", ".join(
[
reverse(
"admin:convenios_convenio_change",
args=[c.id],
)
for c in Convenio.objects.filter(
Q(num_convenio=numero)
| Q(num_processo_sf=numero)
)
]
)
)
)
erros += 1
continue
except Convenio.MultipleObjectsReturned:
self.add_message( self.add_message(
_( _(
f"\tO contrato {numero} no Gescon não informa o CNPJ" f"\t* O contrato {numero} no Gescon pode ser "
"nem o nome do órgão." "relacionado aos seguintes convênios do SIGI: "
+ ", ".join(
[
reverse(
"admin:convenios_convenio_change",
args=[c.id],
)
for c in Convenio.objects.filter(
num_processo_sf=sigad
)
]
)
) )
) )
erros += 1 erros += 1
continue continue
if convenio is not None:
# Encontrou 1: Basta atualizar
convenio.projeto = projeto
convenio.num_processo_sf = sigad
convenio.num_convenio = numero
convenio.data_sigad = contrato["assinatura"]
convenio.observacao = contrato["objeto"]
convenio.data_retorno_assinatura = contrato[
"inicioVigencia"
]
convenio.data_termino_vigencia = contrato[
"terminoVigencia"
]
convenio.data_pub_diario = contrato["publicacao"]
convenio.atualizacao_gescon = timezone.localtime()
atualizados += 1
continue
orgao = None # Não encontrou o convênio. Vamos tentar criar...
# Primeiro, é preciso identificar qual órgão consta no
# contrato do Gescon
if (cnpj is None) and (nome is None):
self.add_message(
_(
f"\t* O contrato {numero} no Gescon não informa "
"nem o CNPJ nem o nome do órgão, então não é "
"possível importar para o SIGI."
)
)
erros += 1
continue
# Vamos tentar primeiro com o CNPJ
if cnpj is not None: if cnpj is not None:
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: try:
orgao = Orgao.objects.get(search_text__iexact=nome) orgao = Orgao.objects.get(cnpj=cnpj_masked)
except ( except Orgao.MultipleObjectsReturned:
Orgao.DoesNotExist, # Pode acontecer de uma cãmara usar o mesmo CNPJ
Orgao.MultipleObjectsReturned, # da prefeitura, e ambos terem convênio com o ILB.
) as e: # Podemos tentar desambiguar pelo nome mais
# semelhante.
orgao = get_semelhantes(
to_ascii(contrato["nomeFornecedor"]).lower(),
[
(
o,
f"{to_ascii(o.nome)} - {o.uf_sigla}".lower(),
)
for o in Orgao.objects.filter(cnpj=cnpj_masked)
.order_by()
.annotate(uf_sigla=F("municipio__uf__sigla"))
],
)[0][0]
except Orgao.DoesNotExist:
# Encontrou 0: Vamos seguir sem órgao e tentar
# encontrar pelo nome logo abaixo
orgao = None orgao = None
pass
if orgao is None: if orgao is None:
# Não achou pelo CNPJ. Bora ver se acha por similaridade
# do nome
if nome is None:
# Também não tem nome... então temos que reportar erro
self.add_message(
_(
f"\t* O contrato {numero} no Gescon "
f"com NUP sigad {sigad}, fornecedor "
f"{cnpj_masked} não pode ser imortado porque "
"não é possível identificar o órgão no SIGI. "
"Cadastre um órgão com o CNPJ desse "
"fornecedor, que na próxima importação este "
"contrato será importado."
)
)
erros += 1
continue
# Primeiro com o nome igual veio do GESCON
semelhantes = get_semelhantes(
to_ascii(contrato["nomeFornecedor"]).lower(),
todos_orgaos,
)
if not semelhantes:
# Não achou, então vamos tentar com o nome limpado
semelhantes = get_semelhantes(
to_ascii(nome).lower(),
todos_orgaos,
)
if len(semelhantes) > 0:
# Encontrou algo semelhante.... bora usar.
orgao = semelhantes[0][0]
else:
# Não encontrou nada parecido. Bora reportar como erro
self.add_message(
_(
f"\t* O contrato {numero} no Gescon "
f"com NUP Sigad {sigad}, indica o "
f"fornecedor com CNPJ {cnpj_masked} "
f"e com o nome {contrato['nomeFornecedor']}, "
"que não tem correspondência no SIGI. "
"Este convênio precisa ser cadastrado "
"manualmente no SIGI para este erro "
"parar de acontecer."
)
)
erros += 1
continue
# Não encontrou o órgão... bora reportar o erro
if orgao is None:
# Em teoria, nunca vai cair aqui... mas...
self.add_message( self.add_message(
_( _(
f"\tÓrgão não encontrado no SIGI ou mais de um " f"\t* Órgão não encontrado no SIGI ou mais de um "
f"órgão encontrado com o mesmo CNPJ ou nome. Favor " f"órgão encontrado com o mesmo CNPJ ou nome. Favor"
f"regularizar o cadastro: " f" regularizar o cadastro: "
f"CNPJ: {contrato['cnpjCpfFornecedor']}, " f"CNPJ: {contrato['cnpjCpfFornecedor']}, "
f"Nome: {contrato['nomeFornecedor']}" f"Nome: {contrato['nomeFornecedor']}"
) )
) )
erros += 1 erros += 1
continue continue
else:
# O mais seguro é o NUP sigad # Bora criar o convênio
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( convenio = Convenio(
casa_legislativa=orgao, casa_legislativa=orgao,
projeto=projeto, projeto=projeto,
@ -864,116 +995,19 @@ class Gescon(models.Model):
data_pub_diario=contrato["publicacao"], data_pub_diario=contrato["publicacao"],
atualizacao_gescon=timezone.localtime(), atualizacao_gescon=timezone.localtime(),
observacao_gescon=_( observacao_gescon=_(
"Importado integralmente do" "Gescon" "Importado integralmente do Gescon"
), ),
) )
convenio.save() convenio.save()
novos += 1 novos += 1
continue continue
elif chk == 1:
convenio = convenios.get()
convenio.atualizacao_gescon = timezone.localtime()
convenio.observacao_gescon = ""
if convenio.casa_legislativa != orgao:
self.add_message(
_(
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!"
)
convenio.save()
erros += 1
continue
if convenio.num_processo_sf != sigad:
self.add_message(
_(
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"
)
alertas += 1
if convenio.num_convenio != numero:
self.add_message(
_(
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 += _(
"Número do convênio atualizado.\n"
)
alertas += 1
if contrato["objeto"] not in convenio.observacao:
convenio.observacao += "\n" + contrato["objeto"]
convenio.observacao_gescon += _(
"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"]
if contrato["codTextoContrato"]:
convenio.id_contrato_gescon = contrato[
"codTextoContrato"
]
else:
convenio.id_contrato_gescon = ""
try:
convenio.save()
except Exception as e:
self.add_message(
_(
"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(
_(
f"\tExistem {chk} convênios no SIGI que "
"correspondem ao mesmo contrato no Gescon (contrato "
f"{numero}, sigad {sigad})"
)
)
erros += 1
continue
if novos or erros or alertas or atualizados: if novos or erros or alertas or atualizados:
report_user = True report_user = True
self.add_message( self.add_message(
_( _(
f"\t{novos} novos convenios adicionados ao SIGI, " f"\n\n\t{novos} novos convenios adicionados ao SIGI, "
f"{atualizados} atualizados, sendo {alertas} com alertas, e " f"{atualizados} atualizados, sendo {alertas} com alertas, e "
f"{erros} reportados com erro." f"{erros} reportados com erro."
) )

Loading…
Cancel
Save