From f6d71249cfe7a930b7c9e9d0861e50c76c4c8f0a Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Tue, 27 Jan 2026 15:22:14 -0300 Subject: [PATCH 1/9] =?UTF-8?q?refact:=20cria=20m=C3=A9todo=20get=5Fproxim?= =?UTF-8?q?o=5Fnumero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/materia/forms.py | 45 ++++------------------------ sapl/materia/models.py | 68 ++++++++++++++++++++++++++++++++++++++++++ sapl/materia/views.py | 48 ++++------------------------- 3 files changed, 78 insertions(+), 83 deletions(-) diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index c685109ae..9c39c58df 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -2458,47 +2458,12 @@ class ConfirmarProposicaoForm(ProposicaoForm): if self.instance.tipo.content_type.model_class( ) == TipoMateriaLegislativa: - numeracao = None - try: - self.logger.debug( - "Tentando obter modelo de sequência de numeração.") - numeracao = BaseAppConfig.objects.last( - ).sequencia_numeracao_protocolo - except AttributeError as e: - self.logger.error("Erro ao obter modelo. " + str(e)) - pass - tipo = self.instance.tipo.tipo_conteudo_related - if tipo.sequencia_numeracao: - numeracao = tipo.sequencia_numeracao - ano = timezone.now().year - if numeracao == 'A': - numero = MateriaLegislativa.objects.filter( - ano=ano, tipo=tipo).aggregate(Max('numero')) - elif numeracao == 'L': - legislatura = Legislatura.objects.filter( - data_inicio__year__lte=ano, - data_fim__year__gte=ano).first() - data_inicio = legislatura.data_inicio - data_fim = legislatura.data_fim - numero = MateriaLegislativa.objects.filter( - data_apresentacao__gte=data_inicio, - data_apresentacao__lte=data_fim, - tipo=tipo).aggregate( - Max('numero')) - elif numeracao == 'U': - numero = MateriaLegislativa.objects.filter( - tipo=tipo).aggregate(Max('numero')) - if numeracao is None: - numero['numero__max'] = 0 - - if cd['numero_materia_futuro'] and not MateriaLegislativa.objects.filter(tipo=tipo, - ano=ano, - numero=cd['numero_materia_futuro']): - max_numero = cd['numero_materia_futuro'] - else: - max_numero = numero['numero__max'] + \ - 1 if numero['numero__max'] else 1 + max_numero, ano = MateriaLegislativa.get_proximo_numero( + tipo=tipo, + ano=None, + numero_preferido=cd.get('numero_materia_futuro', None) + ) # dados básicos materia = MateriaLegislativa() diff --git a/sapl/materia/models.py b/sapl/materia/models.py index da2e0ac91..6c4c31cbf 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -382,6 +382,74 @@ class MateriaLegislativa(models.Model): using=using, update_fields=update_fields) + @staticmethod + def get_proximo_numero(tipo, ano=None, numero_preferido=None): + """ + Retorna o próximo número disponível para uma MateriaLegislativa + baseado no tipo e nas configurações de numeração. + + Args: + tipo: TipoMateriaLegislativa - o tipo da matéria + ano: int - o ano da matéria (default: ano atual) + numero_preferido: int - número preferido/desejado (opcional) + + Returns: + int: O próximo número disponível para a matéria + """ + from django.db.models import Max + from sapl.parlamentares.models import Legislatura + import sapl.base.models + + if ano is None: + ano = timezone.now().year + + # Obtém a configuração de numeração + numeracao = None + try: + numeracao = sapl.base.models.AppConfig.objects.last( + ).sequencia_numeracao_protocolo + except AttributeError: + pass + + # O tipo pode sobrescrever a configuração global + if tipo.sequencia_numeracao: + numeracao = tipo.sequencia_numeracao + + # Calcula o próximo número baseado no tipo de numeração + if numeracao == 'A': # Por ano + numero = MateriaLegislativa.objects.filter( + ano=ano, tipo=tipo).aggregate(Max('numero')) + elif numeracao == 'L': # Por legislatura + legislatura = Legislatura.objects.filter( + data_inicio__year__lte=ano, + data_fim__year__gte=ano).first() + if legislatura: + data_inicio = legislatura.data_inicio + data_fim = legislatura.data_fim + numero = MateriaLegislativa.objects.filter( + data_apresentacao__gte=data_inicio, + data_apresentacao__lte=data_fim, + tipo=tipo).aggregate(Max('numero')) + else: + numero = {'numero__max': 0} + elif numeracao == 'U': # Único/Universal + numero = MateriaLegislativa.objects.filter( + tipo=tipo).aggregate(Max('numero')) + else: + numero = {'numero__max': 0} + + # Verifica se o número preferido está disponível + if numero_preferido and not MateriaLegislativa.objects.filter( + tipo=tipo, + ano=ano, + numero=numero_preferido).exists(): + return int(numero_preferido) + + # Retorna o próximo número sequencial + max_numero = numero['numero__max'] + return ((max_numero + 1) if max_numero else 1), ano + + class Autoria(models.Model): autor = models.ForeignKey(Autor, diff --git a/sapl/materia/views.py b/sapl/materia/views.py index fd05d5e2a..e256bd43b 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -340,53 +340,15 @@ class ProposicaoTaView(IntegracaoTaView): @permission_required('materia.detail_materialegislativa') def recuperar_materia(request): - logger = logging.getLogger(__name__) - username = request.user.username tipo = TipoMateriaLegislativa.objects.get(pk=request.GET['tipo']) - ano = request.GET.get('ano', '') - - if not (tipo and ano): - return JsonResponse({'numero': '', 'ano': ''}) - - numeracao = None - try: - logger.debug("user=" + username + - ". Tentando obter numeração da matéria.") - numeracao = sapl.base.models.AppConfig.objects.last( - ).sequencia_numeracao_protocolo - except AttributeError as e: - logger.error("user=" + username + ". " + str(e) + - " Numeracao da matéria definida como None.") - pass + ano = request.GET.get('ano', None) - if tipo.sequencia_numeracao: - numeracao = tipo.sequencia_numeracao - - if numeracao == 'A': - numero = MateriaLegislativa.objects.filter( - ano=ano, tipo=tipo).aggregate(Max('numero')) - elif numeracao == 'L': - legislatura = Legislatura.objects.filter( - data_inicio__year__lte=ano, - data_fim__year__gte=ano).first() - data_inicio = legislatura.data_inicio - data_fim = legislatura.data_fim - numero = MateriaLegislativa.objects.filter( - data_apresentacao__gte=data_inicio, - data_apresentacao__lte=data_fim, - tipo=tipo).aggregate( - Max('numero')) - elif numeracao == 'U': - numero = MateriaLegislativa.objects.filter( - tipo=tipo).aggregate(Max('numero')) - - if numeracao is None: - numero['numero__max'] = 0 - - max_numero = numero['numero__max'] + 1 if numero['numero__max'] else 1 + max_numero, ano = MateriaLegislativa.get_proximo_numero( + tipo=tipo, + ano=int(ano) if ano else None + ) response = JsonResponse({'numero': max_numero, 'ano': ano}) - return response From bed41c435a6cea82ee1a5160b353b5b49b723a31 Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Tue, 27 Jan 2026 16:38:02 -0300 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20impl=20numera=C3=A7=C3=A3o=20autom?= =?UTF-8?q?=C3=A1tica=20em=20cadastros=20via=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/api/serializers.py | 1 + sapl/api/views_materia.py | 26 ++++++++++++++++++++++++++ sapl/materia/models.py | 5 ++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index b8bb06b4d..15416180f 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -11,6 +11,7 @@ from rest_framework import serializers from rest_framework.fields import SerializerMethodField from sapl.base.models import Autor, CasaLegislativa, Metadata +from sapl.materia.models import MateriaLegislativa from sapl.parlamentares.models import Parlamentar, Mandato, Legislatura from sapl.sessao.models import OrdemDia, SessaoPlenaria diff --git a/sapl/api/views_materia.py b/sapl/api/views_materia.py index 1810aea2e..5badd1cd4 100644 --- a/sapl/api/views_materia.py +++ b/sapl/api/views_materia.py @@ -1,7 +1,9 @@ +from copy import deepcopy from django.apps.registry import apps from django.db.models import Q from rest_framework.decorators import action +from rest_framework.status import HTTP_201_CREATED from rest_framework.response import Response from drfautoapi.drfautoapi import ApiViewSetConstrutor, \ @@ -90,6 +92,30 @@ class _MateriaLegislativaViewSet: class Meta: ordering = ['-ano', 'tipo', 'numero'] + def create(self, request, *args, **kwargs): + data = deepcopy(request.data) + tipo = data.get('tipo', None) + numero = data.get('numero', None) + ano = data.get('ano', None) + + if tipo: + numero, ano = MateriaLegislativa.get_proximo_numero( + tipo=tipo, + ano=ano, + numero_preferido=numero + ) + data['numero'] = numero + data['ano'] = ano + + serializer = self.get_serializer(data=data) + + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) + + + @action(detail=True, methods=['GET']) def ultima_tramitacao(self, request, *args, **kwargs): diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 6c4c31cbf..8c8d2db64 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -411,6 +411,9 @@ class MateriaLegislativa(models.Model): except AttributeError: pass + if not isinstance(tipo, TipoMateriaLegislativa): + tipo = TipoMateriaLegislativa.objects.get(pk=tipo) + # O tipo pode sobrescrever a configuração global if tipo.sequencia_numeracao: numeracao = tipo.sequencia_numeracao @@ -443,7 +446,7 @@ class MateriaLegislativa(models.Model): tipo=tipo, ano=ano, numero=numero_preferido).exists(): - return int(numero_preferido) + return int(numero_preferido), ano # Retorna o próximo número sequencial max_numero = numero['numero__max'] From 467cf5c1012c06c7e064d0f61e5710daecbcaf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LeandroJata=C3=AD?= Date: Tue, 27 Jan 2026 17:02:15 -0300 Subject: [PATCH 3/9] Update sapl/materia/models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sapl/materia/models.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 8c8d2db64..5e13f4ca7 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -441,12 +441,20 @@ class MateriaLegislativa(models.Model): else: numero = {'numero__max': 0} + # Converte o número preferido para inteiro, se possível + numero_preferido_int = None + if numero_preferido: + try: + numero_preferido_int = int(numero_preferido) + except (TypeError, ValueError): + numero_preferido_int = None + # Verifica se o número preferido está disponível - if numero_preferido and not MateriaLegislativa.objects.filter( + if numero_preferido_int is not None and not MateriaLegislativa.objects.filter( tipo=tipo, ano=ano, - numero=numero_preferido).exists(): - return int(numero_preferido), ano + numero=numero_preferido_int).exists(): + return numero_preferido_int, ano # Retorna o próximo número sequencial max_numero = numero['numero__max'] From 49accd656ac33b430b72eab1a5cf1515ade62bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LeandroJata=C3=AD?= Date: Tue, 27 Jan 2026 17:03:23 -0300 Subject: [PATCH 4/9] Update sapl/api/serializers.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sapl/api/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index 15416180f..b8bb06b4d 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -11,7 +11,6 @@ from rest_framework import serializers from rest_framework.fields import SerializerMethodField from sapl.base.models import Autor, CasaLegislativa, Metadata -from sapl.materia.models import MateriaLegislativa from sapl.parlamentares.models import Parlamentar, Mandato, Legislatura from sapl.sessao.models import OrdemDia, SessaoPlenaria From 85bc23f42b05d857ed6469967356620859f10c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LeandroJata=C3=AD?= Date: Tue, 27 Jan 2026 17:05:06 -0300 Subject: [PATCH 5/9] Update sapl/api/views_materia.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sapl/api/views_materia.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sapl/api/views_materia.py b/sapl/api/views_materia.py index 5badd1cd4..836cc8491 100644 --- a/sapl/api/views_materia.py +++ b/sapl/api/views_materia.py @@ -104,8 +104,8 @@ class _MateriaLegislativaViewSet: ano=ano, numero_preferido=numero ) - data['numero'] = numero - data['ano'] = ano + data['numero'] = numero + data['ano'] = ano serializer = self.get_serializer(data=data) From 7120a3bb812532878e2e713e166686a0ca632aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LeandroJata=C3=AD?= Date: Tue, 27 Jan 2026 17:07:48 -0300 Subject: [PATCH 6/9] Update sapl/materia/models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sapl/materia/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 5e13f4ca7..288968191 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -412,7 +412,13 @@ class MateriaLegislativa(models.Model): pass if not isinstance(tipo, TipoMateriaLegislativa): - tipo = TipoMateriaLegislativa.objects.get(pk=tipo) + try: + tipo = TipoMateriaLegislativa.objects.get(pk=tipo) + except TipoMateriaLegislativa.DoesNotExist: + # Fornece uma mensagem mais informativa quando o tipo não é encontrado + raise TipoMateriaLegislativa.DoesNotExist( + _("TipoMateriaLegislativa with pk '%s' does not exist.") % tipo + ) # O tipo pode sobrescrever a configuração global if tipo.sequencia_numeracao: From ae93d269b99d61002fb722b8cb4eb28001fbaf0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LeandroJata=C3=AD?= Date: Tue, 27 Jan 2026 17:08:58 -0300 Subject: [PATCH 7/9] Update sapl/materia/models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sapl/materia/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 288968191..63905cb95 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -394,7 +394,7 @@ class MateriaLegislativa(models.Model): numero_preferido: int - número preferido/desejado (opcional) Returns: - int: O próximo número disponível para a matéria + tuple[int, int]: Uma tupla contendo (numero, ano) da matéria. """ from django.db.models import Max from sapl.parlamentares.models import Legislatura From 0cc097f4d147cd1628b2e726eeaa83979801a52e Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Tue, 27 Jan 2026 17:14:16 -0300 Subject: [PATCH 8/9] Add transaction atomic no endpoint create --- sapl/api/views_materia.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sapl/api/views_materia.py b/sapl/api/views_materia.py index 836cc8491..805453ed7 100644 --- a/sapl/api/views_materia.py +++ b/sapl/api/views_materia.py @@ -1,6 +1,7 @@ from copy import deepcopy from django.apps.registry import apps +from django.db import transaction from django.db.models import Q from rest_framework.decorators import action from rest_framework.status import HTTP_201_CREATED @@ -92,6 +93,7 @@ class _MateriaLegislativaViewSet: class Meta: ordering = ['-ano', 'tipo', 'numero'] + @transaction.atomic def create(self, request, *args, **kwargs): data = deepcopy(request.data) tipo = data.get('tipo', None) From 4c7df42bd0218eed90e1cdec0bdc6307ae684ed9 Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Tue, 27 Jan 2026 17:28:30 -0300 Subject: [PATCH 9/9] =?UTF-8?q?add=20valida=C3=A7=C3=A3o=20de=20tipo=20se?= =?UTF-8?q?=20tipo=20n=C3=A3o=20=C3=A9=20objeto=20do=20model=20TipoMateria?= =?UTF-8?q?Legislativa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/materia/models.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 63905cb95..efbe1a186 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -3,6 +3,7 @@ from datetime import datetime from django.contrib.auth.models import Group from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError from django.db import models from django.db.models.functions import Concat from django.template import defaultfilters @@ -412,14 +413,21 @@ class MateriaLegislativa(models.Model): pass if not isinstance(tipo, TipoMateriaLegislativa): + if tipo is None: + raise ValidationError(_("O tipo é obrigatório.")) + + try: + tipo_id = int(tipo) + except (ValueError, TypeError): + raise ValidationError(_("Tipo inválido: '%s'") % tipo) + try: - tipo = TipoMateriaLegislativa.objects.get(pk=tipo) + tipo = TipoMateriaLegislativa.objects.get(pk=tipo_id) except TipoMateriaLegislativa.DoesNotExist: - # Fornece uma mensagem mais informativa quando o tipo não é encontrado raise TipoMateriaLegislativa.DoesNotExist( - _("TipoMateriaLegislativa with pk '%s' does not exist.") % tipo + _("TipoMateriaLegislativa with pk '%s' does not exist.") % tipo_id ) - + # O tipo pode sobrescrever a configuração global if tipo.sequencia_numeracao: numeracao = tipo.sequencia_numeracao