diff --git a/sigi/apps/casas/views.py b/sigi/apps/casas/views.py
index e37c700..e53c45a 100644
--- a/sigi/apps/casas/views.py
+++ b/sigi/apps/casas/views.py
@@ -1,7 +1,5 @@
import csv
-from functools import reduce
-from django.db.models import Count, Q, Prefetch
-from django.contrib.admin.sites import site
+from django.db.models import Count, Q, Prefetch, F
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
@@ -19,10 +17,10 @@ from django.views.generic import (
DeleteView,
ListView,
UpdateView,
- DetailView,
)
+from django_weasyprint.views import WeasyTemplateResponse
from rest_framework import generics, filters
-from sigi.apps.casas.forms import FuncionarioForm
+from sigi.apps.casas.forms import FuncionarioForm, CnpjErradoForm
from sigi.apps.casas.models import Funcionario, Orgao, TipoOrgao
from sigi.apps.casas.serializers import OrgaoAtendidoSerializer
from sigi.apps.home.mixins import ContatoInterlegisViewMixin
@@ -36,6 +34,7 @@ from sigi.apps.ocorrencias.models import Ocorrencia
from sigi.apps.servicos.models import Servico, TipoServico
from sigi.apps.eventos.models import Evento, TipoEvento
from sigi.apps.convenios.models import Convenio
+from sigi.apps.utils import valida_cnpj
def resumo_carteira(casas):
@@ -339,6 +338,117 @@ def painel_relacionamento(request):
return render(request, "casas/painel.html", context)
+@login_required
+@staff_member_required
+def cnpj_duplicado(request):
+ formato = request.GET.get("fmt", "html")
+ dups = (
+ Orgao.objects.exclude(cnpj="")
+ .order_by("cnpj")
+ .values("cnpj")
+ .annotate(tot=Count("cnpj"))
+ .filter(tot__gt=1)
+ .values("cnpj")
+ )
+ orgaos = (
+ Orgao.objects.filter(cnpj__in=dups)
+ .order_by("cnpj", "nome", "municipio__nome", "municipio__uf")
+ .prefetch_related("tipo", "municipio", "municipio__uf")
+ )
+ context = {
+ "orgaos": orgaos,
+ "title": _("Órgãos com CNPJ duplicado"),
+ }
+ if formato == "pdf":
+ return WeasyTemplateResponse(
+ filename="cnpj_duplicado.pdf",
+ request=request,
+ template="casas/cnpj_duplicado_pdf.html",
+ context=context,
+ content_type="application/pdf",
+ )
+ elif formato == "csv":
+ response = HttpResponse(content_type="text/csv")
+ response["Content-Disposition"] = (
+ 'attachment; filename="cnpj_duplicado.csv"'
+ )
+ fieldnames = [
+ "id",
+ "cnpj",
+ "tipo__nome",
+ "sigla",
+ "nome",
+ "municipio__nome",
+ "municipio__uf__sigla",
+ ]
+ writer = csv.DictWriter(response, fieldnames)
+ writer.writeheader()
+ writer.writerows(orgaos.values(*fieldnames))
+ return response
+ return render(request, "casas/cnpj_duplicado.html", context=context)
+
+
+@login_required
+@staff_member_required
+def cnpj_errado(request):
+ formato = request.GET.get("fmt", "html")
+ form = CnpjErradoForm(request.GET)
+ if form.is_valid():
+ has_convenio = form.cleaned_data.get("has_convenio", False)
+ else:
+ has_convenio = False
+
+ todos_orgaos = (
+ Orgao.objects.exclude(cnpj="")
+ .order_by("tipo", "cnpj", "nome")
+ .annotate(
+ tipo_nome=F("tipo__nome"),
+ municipio_nome=F("municipio__nome"),
+ uf_sigla=F("municipio__uf__sigla"),
+ )
+ )
+ if has_convenio:
+ todos_orgaos = todos_orgaos.exclude(convenio=None)
+ orgaos = []
+ for orgao in todos_orgaos:
+ if not valida_cnpj(orgao.cnpj):
+ orgaos.append(orgao)
+ context = {
+ "orgaos": orgaos,
+ "form": form,
+ "title": _("Órgãos com CNPJ digitado errado"),
+ }
+ if formato == "pdf":
+ return WeasyTemplateResponse(
+ filename="cnpj_errado.pdf",
+ request=request,
+ template="casas/cnpj_errado_pdf.html",
+ context=context,
+ content_type="application/pdf",
+ )
+ elif formato == "csv":
+ response = HttpResponse(content_type="text/csv")
+ response["Content-Disposition"] = (
+ 'attachment; filename="cnpj_errado.csv"'
+ )
+ fieldnames = [
+ "id",
+ "cnpj",
+ "tipo_nome",
+ "sigla",
+ "nome",
+ "municipio_nome",
+ "uf_sigla",
+ ]
+ writer = csv.DictWriter(response, fieldnames)
+ writer.writeheader()
+ writer.writerows(
+ [{f: getattr(o, f) for f in fieldnames} for o in orgaos]
+ )
+ return response
+ return render(request, "casas/cnpj_errado.html", context=context)
+
+
class GerentesListView(PermissionRequiredMixin, ListView):
template_name = "admin/casas/gerentes_list.html"
_tipos = None
diff --git a/sigi/apps/convenios/admin.py b/sigi/apps/convenios/admin.py
index f3802e9..2cf6cc4 100644
--- a/sigi/apps/convenios/admin.py
+++ b/sigi/apps/convenios/admin.py
@@ -1,6 +1,5 @@
from django.db.models import Q
from django.contrib import admin
-from django.http import HttpResponse, HttpResponseRedirect
from django.utils import timezone
from django.utils.translation import gettext as _
from django.utils.safestring import mark_safe
@@ -14,12 +13,10 @@ from sigi.apps.convenios.models import (
Convenio,
EquipamentoPrevisto,
Anexo,
- Tramitacao,
Gescon,
)
from sigi.apps.utils.mixins import AsciifyQParameter
-from sigi.apps.servidores.models import Servidor
-from sigi.apps.casas.admin import ConveniosInline, GerentesInterlegisFilter
+from sigi.apps.casas.admin import GerentesInterlegisFilter
from sigi.apps.utils.mixins import (
ReturnMixin,
CartExportReportMixin,
@@ -172,6 +169,7 @@ class ConvenioAdmin(
_("Gescon"),
{
"fields": (
+ "erro_gescon",
"atualizacao_gescon",
"observacao_gescon",
"link_gescon",
@@ -181,6 +179,7 @@ class ConvenioAdmin(
)
readonly_fields = (
"data_sigi",
+ "erro_gescon",
"atualizacao_gescon",
"observacao_gescon",
"link_gescon",
@@ -210,6 +209,7 @@ class ConvenioAdmin(
"conveniada",
"equipada",
"casa_legislativa__municipio__uf",
+ "erro_gescon",
)
ordering = (
"casa_legislativa__municipio__uf__sigla",
diff --git a/sigi/apps/convenios/migrations/0036_convenio_erro_gescon.py b/sigi/apps/convenios/migrations/0036_convenio_erro_gescon.py
new file mode 100644
index 0000000..d10c793
--- /dev/null
+++ b/sigi/apps/convenios/migrations/0036_convenio_erro_gescon.py
@@ -0,0 +1,20 @@
+# Generated by Django 5.0.4 on 2024-05-15 11:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("convenios", "0035_convenio_data_extincao_convenio_motivo_extincao"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="convenio",
+ name="erro_gescon",
+ field=models.BooleanField(
+ default=False, max_length=1, verbose_name="erro no Gescon"
+ ),
+ ),
+ ]
diff --git a/sigi/apps/convenios/models.py b/sigi/apps/convenios/models.py
index 5e76640..e0fcf38 100644
--- a/sigi/apps/convenios/models.py
+++ b/sigi/apps/convenios/models.py
@@ -5,6 +5,7 @@ from hashlib import md5
from pathlib import Path
from django.db import models
from django.db.models import Q, F
+from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail
from django.core.validators import FileExtensionValidator
from django.template import Template, Context
@@ -20,7 +21,7 @@ from weasyprint import HTML
from sigi.apps.contatos.models import Municipio, UnidadeFederativa
from sigi.apps.parlamentares.models import Parlamentar
from sigi.apps.utils import to_ascii
-from sigi.apps.casas.models import Funcionario, Orgao, TipoOrgao
+from sigi.apps.casas.models import Funcionario, Orgao
from sigi.apps.servidores.models import Servidor, Servico
from sigi.apps.utils import editor_help, mask_cnpj
@@ -327,6 +328,11 @@ class Convenio(models.Model):
atualizacao_gescon = models.DateTimeField(
_("Data de atualização pelo Gescon"), blank=True, null=True
)
+ erro_gescon = models.BooleanField(
+ _("erro no Gescon"),
+ max_length=1,
+ default=False,
+ )
observacao_gescon = models.TextField(
_("Observações da atualização do Gescon"), blank=True
)
@@ -636,7 +642,7 @@ class Gescon(models.Model):
importados/atualizados)
"""
- def mathnames(nome, orgaos):
+ def mathnames(nome, orgaos, all=False):
for o, nome_canonico in orgaos:
ratio = SequenceMatcher(
None, to_ascii(nome).lower(), nome_canonico
@@ -644,9 +650,9 @@ class Gescon(models.Model):
if ratio > 0.9:
yield (o, ratio)
- def get_semelhantes(nome, orgaos):
+ def get_semelhantes(nome, orgaos, all=False):
return sorted(
- mathnames(nome, orgaos),
+ mathnames(nome, orgaos, all),
key=lambda m: m[1],
)
@@ -700,6 +706,9 @@ class Gescon(models.Model):
requests.packages.urllib3.disable_warnings()
report_user = False
+ Convenio.objects.update(erro_gescon=False)
+
+ dominio = get_current_site(None).domain
for sigla_gescon, sigla_sigi in subespecies:
self.add_message(_(f"\n**Importando subespécie {sigla_gescon}**"))
@@ -784,6 +793,7 @@ class Gescon(models.Model):
cnpj_masked = mask_cnpj(cnpj)
else:
cnpj = None
+ cnpj_masked = None
if contrato["nomeFornecedor"]:
nome = to_ascii(
@@ -799,70 +809,58 @@ class Gescon(models.Model):
nome = None
# Buscar o Convenio pelo NUP #
- try:
- convenio = Convenio.objects.get(
- projeto=projeto, num_processo_sf=sigad
- )
- except Convenio.DoesNotExist:
+ convenios = Convenio.objects.filter(
+ projeto=projeto, num_processo_sf=sigad
+ )
+ if convenios.count() == 0:
# 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)
- )
- ]
- )
+ convenios = Convenio.objects.filter(
+ Q(projeto=projeto)
+ & Q(Q(num_convenio=numero) | Q(num_processo_sf=numero))
+ )
+ if convenios.count() > 1:
+ # Encontrou N: Marcamos todos como erro e reportamos
+ urls = ", ".join(
+ [
+ '{id}'.format(
+ dominio=dominio,
+ uri=reverse(
+ "admin:convenios_convenio_change",
+ args=[c.id],
+ ),
+ id=c.id,
)
- )
- erros += 1
- continue
- except Convenio.MultipleObjectsReturned:
+ for c in convenios
+ ]
+ )
+ convenios.update(
+ erro_gescon=True,
+ observacao_gescon=_(
+ "Este convênio possui o mesmo número dos "
+ f"convenios {urls}"
+ ),
+ )
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(
- num_processo_sf=sigad
- )
- ]
- )
+ f"\t* O contrato {numero} no Gescon pode "
+ "ser relacionado aos seguintes convênios "
+ f"do SIGI: {urls}"
)
)
erros += 1
- continue
- if convenio is not None:
- # Encontrou 1: Basta atualizar
+ # Porém, talvez seja possível ser desambiguado pelo CNPJ do
+ # fornecedor
+ if cnpj_masked is not None:
+ convenios = convenios.filter(
+ casa_legislativa__cnpj=cnpj_masked
+ )
+ if convenios.count() != 1:
+ # Continua ambíguo. Não dá pra fazer nada.
+ continue
+ if convenios.count() == 1:
+ # Achou exatamente o único que deveria existir. Basta
+ # atualizar os dados
+ convenio = convenios.get()
convenio.projeto = projeto
convenio.num_processo_sf = sigad
convenio.num_convenio = numero
@@ -876,10 +874,25 @@ class Gescon(models.Model):
]
convenio.data_pub_diario = contrato["publicacao"]
convenio.atualizacao_gescon = timezone.localtime()
+ convenio.erro_gescon = False
+ convenio.observacao_gescon = ""
+ convenio.id_contrato_gescon = (
+ contrato["codTextoContrato"] or ""
+ )
+ convenio.save()
atualizados += 1
+ # Corrigir o CNPJ do órgão se estiver diferente do Gescon
+ # O gescon é um pouquinho mais confiável, por enquanto.
+ if (
+ cnpj_masked
+ and convenio.casa_legislativa.cnpj != cnpj_masked
+ ):
+ convenio.casa_legislativa.cnpj = cnpj_masked
+ convenio.casa_legislativa.save()
continue
- # Não encontrou o convênio. Vamos tentar criar...
+ # Se chegou aqui, é porque não encontrou o convênio.
+ # Um novo convênio precisa ser criado.
# Primeiro, é preciso identificar qual órgão consta no
# contrato do Gescon
if (cnpj is None) and (nome is None):
@@ -897,7 +910,7 @@ class Gescon(models.Model):
try:
orgao = Orgao.objects.get(cnpj=cnpj_masked)
except Orgao.MultipleObjectsReturned:
- # Pode acontecer de uma cãmara usar o mesmo CNPJ
+ # Pode acontecer de uma câmara usar o mesmo CNPJ
# da prefeitura, e ambos terem convênio com o ILB.
# Podemos tentar desambiguar pelo nome mais
# semelhante.
@@ -912,12 +925,12 @@ class Gescon(models.Model):
.order_by()
.annotate(uf_sigla=F("municipio__uf__sigla"))
],
+ all=True,
)[0][0]
except Orgao.DoesNotExist:
# Encontrou 0: Vamos seguir sem órgao e tentar
# encontrar pelo nome logo abaixo
orgao = None
-
if orgao is None:
# Não achou pelo CNPJ. Bora ver se acha por similaridade
# do nome
@@ -936,7 +949,7 @@ class Gescon(models.Model):
)
erros += 1
continue
- # Primeiro com o nome igual veio do GESCON
+ # Tentar primeiro com o nome igual veio do GESCON
semelhantes = get_semelhantes(
to_ascii(contrato["nomeFornecedor"]).lower(),
todos_orgaos,
@@ -997,9 +1010,20 @@ class Gescon(models.Model):
observacao_gescon=_(
"Importado integralmente do Gescon"
),
+ id_contrato_gescon=(
+ contrato["codTextoContrato"] or ""
+ ),
)
convenio.save()
novos += 1
+ # Corrigir o CNPJ do órgão se estiver diferente do Gescon
+ # O gescon é um pouquinho mais confiável, por enquanto.
+ if (
+ cnpj_masked
+ and convenio.casa_legislativa.cnpj != cnpj_masked
+ ):
+ convenio.casa_legislativa.cnpj = cnpj_masked
+ convenio.casa_legislativa.save()
continue
if novos or erros or alertas or atualizados:
diff --git a/sigi/apps/convenios/templates/convenios/erros_gescon.html b/sigi/apps/convenios/templates/convenios/erros_gescon.html
new file mode 100644
index 0000000..0c051d6
--- /dev/null
+++ b/sigi/apps/convenios/templates/convenios/erros_gescon.html
@@ -0,0 +1,41 @@
+{% extends "admin/base_site.html" %}
+{% load static i18n %}
+
+{% block extrastyle %}
+{{ block.super }}
+
+
+{% endblock %}
+
+{% block coltype %}colMS{% endblock %}
+
+{% block content_title %}
+
+ {% blocktranslate count counter=convenios.count %}
+ Um convênio com erro na importação do Gescon
+ {% plural %}
+ {{ counter }} convênios com erro na importação do Gescon
+ {% endblocktranslate %}
+
+{% endblock %}
+
+{% block breadcrumbs %}
+{% endblock %}
+
+{% block content %}
+ {% include "convenios/snippets/erros_gescon_snippet.html" with mode="html" %}
+{% endblock %}
+
+{% block footer %}
+ {{ block.super }}
+
+{% endblock %}
\ No newline at end of file
diff --git a/sigi/apps/convenios/templates/convenios/erros_gescon_pdf.html b/sigi/apps/convenios/templates/convenios/erros_gescon_pdf.html
new file mode 100644
index 0000000..61c350c
--- /dev/null
+++ b/sigi/apps/convenios/templates/convenios/erros_gescon_pdf.html
@@ -0,0 +1,21 @@
+{% extends "pdf/base_report.html" %}
+{% load static i18n %}
+
+{% block page_size %}A4 landscape{% endblock %}
+
+{% block report_name %}
+ {% blocktranslate count counter=convenios.count %}
+ Um convênio com erro na importação do Gescon
+ {% plural %}
+ {{ counter }} convênios com erro na importação do Gescon
+ {% endblocktranslate %}
+{% endblock report_name %}
+
+{% block main_content %}
+ {% include "convenios/snippets/erros_gescon_snippet.html" %}
+
+
+
{% translate "Resumo da última importação de dados do Gescon" %}