diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 4dd2000..d973d0b 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -37,4 +37,5 @@ weasyprint==68.1
xlsxwriter==3.2.9
djbs-theme @ git+https://github.com/interlegis/djbs-theme.git@v1.0.8
django-dashboard @ git+https://github.com/interlegis/django-dashboard.git
-dx-job-controller @ git+https://github.com/interlegis/dx-job-controller.git@v1.0.3
+dx-job-controller @ git+https://github.com/interlegis/dx-job-controller.git@v1.0.5
+django-appconfig @ git+https://github.com/interlegis/django-appconfig.git@v1.0.0
\ No newline at end of file
diff --git a/sigi/apps/convenios/admin.py b/sigi/apps/convenios/admin.py
index 4bea15e..ceb3111 100644
--- a/sigi/apps/convenios/admin.py
+++ b/sigi/apps/convenios/admin.py
@@ -18,7 +18,6 @@ from sigi.apps.convenios.models import (
Convenio,
EquipamentoPrevisto,
Anexo,
- Gescon,
)
from sigi.apps.utils.mixins import AsciifyQParameter
from sigi.apps.casas.admin import GerentesInterlegisFilter
@@ -344,14 +343,5 @@ class EquipamentoPrevistoAdmin(admin.ModelAdmin):
)
-@admin.register(Gescon)
-class GesconAdmin(admin.ModelAdmin):
- list_display = (
- "url_gescon",
- "email",
- )
- exclude = ["ultima_importacao", "checksums"]
-
-
admin.site.register(StatusConvenio)
admin.site.register(TipoSolicitacao)
diff --git a/sigi/apps/convenios/appconfig.py b/sigi/apps/convenios/appconfig.py
new file mode 100644
index 0000000..bcc92ee
--- /dev/null
+++ b/sigi/apps/convenios/appconfig.py
@@ -0,0 +1,75 @@
+from django import forms
+from django.utils.translation import gettext_lazy as _
+
+
+class AppConfigForm(forms.Form):
+ form_title = _("Definições para convênios")
+ form_subtitle = _("Importação do Gescon")
+ url_gescon = forms.URLField(
+ required=True,
+ label=_("Webservice Gescon"),
+ initial=(
+ "https://adm.senado.gov.br/gestao-contratos/api/contratos"
+ "/busca?subespecie={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ção {s}.
Por exemplo: "
+ "https://adm.senado.gov.br/gestao-contratos/api/contratos"
+ "/busca?subespecie={s}"
+ ),
+ )
+ subespecies = forms.CharField(
+ required=True,
+ label=_("Subespécies"),
+ initial="AC=ACT\nTC=TC",
+ help_text=_(
+ "Informe as siglas das subespécies 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ão encontradas serão ignoradas."
+ ),
+ widget=forms.Textarea(),
+ )
+ palavras = forms.CharField(
+ required=True,
+ label=_("Palavras de filtro"),
+ initial="ILB\nINTERLEGIS\nSCCO",
+ help_text=_(
+ "Palavras que devem aparecer no campo OBJETO dos dados do "
+ "Gescon para identificar se o contrato pertence ao ILB. "
+ "
- Informe uma palavra por linha.
"
+ "- Ocorrendo qualquer uma das palavras, o contrato será "
+ "importado.
"
+ ),
+ widget=forms.Textarea(),
+ )
+ palavras_excluir = forms.CharField(
+ required=True,
+ label=_("palavras de exclusão"),
+ initial="DTCOM",
+ help_text=_(
+ "Palavras que não podem aparecer no campo OBJETO dos dados do "
+ "Gescon."
+ "- Informe uma palavra por linha.
"
+ "- Ocorrendo qualquer uma das palavras, o contrato será "
+ "ignorado.
"
+ ),
+ widget=forms.Textarea(),
+ )
+ orgaos_gestores = forms.CharField(
+ required=True,
+ label=_("Órgãos gestores"),
+ initial="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.
"
+ ),
+ widget=forms.Textarea(),
+ )
diff --git a/sigi/apps/convenios/jobs/hourly/importa_gescon.py b/sigi/apps/convenios/jobs/hourly/importa_gescon.py
index cfd60fd..501a509 100644
--- a/sigi/apps/convenios/jobs/hourly/importa_gescon.py
+++ b/sigi/apps/convenios/jobs/hourly/importa_gescon.py
@@ -1,16 +1,522 @@
+import re
import sys
+from django.forms.models import model_to_dict
+import requests
+from appconfig import config
+from hashlib import md5
+from url_normalize import url_normalize
+from django.db.models import Q, F
+from django.contrib.sites.shortcuts import get_current_site
+from django.urls import reverse
+from django.utils import timezone
from django.utils.translation import gettext as _
from django_extensions.management.jobs import HourlyJob
-from sigi.apps.convenios.models import Gescon
+from sigi.apps.utils import mask_cnpj, to_ascii
+from sigi.apps.casas.models import Orgao
+from sigi.apps.convenios.models import Projeto, Convenio
class Job(HourlyJob):
help = "Carga de dados do Gescon."
+ gescon = None
+ dominio = None
+ subespecies = None
+ todos_orgaos = None
+
+ novos = 0
+ erros = 0
+ atualizados = 0
+
def execute(self):
- gescon = Gescon.load()
- errors = gescon.importa_contratos()
- if errors:
- print(gescon.ultima_importacao, file=sys.stderr)
- else:
- print(gescon.ultima_importacao)
+ print(
+ "Importação iniciada em {timezone.localtime():%d/%m/%Y %H:%M:%S}\n"
+ "==========================================================\n"
+ )
+
+ if not self.setup():
+ return
+
+ for sigla_gescon, sigla_sigi in self.subespecies:
+ print(
+ _("\n**Importando subespécie {sigla_gescon}**").format(
+ sigla_gescon=sigla_gescon
+ )
+ )
+ try:
+ projeto = Projeto.objects.get(sigla=sigla_sigi)
+ except Projeto.DoesNotExist:
+ print(
+ _(
+ "Projeto com a sigla {sigla_sigi} não existe no SIGI"
+ ).format(sigla_sigi=sigla_sigi),
+ file=sys.stderr,
+ )
+ continue
+ except Projeto.MultipleObjectsReturned:
+ print(
+ _(
+ "A sigla {sigla_sigi} foi cadastrada com duplicidade "
+ "no SIGI"
+ ).format(sigla_sigi=sigla_sigi),
+ file=sys.stderr,
+ )
+ continue
+
+ nossos = self.get_contratos_gescon(sigla_gescon)
+ if not nossos:
+ print(
+ _(
+ "Nenhum contrato do tipo {sigla_gescon} pertence ao "
+ "ILB / Interlegis"
+ ).format(sigla_gescon=sigla_gescon)
+ )
+ continue
+
+ # Desmarca erros de importações passadas
+ Convenio.objects.filter(projeto=projeto).update(erro_gescon=False)
+
+ for contrato in nossos:
+ if contrato["numero"] is None:
+ print(
+ _(
+ "\t* O contrato de id {id} subespécie {sub} foi "
+ "cadastrado no Gescon sem número e não será "
+ "importado."
+ ).format(id=contrato["id"], sub=contrato["subEspecie"]),
+ file=sys.stderr,
+ )
+ continue
+ numero = re.sub(
+ r"(\d{4})(\d{4})", r"\1/\2", contrato["numero"].zfill(8)
+ )
+ sigad = re.sub(
+ r"(\d{5})(\d{6})(\d{4})(\d{2})",
+ r"\1.\2/\3-\4",
+ contrato["processo"].zfill(17),
+ )
+
+ if contrato["cnpjCpfFornecedor"]:
+ cnpj = contrato["cnpjCpfFornecedor"].zfill(14)
+ cnpj_masked = mask_cnpj(cnpj)
+ else:
+ cnpj = None
+ cnpj_masked = None
+
+ if contrato["nomeFornecedor"]:
+ nome = to_ascii(
+ contrato["nomeFornecedor"]
+ .replace("VEREADORES DE", "")
+ .replace("DO ESTADO", "")
+ .split("-")[0]
+ .split("/")[0]
+ .strip()
+ .replace(" ", " ")
+ )
+ else:
+ nome = None
+
+ convenio = self.get_convenio(
+ contrato, projeto, sigad, numero, cnpj_masked
+ )
+
+ if convenio:
+ # Encontrou um único convênio, basta atualizar seus dados
+ self.update_convenio(
+ convenio, contrato, projeto, sigad, numero
+ )
+ self.update_orgao(convenio.casa_legislativa, cnpj_masked)
+ else:
+ # Não encontrou o convênio. Um novo convênio precisa ser
+ # criado. Primeiro, é preciso identificar qual órgão
+ # consta no contrato do Gescon
+ orgao = self.get_orgao(
+ contrato, cnpj, cnpj_masked, nome, numero, sigad
+ )
+
+ if orgao is None:
+ # Não encontrou o órgão... bora reportar o erro
+ print(
+ _(
+ "\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: \n"
+ "\t * CNPJ: {cnpj}\n"
+ "\t * Nome: {nome}"
+ ).format(
+ cnpj=contrato["cnpjCpfFornecedor"],
+ nome=contrato["nomeFornecedor"],
+ ),
+ file=sys.stderr,
+ )
+ self.erros += 1
+ else:
+ # Bora criar o convênio
+ self.create_convenio(
+ orgao, contrato, projeto, sigad, numero
+ )
+ self.update_orgao(orgao, cnpj_masked)
+ self.novos += 1
+
+ print("\n\nRESUMO DA IMPORTAÇÃO")
+ print("--------------------\n\n")
+ print(
+ "Novos convenios adicionados ao SIGI: {novos}".format(
+ novos=self.novos
+ )
+ )
+ print(
+ "Convênios atualizados..............: {atualizados}".format(
+ atualizados=self.atualizados
+ )
+ )
+ print(
+ "Erros encontrados..................: {erros}".format(
+ erros=self.erros
+ )
+ )
+
+ def setup(self):
+ self.novos = 0
+ self.erros = 0
+ self.atualizados = 0
+
+ self.gescon = config.convenios
+ self.gescon.ultima_importacao = None
+ if self.gescon.checksums is None:
+ self.gescon.checksums = {}
+
+ self.dominio = get_current_site(None).domain
+
+ if self.gescon.palavras == "":
+ print(
+ _("Nenhuma palavra de pesquisa definida - processo abortado."),
+ file=sys.stderr,
+ )
+ return False
+
+ if self.gescon.orgaos_gestores == "":
+ print(
+ _("Nenhum órgão gestor definido - processo abortado"),
+ file=sys.stderr,
+ )
+ return False
+
+ if self.gescon.subespecies == "":
+ print(
+ _("Nenhuma subespécie definida - processo abortado."),
+ file=sys.stderr,
+ )
+ return False
+
+ if "{s}" not in self.gescon.url_gescon:
+ print(
+ _(
+ "Falta a marcação {s} na URL para indicar o local onde "
+ "inserir a sigla da subespécia na consulta ao webservice "
+ "- processo abortado."
+ ),
+ file=sys.stderr,
+ )
+ return False
+
+ self.subespecies = {
+ tuple(s.split("=")) for s in self.gescon.subespecies.split()
+ }
+ self.todos_orgaos = [
+ (o, f"{to_ascii(o.nome)} - {o.uf_sigla}".lower())
+ for o in Orgao.objects.all()
+ .order_by()
+ .annotate(uf_sigla=F("municipio__uf__sigla"))
+ ]
+
+ self.palavras = self.gescon.palavras.splitlines()
+ self.orgaos = self.gescon.orgaos_gestores.splitlines()
+ self.excludentes = self.gescon.palavras_excluir.splitlines()
+
+ return True
+
+ def get_contratos_gescon(self, sigla_gescon):
+ requests.packages.urllib3.disable_warnings()
+
+ url = self.gescon.url_gescon.format(s=sigla_gescon)
+
+ try:
+ response = requests.get(url, verify=False)
+ except Exception as e:
+ print(
+ _("\tErro ao acessar {url}: {message}").format(
+ url=url, message=e.message
+ ),
+ file=sys.stderr,
+ )
+ return None
+
+ if response.status_code != 200:
+ print(
+ _(
+ "\tErro na leitura dos dados de {url}: "
+ "[{status_code}] {reason}"
+ ).format(
+ url=url,
+ status_code=response.status_code,
+ reason=response.reason,
+ ),
+ file=sys.stderr,
+ )
+ return None
+
+ if "application/json" not in response.headers.get("Content-Type", ""):
+ print(
+ _(
+ "\tResultado da consulta à {url} não retornou dados "
+ "em formato json: {content_type}"
+ ).format(
+ url=url,
+ content_type=response.headers.get("Content-Type", ""),
+ ),
+ file=sys.stderr,
+ )
+ return None
+
+ md5sum = md5(response.text.encode(response.encoding)).hexdigest()
+ if (
+ sigla_gescon in self.gescon.checksums
+ and self.gescon.checksums[sigla_gescon] == md5sum
+ ):
+ print(
+ _(
+ "\tDados da subespécie {sigla_gescon} inalterados no "
+ "Gescon. Processamento desnecessário."
+ ).format(sigla_gescon=sigla_gescon)
+ )
+ return None
+ self.gescon.checksums[sigla_gescon] = md5sum
+
+ 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 self.palavras)
+ or any(
+ orgao in c["orgaosGestoresTitulares"]
+ for orgao in self.orgaos
+ if c["orgaosGestoresTitulares"] is not None
+ )
+ )
+ and not any(palavra in c["objeto"] for palavra in self.excludentes)
+ ]
+
+ print(
+ _("\t{qty} contratos encontrados no Gescon").format(qty=len(nossos))
+ )
+
+ return nossos
+
+ def get_convenio(self, contrato, projeto, sigad, numero, cnpj_masked):
+ # Buscar o convenio pelo ID do Gescon
+ try:
+ convenio = Convenio.objects.get(id_gescon=contrato["id"])
+ return convenio
+ except Convenio.DoesNotExist:
+ # Não existe, mas vamos buscar com outras chaves candidatas
+ pass
+
+ # Buscar o Convenio pelo NUP #
+ convenios = Convenio.objects.filter(
+ projeto=projeto, num_processo_sf=sigad
+ )
+ num_convenios = convenios.count()
+ if num_convenios == 0:
+ # Nenhum convênio. Vamos continuar buscando em todos convenios
+ # desse tipo de projeto
+ convenios = Convenio.objects.filter(projeto=projeto)
+ elif num_convenios == 1:
+ # Encontrou um único, então deve ser ele
+ return convenios.get()
+
+ # Se chegou aqui, é porque encontrou vários ou nenhum.
+ # Vamos tentar diferenciar pelas outras chaves candidatas
+
+ convenios = convenios.filter(
+ Q(Q(num_convenio=numero) | Q(num_processo_sf=numero))
+ )
+ num_convenios = convenios.count()
+
+ if num_convenios == 0:
+ # Nenhum encontrado. Já finaliza como não encontrado
+ return None
+ elif num_convenios == 1:
+ # Achou um. Deve ser ele
+ return convenios.get()
+
+ # Encontrou N: Marcamos todos como erro e reportamos
+ urls = []
+ for c in convenios:
+ uri = reverse("admin:convenios_convenio_change", args=[c.id])
+ url = url_normalize(f"{self.dominio}{uri}")
+ urls.append(f'{c.id}')
+ urls = ", ".join(urls)
+ convenios.update(
+ erro_gescon=True,
+ observacao_gescon=_(
+ "Este convênio possui o mesmo número dos convenios {urls}"
+ ).format(urls=urls),
+ )
+ print(
+ _(
+ "\t* O contrato {numero} no Gescon pode "
+ "ser relacionado aos seguintes convênios "
+ "do SIGI: {urls}"
+ ).format(numero=numero, urls=urls),
+ file=sys.stderr,
+ )
+ self.erros += 1
+
+ # 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:
+ # Achou exatamente o único que deveria existir.
+ return convenios.get()
+ # Continua ambíguo. Não dá pra fazer nada.
+ return None
+
+ def get_orgao(self, contrato, cnpj, cnpj_masked, nome, numero, sigad):
+ if (cnpj is None) and (nome is None):
+ print(
+ _(
+ "\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."
+ ).format(numero=numero),
+ file=sys.stderr,
+ )
+ self.erros += 1
+ return None
+ # Vamos tentar primeiro com o CNPJ
+ if cnpj is not None:
+ try:
+ orgao = Orgao.objects.get(cnpj=cnpj_masked)
+ return orgao
+ except Orgao.MultipleObjectsReturned:
+ # 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.
+ orgaos = 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"))
+ ],
+ min_ratio=0,
+ )
+ if orgaos:
+ # Retorna o mais semelhante
+ return orgaos[0][0]
+ except Orgao.DoesNotExist:
+ # Não encontrou nenhum. Vamos seguir sem órgao e tentar
+ # encontrar pelo nome logo abaixo
+ pass
+ # 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
+ print(
+ _(
+ "\t* O contrato {numero} no Gescon com NUP sigad {sigad}, "
+ "fornecedor {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."
+ ).format(numero=numero, sigad=sigad, cnpj_masked=cnpj_masked),
+ file=sys.stderr,
+ )
+ self.erros += 1
+ return None
+ # Tentar primeiro com o nome igual veio do GESCON
+ semelhantes = Orgao.get_semelhantes(
+ to_ascii(contrato["nomeFornecedor"]).lower(),
+ self.todos_orgaos,
+ )
+ if not semelhantes:
+ # Não achou, então vamos tentar com o nome limpado
+ semelhantes = Orgao.get_semelhantes(
+ to_ascii(nome).lower(),
+ self.todos_orgaos,
+ )
+ if len(semelhantes) > 0:
+ # Encontrou algo semelhante.... bora usar.
+ return semelhantes[0][0]
+ # Não encontrou nada parecido. Bora reportar como erro
+ print(
+ _(
+ "\t* O contrato {numero} no Gescon com NUP Sigad {sigad}, "
+ "indica o fornecedor com CNPJ {cnpj_masked} e com o nome "
+ "{nome}, que não tem correspondência no SIGI. Este convênio "
+ "precisa ser cadastrado manualmente no SIGI para este erro "
+ "parar de acontecer."
+ ).format(
+ numero=numero,
+ sigad=sigad,
+ cnpj_masked=cnpj_masked,
+ nome=contrato["nomeFornecedor"],
+ ),
+ file=sys.stderr,
+ )
+ self.erros += 1
+ return None
+
+ def update_convenio(self, convenio, contrato, projeto, sigad, numero):
+ antes = model_to_dict(convenio)
+ 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()
+ convenio.erro_gescon = False
+ convenio.observacao_gescon = ""
+ convenio.id_contrato_gescon = contrato["codTextoContrato"] or ""
+ convenio.id_gescon = contrato["id"]
+ if antes != model_to_dict(convenio):
+ convenio.save()
+ self.atualizados += 1
+
+ def create_convenio(self, orgao, contrato, projeto, sigad, numero):
+ convenio = Convenio(
+ casa_legislativa=orgao,
+ projeto=projeto,
+ num_processo_sf=sigad,
+ num_convenio=numero,
+ data_sigi=timezone.localdate(),
+ data_sigad=contrato["assinatura"],
+ observacao=contrato["objeto"],
+ data_retorno_assinatura=contrato["inicioVigencia"],
+ data_termino_vigencia=contrato["terminoVigencia"],
+ data_pub_diario=contrato["publicacao"],
+ atualizacao_gescon=timezone.localtime(),
+ observacao_gescon=_("Importado integralmente do Gescon"),
+ id_contrato_gescon=(contrato["codTextoContrato"] or ""),
+ id_gescon=contrato["id"],
+ )
+ convenio.save()
+
+ def update_orgao(self, orgao, cnpj_masked):
+ # Corrigir o CNPJ do órgão se estiver diferente do
+ # Gescon. O gescon é um pouquinho mais confiável,
+ # por enquanto.
+ if cnpj_masked and orgao.cnpj != cnpj_masked:
+ orgao.cnpj = cnpj_masked
+ orgao.save()
diff --git a/sigi/apps/convenios/migrations/0041_delete_gescon.py b/sigi/apps/convenios/migrations/0041_delete_gescon.py
new file mode 100644
index 0000000..27777af
--- /dev/null
+++ b/sigi/apps/convenios/migrations/0041_delete_gescon.py
@@ -0,0 +1,19 @@
+# Generated by Django 6.0.5 on 2026-09-22 12:23
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ (
+ "convenios",
+ "0040_alter_convenio_options_convenio_id_gescon_and_more",
+ ),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name="Gescon",
+ ),
+ ]
diff --git a/sigi/apps/convenios/models.py b/sigi/apps/convenios/models.py
index 72adea2..b33478a 100644
--- a/sigi/apps/convenios/models.py
+++ b/sigi/apps/convenios/models.py
@@ -565,602 +565,3 @@ class Tramitacao(models.Model):
if self.observacao:
result = f"{result} ({self.observacao})"
return result
-
-
-class Gescon(models.Model):
- _report_user = False
- _dominio = ""
- _novos = 0
- _erros = 0
- _alertas = 0
- _atualizados = 0
- _todos_orgaos = None
-
- url_gescon = models.URLField(
- _("Webservice Gescon"),
- 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ção {s}.
Por exemplo: "
- "https://adm.senado.gov.br/gestao-contratos/api/contratos"
- "/busca?especie={s}"
- ),
- )
- subespecies = models.TextField(
- _("Subespécies"),
- default="AC=ACT\nPI=PI\nCN=PML\nTA=PML",
- help_text=_(
- "Informe as siglas das subespécies 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ão encontradas serão ignoradas."
- ),
- )
- palavras = models.TextField(
- _("Palavras de filtro"),
- default="ILB\nINTERLEGIS",
- help_text=_(
- "Palavras que devem aparecer no campo OBJETO dos dados do "
- "Gescon para identificar se o contrato pertence ao ILB. "
- "- Informe uma palavra por linha.
"
- "- Ocorrendo qualquer uma das palavras, o contrato será "
- "importado.
"
- ),
- )
- palavras_excluir = models.TextField(
- _("palavras de exclusão"),
- default="DTCOM",
- help_text=_(
- "Palavras que não podem aparecer no campo OBJETO dos dados do "
- "Gescon."
- "- Informe uma palavra por linha.
"
- "- Ocorrendo qualquer uma das palavras, o contrato será "
- "ignorado.
"
- ),
- )
- 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 "
- "importação será enviado."
- ),
- )
- ultima_importacao = models.TextField(
- _("Resultado da última importação"), blank=True
- )
- checksums = models.JSONField(null=True)
-
- class Meta:
- verbose_name = _("Configuração do Gescon")
- verbose_name_plural = _("Configurações do Gescon")
-
- def __str__(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\n"
- if save:
- self.save()
- self.email_report()
-
- def email_report(self):
- if self.email:
- send_mail(
- subject=_("Relatório de importação GESCON"),
- message=self.ultima_importacao,
- recipient_list=self.email,
- fail_silently=True,
- )
- else:
- self.ultima_importacao += _(
- "\n\n*Não foi definida uma caixa de e-mail nas configurações "
- "do Gescon*"
- )
- self.save()
-
- def importa_contratos(self):
- """Importa dados de convênios do GESCON
-
- Returns:
- boolean: Indica se há o que reportar ao usuário (erro ou dados
- importados/atualizados)
- """
-
- self._dominio = get_current_site(None).domain
-
- self.ultima_importacao = ""
- if self.checksums is None:
- self.checksums = {}
- self.add_message(
- _(
- f"Importação iniciada em {timezone.localtime():%d/%m/%Y %H:%M:%S}\n"
- "==========================================================\n"
- )
- )
-
- if self.palavras == "" or self.orgaos_gestores == "":
- self.add_message(
- _(
- "Nenhuma palavra de pesquisa ou orgãos "
- "gestores definidos - processo abortado."
- ),
- True,
- )
- return True
-
- if self.subespecies == "":
- self.add_message(
- _("Nenhuma subespécie definida - processo abortado."), True
- )
- return True
-
- if "{s}" not in self.url_gescon:
- self.add_message(
- _(
- "Falta a marcação {s} na URL para indicar o local onde "
- "inserir a sigla da subespécia na consulta ao webservice "
- "- processo abortado."
- ),
- True,
- )
- return True
-
- subespecies = {tuple(s.split("=")) for s in self.subespecies.split()}
- self._todos_orgaos = [
- (o, f"{to_ascii(o.nome)} - {o.uf_sigla}".lower())
- for o in Orgao.objects.all()
- .order_by()
- .annotate(uf_sigla=F("municipio__uf__sigla"))
- ]
-
- self._report_user = False
-
- for sigla_gescon, sigla_sigi in subespecies:
- self.add_message(
- _("\n**Importando subespécie {sigla_gescon}**").format(
- sigla_gescon=sigla_gescon
- )
- )
- try:
- projeto = Projeto.objects.get(sigla=sigla_sigi)
- except Projeto.DoesNotExist:
- self.add_message(
- _(
- "Projeto com a sigla {sigla_sigi} não existe no SIGI"
- ).format(sigla_sigi=sigla_sigi)
- )
- continue
- except Projeto.MultipleObjectsReturned:
- self.add_message(
- _(
- "A sigla {sigla_sigi} foi cadastrada com duplicidade "
- "no SIGI"
- ).format(sigla_sigi=sigla_sigi)
- )
- continue
-
- nossos = self._get_contratos_gescon(sigla_gescon)
- if not nossos:
- self.add_message(
- _(
- "Nenhum contrato do tipo {sigla_gescon} pertence ao "
- "ILB / Interlegis"
- ).format(sigla_gescon=sigla_gescon)
- )
- continue
-
- # Desmarca erros de importações passadas
- Convenio.objects.filter(projeto=projeto).update(erro_gescon=False)
-
- self._novos = 0
- self._erros = 0
- self._alertas = 0
- self._atualizados = 0
-
- for contrato in nossos:
- if contrato["numero"] is None:
- self.add_message(
- _(
- "\t* O contrato de id {id} subespécie {sub} foi "
- "cadastrado no Gescon sem número e não será "
- "importado."
- ).format(id=contrato["id"], sub=contrato["subEspecie"])
- )
- self._report_user = True
- continue
- numero = re.sub(
- r"(\d{4})(\d{4})", r"\1/\2", contrato["numero"].zfill(8)
- )
- sigad = re.sub(
- r"(\d{5})(\d{6})(\d{4})(\d{2})",
- r"\1.\2/\3-\4",
- contrato["processo"].zfill(17),
- )
-
- if contrato["cnpjCpfFornecedor"]:
- cnpj = contrato["cnpjCpfFornecedor"].zfill(14)
- cnpj_masked = mask_cnpj(cnpj)
- else:
- cnpj = None
- cnpj_masked = None
-
- if contrato["nomeFornecedor"]:
- nome = to_ascii(
- contrato["nomeFornecedor"]
- .replace("VEREADORES DE", "")
- .replace("DO ESTADO", "")
- .split("-")[0]
- .split("/")[0]
- .strip()
- .replace(" ", " ")
- )
- else:
- nome = None
-
- convenio = self._get_convenio(
- contrato, projeto, sigad, numero, cnpj_masked
- )
-
- if convenio:
- # Encontrou um único convênio, basta atualizar seus dados
- self._update_convenio(
- convenio, contrato, projeto, sigad, numero
- )
- self._update_orgao(convenio.casa_legislativa, cnpj_masked)
- self._atualizados += 1
- else:
- # Não encontrou o convênio. Um novo convênio precisa ser
- # criado. Primeiro, é preciso identificar qual órgão
- # consta no contrato do Gescon
-
- orgao = self._get_orgao(
- contrato, cnpj, cnpj_masked, nome, numero, sigad
- )
-
- if orgao is None:
- # Não encontrou o órgão... bora reportar o erro
- 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: \n"
- "\t * CNPJ: {cnpj}\n"
- "\t * Nome: {nome}"
- ).format(
- cnpj=contrato["cnpjCpfFornecedor"],
- nome=contrato["nomeFornecedor"],
- )
- )
- self._erros += 1
- else:
- # Bora criar o convênio
- self._create_convenio(
- orgao, contrato, projeto, sigad, numero
- )
- self._update_orgao(orgao, cnpj_masked)
- self._novos += 1
-
- if self._novos or self._erros or self._alertas or self._atualizados:
- self._report_user = True
-
- self.add_message(
- _(
- "\n\n\t{novos} novos convenios adicionados ao SIGI, "
- "{atualizados} atualizados, sendo {alertas} com alertas, e "
- "{erros} reportados com erro."
- ).format(
- novos=self._novos,
- atualizados=self._atualizados,
- alertas=self._alertas,
- erros=self._erros,
- )
- )
-
- self.save()
- return self._report_user
-
- def _get_contratos_gescon(self, sigla_gescon):
- requests.packages.urllib3.disable_warnings()
-
- url = self.url_gescon.format(s=sigla_gescon)
- palavras = self.palavras.splitlines()
- orgaos = self.orgaos_gestores.splitlines()
- excludentes = self.palavras_excluir.splitlines()
-
- try:
- response = requests.get(url, verify=False)
- except Exception as e:
- self.add_message(_(f"\tErro ao acessar {url}: {e.message}"))
- self._report_user = True
- return None
-
- if response.status_code != 200:
- self.add_message(
- _(
- "\tErro na leitura dos dados de {url}: "
- "[{status_code}] {reason}"
- ).format(
- url=url,
- status_code=response.status_code,
- reason=response.reason,
- )
- )
- self._report_user = True
- return None
-
- if "application/json" not in response.headers.get("Content-Type", ""):
- self.add_message(
- _(
- "\tResultado da consulta à {url} não retornou dados "
- "em formato json: {content_type}"
- ).format(
- url=url,
- content_type=response.headers.get("Content-Type", ""),
- )
- )
- self._report_user = True
- return None
-
- md5sum = md5(response.text.encode(response.encoding)).hexdigest()
- if (
- sigla_gescon in self.checksums
- and self.checksums[sigla_gescon] == md5sum
- ):
- self.add_message(
- _(
- "\tDados da subespécie {sigla_gescon} inalterados no "
- "Gescon. Processamento desnecessário."
- ).format(sigla_gescon=sigla_gescon)
- )
- return None
- self.checksums[sigla_gescon] = md5sum
-
- 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)
- or any(
- orgao in c["orgaosGestoresTitulares"]
- for orgao in orgaos
- if c["orgaosGestoresTitulares"] is not None
- )
- )
- and not any(palavra in c["objeto"] for palavra in excludentes)
- ]
-
- self.add_message(
- _("\t{qty} contratos encontrados no Gescon").format(qty=len(nossos))
- )
-
- return nossos
-
- def _get_convenio(self, contrato, projeto, sigad, numero, cnpj_masked):
- # Buscar o convenio pelo ID do Gescon
- try:
- convenio = Convenio.objects.get(id_gescon=contrato["id"])
- return convenio
- except Convenio.DoesNotExist:
- # Não existe, mas vamos buscar com outras chaves candidatas
- pass
-
- # Buscar o Convenio pelo NUP #
- convenios = Convenio.objects.filter(
- projeto=projeto, num_processo_sf=sigad
- )
- num_convenios = convenios.count()
- if num_convenios == 0:
- # Nenhum convênio. Vamos continuar buscando em todos convenios
- # desse tipo de projeto
- convenios = Convenio.objects.filter(projeto=projeto)
- elif num_convenios == 1:
- # Encontrou um único, então deve ser ele
- return convenios.get()
-
- # Se chegou aqui, é porque encontrou vários ou nenhum.
- # Vamos tentar diferenciar pelas outras chaves candidatas
-
- convenios = convenios.filter(
- Q(Q(num_convenio=numero) | Q(num_processo_sf=numero))
- )
- num_convenios = convenios.count()
-
- if num_convenios == 0:
- # Nenhum encontrado. Já finaliza como não encontrado
- return None
- elif num_convenios == 1:
- # Achou um. Deve ser ele
- return convenios.get()
-
- # Encontrou N: Marcamos todos como erro e reportamos
- urls = []
- for c in convenios:
- uri = reverse("admin:convenios_convenio_change", args=[c.id])
- url = url_normalize(f"{self._dominio}{uri}")
- urls.append(f'{c.id}')
- urls = ", ".join(urls)
- convenios.update(
- erro_gescon=True,
- observacao_gescon=_(
- "Este convênio possui o mesmo número dos convenios {urls}"
- ).format(urls=urls),
- )
- self.add_message(
- _(
- "\t* O contrato {numero} no Gescon pode "
- "ser relacionado aos seguintes convênios "
- "do SIGI: {urls}"
- ).format(numero=numero, urls=urls)
- )
- self._erros += 1
-
- # 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:
- # Achou exatamente o único que deveria existir.
- return convenios.get()
- # Continua ambíguo. Não dá pra fazer nada.
- return None
-
- def _get_orgao(self, contrato, cnpj, cnpj_masked, nome, numero, sigad):
- if (cnpj is None) and (nome is None):
- self.add_message(
- _(
- "\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."
- ).format(numero=numero)
- )
- self._erros += 1
- return None
- # Vamos tentar primeiro com o CNPJ
- if cnpj is not None:
- try:
- orgao = Orgao.objects.get(cnpj=cnpj_masked)
- return orgao
- except Orgao.MultipleObjectsReturned:
- # 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.
- orgaos = 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"))
- ],
- min_ratio=0,
- )
- if orgaos:
- # Retorna o mais semelhante
- return orgaos[0][0]
- except Orgao.DoesNotExist:
- # Não encontrou nenhum. Vamos seguir sem órgao e tentar
- # encontrar pelo nome logo abaixo
- pass
- # 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(
- _(
- "\t* O contrato {numero} no Gescon com NUP sigad {sigad}, "
- "fornecedor {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."
- ).format(numero=numero, sigad=sigad, cnpj_masked=cnpj_masked)
- )
- self._erros += 1
- return None
- # Tentar primeiro com o nome igual veio do GESCON
- semelhantes = Orgao.get_semelhantes(
- to_ascii(contrato["nomeFornecedor"]).lower(),
- self._todos_orgaos,
- )
- if not semelhantes:
- # Não achou, então vamos tentar com o nome limpado
- semelhantes = Orgao.get_semelhantes(
- to_ascii(nome).lower(),
- self._todos_orgaos,
- )
- if len(semelhantes) > 0:
- # Encontrou algo semelhante.... bora usar.
- return semelhantes[0][0]
- # Não encontrou nada parecido. Bora reportar como erro
- self.add_message(
- _(
- "\t* O contrato {numero} no Gescon com NUP Sigad {sigad}, "
- "indica o fornecedor com CNPJ {cnpj_masked} e com o nome "
- "{nome}, que não tem correspondência no SIGI. Este convênio "
- "precisa ser cadastrado manualmente no SIGI para este erro "
- "parar de acontecer."
- ).format(
- numero=numero,
- sigad=sigad,
- cnpj_masked=cnpj_masked,
- nome=contrato["nomeFornecedor"],
- )
- )
- self._erros += 1
- return None
-
- def _update_convenio(self, convenio, contrato, projeto, sigad, numero):
- 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()
- convenio.erro_gescon = False
- convenio.observacao_gescon = ""
- convenio.id_contrato_gescon = contrato["codTextoContrato"] or ""
- convenio.id_gescon = contrato["id"]
- convenio.save()
-
- def _create_convenio(self, orgao, contrato, projeto, sigad, numero):
- convenio = Convenio(
- casa_legislativa=orgao,
- projeto=projeto,
- num_processo_sf=sigad,
- num_convenio=numero,
- data_sigi=timezone.localdate(),
- data_sigad=contrato["assinatura"],
- observacao=contrato["objeto"],
- data_retorno_assinatura=contrato["inicioVigencia"],
- data_termino_vigencia=contrato["terminoVigencia"],
- data_pub_diario=contrato["publicacao"],
- atualizacao_gescon=timezone.localtime(),
- observacao_gescon=_("Importado integralmente do Gescon"),
- id_contrato_gescon=(contrato["codTextoContrato"] or ""),
- id_gescon=contrato["id"],
- )
- convenio.save()
-
- def _update_orgao(self, orgao, cnpj_masked):
- # Corrigir o CNPJ do órgão se estiver diferente do
- # Gescon. O gescon é um pouquinho mais confiável,
- # por enquanto.
- if cnpj_masked and orgao.cnpj != cnpj_masked:
- orgao.cnpj = cnpj_masked
- orgao.save()
-
- @classmethod
- def load(cls):
- obj, created = cls.objects.get_or_create(pk=1)
- return obj
diff --git a/sigi/apps/convenios/templates/convenios/report/erros_gescon_report_view/report_pdf.html b/sigi/apps/convenios/templates/convenios/report/erros_gescon_report_view/report_pdf.html
deleted file mode 100644
index a4b94b0..0000000
--- a/sigi/apps/convenios/templates/convenios/report/erros_gescon_report_view/report_pdf.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "utils/report/report_pdf.html" %}
-{% load i18n %}
-
-{% block main_content %}
- {{ block.super }}
-
-
-
{% translate "Resumo da última importação de dados do Gescon" %}
- {{ ultima_importacao }}
-
-{% endblock %}
\ No newline at end of file
diff --git a/sigi/apps/convenios/urls.py b/sigi/apps/convenios/urls.py
index 4053e08..12f4b2f 100644
--- a/sigi/apps/convenios/urls.py
+++ b/sigi/apps/convenios/urls.py
@@ -17,5 +17,4 @@ urlpatterns = [
views.report_regiao,
name="convenios-report_regiao_pdf",
),
- path("importar/", views.importar_gescon, name="importar-gescon"),
]
diff --git a/sigi/apps/convenios/views.py b/sigi/apps/convenios/views.py
index beef45e..0da8492 100644
--- a/sigi/apps/convenios/views.py
+++ b/sigi/apps/convenios/views.py
@@ -20,7 +20,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django_weasyprint.views import WeasyTemplateResponse
from sigi.apps.casas.models import Orgao
from sigi.apps.contatos.models import UnidadeFederativa
-from sigi.apps.convenios.models import Convenio, Gescon, Projeto
+from sigi.apps.convenios.models import Convenio, Projeto
from sigi.apps.utils import get_sigad_url
from sigi.apps.utils.views import ReportListView
@@ -69,20 +69,6 @@ class ErrosGesconReportView(
count,
)
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- rst = Gescon.load().ultima_importacao
- parts = publish_parts(
- rst,
- writer_name="html5",
- settings_overrides={
- "input_encoding": "unicode",
- "output_encoding": "unicode",
- },
- )
- context["ultima_importacao"] = mark_safe(parts["html_body"])
- return context
-
@login_required
@staff_member_required
@@ -297,23 +283,6 @@ def casas_estado_to_tabela(casas, convenios, regiao):
}
-@login_required
-@staff_member_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()
-
- context = {"gescon": gescon}
-
- return render(request, "convenios/importar_gescon.html", context)
-
-
def normaliza_data(get, nome_param):
if nome_param in get:
value = get.get(nome_param, "")
diff --git a/sigi/apps/eventos/appconfig.py b/sigi/apps/eventos/appconfig.py
new file mode 100644
index 0000000..c398d26
--- /dev/null
+++ b/sigi/apps/eventos/appconfig.py
@@ -0,0 +1,15 @@
+from django import forms
+from django.utils.translation import gettext_lazy as _
+
+
+class AppConfigForm(forms.Form):
+ encerra_inscricao = forms.IntegerField(
+ min_value=0,
+ required=True,
+ label=_("Encerra inscrições de oficinas no Portal"),
+ initial=30,
+ help_text=_(
+ "Número de dias a esperar após o término da oficina antes de "
+ "a remover do Portal Interlegis"
+ ),
+ )
diff --git a/sigi/apps/eventos/jobs/daily/encerra_inscricao.py b/sigi/apps/eventos/jobs/daily/encerra_inscricao.py
index a4c00d9..90b26a2 100644
--- a/sigi/apps/eventos/jobs/daily/encerra_inscricao.py
+++ b/sigi/apps/eventos/jobs/daily/encerra_inscricao.py
@@ -1,7 +1,7 @@
+from appconfig import config
from django_extensions.management.jobs import DailyJob
from django.utils import timezone
from django.utils.translation import gettext as _
-from sigi.apps.utils.models import Config
from sigi.apps.eventos.models import Evento
INSCRICOES_ENCERRADAS = _("INSCRIÇÕES ENCERRADAS")
@@ -14,10 +14,11 @@ class Job(DailyJob):
report_data = []
def execute(self):
- dias_a_retroagir = int(Config.get_param("ENCERRA_INSCRICAO")[0])
self.report_data = []
hoje = timezone.localtime().replace(hour=23, minute=59, second=59)
- retroagir = hoje - timezone.timedelta(days=dias_a_retroagir)
+ retroagir = hoje - timezone.timedelta(
+ days=config.eventos.encerra_inscricao
+ )
total_encerrar = 0
total_despublicar = 0
diff --git a/sigi/apps/utils/admin.py b/sigi/apps/utils/admin.py
index bd57de4..524527d 100644
--- a/sigi/apps/utils/admin.py
+++ b/sigi/apps/utils/admin.py
@@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
from django_extensions.management.jobs import get_job, get_jobs
from tinymce.models import HTMLField
from tinymce.widgets import AdminTinyMCE
-from sigi.apps.utils.models import SigiAlert, AlertViews, Config
+from sigi.apps.utils.models import SigiAlert, AlertViews
class AlertViewsInline(admin.TabularInline):
@@ -26,9 +26,3 @@ class SigiAlertAdmin(admin.ModelAdmin):
formfield_overrides = {HTMLField: {"widget": AdminTinyMCE}}
list_filter = ("destinatarios",)
inlines = [AlertViewsInline]
-
-
-@admin.register(Config)
-class ConfigAdmin(admin.ModelAdmin):
- list_display = ["parametro", "valor"]
- list_filter = ["parametro"]
diff --git a/sigi/apps/utils/admin_urls.py b/sigi/apps/utils/admin_urls.py
deleted file mode 100644
index aef93f6..0000000
--- a/sigi/apps/utils/admin_urls.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from django.urls import path, include
-from sigi.apps.utils import views
-
-urlpatterns = [
- path(
- "runjob//",
- views.user_run_job,
- name="utils_runjob",
- ),
-]
diff --git a/sigi/apps/utils/management/jobs.py b/sigi/apps/utils/management/jobs.py
index 602e621..8160837 100644
--- a/sigi/apps/utils/management/jobs.py
+++ b/sigi/apps/utils/management/jobs.py
@@ -7,11 +7,6 @@ from django.contrib.contenttypes.models import ContentType
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django_extensions.management.jobs import BaseJob
-from sigi.apps.utils.models import Config
-
-
-class MisconfiguredError(Exception):
- pass
class QuarterDailyJob(BaseJob):
diff --git a/sigi/apps/utils/migrations/0009_delete_config.py b/sigi/apps/utils/migrations/0009_delete_config.py
new file mode 100644
index 0000000..420cab0
--- /dev/null
+++ b/sigi/apps/utils/migrations/0009_delete_config.py
@@ -0,0 +1,16 @@
+# Generated by Django 6.0.5 on 2026-09-22 11:22
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("utils", "0008_sigialert_repeticao_alertviews"),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name="Config",
+ ),
+ ]
diff --git a/sigi/apps/utils/models.py b/sigi/apps/utils/models.py
index ed2a6b5..fae9ad3 100644
--- a/sigi/apps/utils/models.py
+++ b/sigi/apps/utils/models.py
@@ -71,44 +71,3 @@ class AlertViews(models.Model):
alert=str(self.alert),
visualizacoes=self.visualizacoes,
)
-
-
-class Config(models.Model):
- PARAMETRO_CHOICES = (
- ("ENCERRA_INSCRICAO", _("Encerra inscrições de oficinas no Portal")),
- ("EMAIL_JOBS", _("E-mail de jobs")),
- )
- DEFAULTS = {
- "ENCERRA_INSCRICAO": "30",
- "EMAIL_JOBS": "sigi@interlegis.leg.br",
- }
- parametro = models.CharField(
- _("parâmetro"), max_length=100, choices=PARAMETRO_CHOICES
- )
- valor = models.CharField(_("valor do parâmettro"), max_length=200)
-
- class Meta:
- ordering = ("parametro",)
- verbose_name = _("Parâmetro de configuração")
- verbose_name_plural = _("Parâmetros de configuração")
-
- def __str__(self):
- return f"{self.get_parametro_display()}: {self.valor}"
-
- @classmethod
- def get_param(cls, parametro):
- if parametro not in cls.DEFAULTS:
- raise cls.DoesNotExist(
- _(
- f"Não existe o parâmetro '{parametro}'. "
- f"As opções são {', '.join(cls.DEFAULTS.keys())}."
- )
- )
- valores = list(
- cls.objects.filter(parametro=parametro).values_list(
- "valor", flat=True
- )
- )
- if not valores:
- valores.append(cls.DEFAULTS[parametro])
- return valores
diff --git a/sigi/apps/utils/templates3/admin/jobs/job_result.html b/sigi/apps/utils/templates3/admin/jobs/job_result.html
deleted file mode 100644
index cfabf66..0000000
--- a/sigi/apps/utils/templates3/admin/jobs/job_result.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% load static i18n %}
-
-{% block extrastyle %}
-{{ block.super }}
-
-{% endblock %}
-{% block content %}{{ content|safe }}{% endblock %}
\ No newline at end of file
diff --git a/sigi/apps/utils/views.py b/sigi/apps/utils/views.py
index 460dd98..4d15f6b 100644
--- a/sigi/apps/utils/views.py
+++ b/sigi/apps/utils/views.py
@@ -154,57 +154,3 @@ class ReportListView(ListView):
def _is_csv(self):
return self.request.GET.get(self.format_param_name, "html") == "csv"
-
-
-@login_required
-@user_passes_test(lambda user: user.is_superuser)
-def user_run_job(request, job_name):
- job = get_job(None, job_name)()
- start_time = timezone.localtime()
- if hasattr(job, "do_job"):
- job.do_job()
- end_time = timezone.localtime()
- rst, html = job.prepare_report(start_time, end_time)
- else:
- with io.StringIO() as so_buf, io.StringIO() as se_buf, redirect_stdout(
- so_buf
- ), redirect_stderr(se_buf):
- job.execute()
- messages = so_buf.getvalue()
- errors = se_buf.getvalue()
- report_data = ["", "MENSAGENS", "---------", ""]
- if messages:
- report_data.extend(messages.splitlines())
- else:
- report_data.extend(["Nenhuma mensagem gerada", ""])
- report_data.extend(["", "ERROS", "-----", ""])
- if errors:
- report_data.extend(errors.splitlines())
- else:
- report_data.extend(["Nenhum erro gerado", ""])
- end_time = timezone.localtime()
- rst = render_to_string(
- "emails/base_report.rst",
- {
- "title": job.help,
- "start_time": start_time,
- "end_time": end_time,
- "report_data": report_data,
- },
- )
- html = docutils.core.publish_string(
- rst,
- writer_name="html5",
- settings_overrides={
- "input_encoding": "unicode",
- "output_encoding": "unicode",
- },
- )
-
- dp = Selector(text=html)
-
- return render(
- request,
- "admin/jobs/job_result.html",
- {"content": dp.xpath("//body/*").get()},
- )
diff --git a/sigi/menu_conf.yaml b/sigi/menu_conf.yaml
index 4318f17..4830db6 100644
--- a/sigi/menu_conf.yaml
+++ b/sigi/menu_conf.yaml
@@ -6,25 +6,17 @@ admin_menu:
view_name: admin:auth_user_changelist
- title: Grupos
view_name: admin:auth_group_changelist
- - title: Importar Gescon
- view_name: importar-gescon
- title: Todos os senadores
view_name: admin:parlamentares_senador_changelist
- title: Avisos do sistema
view_name: admin:utils_sigialert_changelist
- - title: Sincroniza eventos com Saberes
- view_name: utils_runjob
- view_param: sincroniza_saberes
- - title: Encerra inscrições de eventos
- view_name: utils_runjob
- view_param: encerra_inscricao
- title: Jobs de cron
view_name: admin:job_controller_cronjob_changelist
- title: Jobs agendados
view_name: admin:job_controller_jobschedule_changelist
querystr: status__exact=A
- title: Configurações
- view_name: admin:utils_config_changelist
+ view_name: appconfig:appconfiglist
main_menu:
- title: Relatórios
icon: pdf
diff --git a/sigi/settings.py b/sigi/settings.py
index 6dd3533..e6116f0 100644
--- a/sigi/settings.py
+++ b/sigi/settings.py
@@ -65,6 +65,7 @@ INSTALLED_APPS = [
"tinymce",
"rest_framework",
"drf_spectacular",
+ "appconfig",
"sigi.apps.casas",
"sigi.apps.contatos",
"sigi.apps.convenios",
diff --git a/sigi/urls.py b/sigi/urls.py
index 6d62dca..beccc53 100644
--- a/sigi/urls.py
+++ b/sigi/urls.py
@@ -40,7 +40,7 @@ urlpatterns = [
path("admin/eventos/", include("sigi.apps.eventos.admin_urls")),
path("admin/convenios/", include("sigi.apps.convenios.urls")),
path("admin/ocorrencias/", include("sigi.apps.ocorrencias.admin_urls")),
- path("admin/utils/", include("sigi.apps.utils.admin_urls")),
+ path("admin/", include("appconfig.urls")),
path("admin/", admin.site.urls),
path("dash/", dashboard.urls),
path(