From 61f1a8b2f607d544dc01f6f985f13368fc0b4a11 Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Tue, 27 Jan 2026 15:22:14 -0300 Subject: [PATCH 01/16] =?UTF-8?q?refact:=20cria=20m=C3=A9todo=20get=5Fprox?= =?UTF-8?q?imo=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 6dc499145d5446030dc86da81437031d0ebedb62 Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Tue, 27 Jan 2026 16:38:02 -0300 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20impl=20numera=C3=A7=C3=A3o=20auto?= =?UTF-8?q?m=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 a85a83e4d50896ad23907863721ac9daa5ea055b 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 03/16] 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 e9a9ab569ee5bbd1f15cf506709ef0f582325747 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 04/16] 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 43e6850dca0e959fc3424172b415117821fa8aff 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 05/16] 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 51a2ccd3294f3f60aa0c5266a9227c10a855cb81 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 06/16] 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 346e833f7dca16a1a2745ad7dc4876251f29b4b9 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 07/16] 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 e9da827ed3c963f840e01d4a7be4e5e2d51c0ed0 Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Tue, 27 Jan 2026 17:14:16 -0300 Subject: [PATCH 08/16] 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 a9f66a99d063e6b3bad947e9727b28d88bc5f272 Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Tue, 27 Jan 2026 17:28:30 -0300 Subject: [PATCH 09/16] =?UTF-8?q?add=20valida=C3=A7=C3=A3o=20de=20tipo=20s?= =?UTF-8?q?e=20tipo=20n=C3=A3o=20=C3=A9=20objeto=20do=20model=20TipoMateri?= =?UTF-8?q?aLegislativa?= 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 From 9d3687981bacbe385dfeee2c7457d9898c9b59f6 Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Mon, 16 Mar 2026 08:32:21 -0300 Subject: [PATCH 10/16] =?UTF-8?q?refact:=20aplica=20solicita=C3=A7=C3=B5es?= =?UTF-8?q?=20de=20reviewer=20e=20cria=20testes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/api/views_materia.py | 62 ++- sapl/materia/models.py | 28 +- .../tests/test_materia_proximo_numero.py | 385 ++++++++++++++++++ 3 files changed, 451 insertions(+), 24 deletions(-) create mode 100644 sapl/materia/tests/test_materia_proximo_numero.py diff --git a/sapl/api/views_materia.py b/sapl/api/views_materia.py index 805453ed7..c8bc24422 100644 --- a/sapl/api/views_materia.py +++ b/sapl/api/views_materia.py @@ -1,10 +1,9 @@ -from copy import deepcopy from django.apps.registry import apps -from django.db import transaction +from django.db import IntegrityError, transaction from django.db.models import Q from rest_framework.decorators import action -from rest_framework.status import HTTP_201_CREATED +from rest_framework.status import HTTP_201_CREATED, HTTP_409_CONFLICT from rest_framework.response import Response from drfautoapi.drfautoapi import ApiViewSetConstrutor, \ @@ -93,31 +92,62 @@ class _MateriaLegislativaViewSet: class Meta: ordering = ['-ano', 'tipo', 'numero'] + _MAX_RETRIES_NUMERO = 3 + @transaction.atomic def create(self, request, *args, **kwargs): - data = deepcopy(request.data) + data = dict(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 - + if tipo and not numero: + # Número não fornecido pelo cliente: auto-gerar próximo disponível. + # select_for_update() em get_proximo_numero previne race conditions. + # Retry como camada extra de segurança contra IntegrityError residual. + for tentativa in range(self._MAX_RETRIES_NUMERO): + try: + with transaction.atomic(): + numero_gerado, ano_gerado = MateriaLegislativa.get_proximo_numero( + tipo=tipo, ano=ano + ) + data['numero'] = numero_gerado + data['ano'] = ano_gerado + + 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) + except IntegrityError: + if tentativa == self._MAX_RETRIES_NUMERO - 1: + return Response( + {'detail': 'Não foi possível gerar um número único após ' + '%d tentativas. Tente novamente.' % self._MAX_RETRIES_NUMERO}, + status=HTTP_409_CONFLICT + ) + continue + + # Número fornecido pelo cliente (ou tipo ausente): + # respeitar os dados enviados. Se houver conflito de unicidade, + # retornar erro explícito em vez de sobrescrever silenciosamente. serializer = self.get_serializer(data=data) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) + try: + with transaction.atomic(): + self.perform_create(serializer) + except IntegrityError: + return Response( + {'numero': [ + 'O número %s já está em uso para este tipo/ano. ' + 'Remova o campo "numero" para auto-gerar o próximo disponível.' % numero + ]}, + status=HTTP_409_CONFLICT + ) 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 efbe1a186..641a8e259 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -5,14 +5,17 @@ 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 import Max from django.db.models.functions import Concat from django.template import defaultfilters from django.utils import formats, timezone from django.utils.translation import ugettext_lazy as _ from model_utils import Choices -from sapl.base.models import SEQUENCIA_NUMERACAO_PROTOCOLO, Autor + +from sapl.base.models import SEQUENCIA_NUMERACAO_PROTOCOLO, Autor, AppConfig as BaseAppConfig from sapl.comissoes.models import Comissao, Reuniao +from sapl.parlamentares.models import Legislatura from sapl.compilacao.models import (PerfilEstruturalTextoArticulado, TextoArticulado) from sapl.parlamentares.models import Parlamentar @@ -389,17 +392,21 @@ class MateriaLegislativa(models.Model): Retorna o próximo número disponível para uma MateriaLegislativa baseado no tipo e nas configurações de numeração. + IMPORTANTE: Este método utiliza select_for_update() e DEVE ser + chamado dentro de uma transação (transaction.atomic) para garantir + proteção contra race conditions em acessos concorrentes. + Args: - tipo: TipoMateriaLegislativa - o tipo da matéria + tipo: TipoMateriaLegislativa ou int/str - o tipo da matéria ano: int - o ano da matéria (default: ano atual) - numero_preferido: int - número preferido/desejado (opcional) + numero_preferido: int - número preferido/desejado (opcional). + Se fornecido e disponível, será retornado. Caso contrário, + retorna o próximo sequencial. Returns: tuple[int, int]: Uma tupla contendo (numero, ano) da 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 @@ -407,7 +414,7 @@ class MateriaLegislativa(models.Model): # Obtém a configuração de numeração numeracao = None try: - numeracao = sapl.base.models.AppConfig.objects.last( + numeracao = BaseAppConfig.objects.last( ).sequencia_numeracao_protocolo except AttributeError: pass @@ -427,7 +434,12 @@ class MateriaLegislativa(models.Model): raise TipoMateriaLegislativa.DoesNotExist( _("TipoMateriaLegislativa with pk '%s' does not exist.") % tipo_id ) - + + # Lock na linha do TipoMateriaLegislativa para serializar + # gerações concorrentes de número do mesmo tipo. + # Requer que o chamador esteja dentro de transaction.atomic(). + TipoMateriaLegislativa.objects.select_for_update().get(pk=tipo.pk) + # O tipo pode sobrescrever a configuração global if tipo.sequencia_numeracao: numeracao = tipo.sequencia_numeracao diff --git a/sapl/materia/tests/test_materia_proximo_numero.py b/sapl/materia/tests/test_materia_proximo_numero.py new file mode 100644 index 000000000..d1d08f0a1 --- /dev/null +++ b/sapl/materia/tests/test_materia_proximo_numero.py @@ -0,0 +1,385 @@ +from datetime import date + +import pytest +from django.db import IntegrityError, transaction +from model_bakery import baker +from rest_framework.test import APIClient + +from sapl.base.models import AppConfig as BaseAppConfig +from sapl.materia.models import (MateriaLegislativa, RegimeTramitacao, + TipoMateriaLegislativa) +from sapl.parlamentares.models import Legislatura + + +# --------------------------------------------------------------------------- +# Helpers / fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def tipo_materia(db): + """TipoMateriaLegislativa sem sequencia_numeracao própria (usa global).""" + return baker.make(TipoMateriaLegislativa, descricao='Projeto de Lei', + sigla='PL', sequencia_numeracao='') + + +@pytest.fixture +def tipo_materia_anual(db): + return baker.make(TipoMateriaLegislativa, descricao='Requerimento', + sigla='REQ', sequencia_numeracao='A') + + +@pytest.fixture +def tipo_materia_unico(db): + return baker.make(TipoMateriaLegislativa, descricao='Indicação', + sigla='IND', sequencia_numeracao='U') + + +@pytest.fixture +def tipo_materia_legislatura(db): + return baker.make(TipoMateriaLegislativa, descricao='PEC', + sigla='PEC', sequencia_numeracao='L') + + +@pytest.fixture +def regime(db): + return baker.make(RegimeTramitacao, descricao='Normal') + + +@pytest.fixture +def app_config(db): + return BaseAppConfig.objects.create(sequencia_numeracao_protocolo='A') + + +def _criar_materia(tipo, numero, ano, regime, **kwargs): + """Atalho para criar MateriaLegislativa com campos obrigatórios.""" + defaults = dict( + tipo=tipo, + numero=numero, + ano=ano, + data_apresentacao=date(ano, 1, 1), + regime_tramitacao=regime, + ementa='Ementa de teste', + ) + defaults.update(kwargs) + return MateriaLegislativa.objects.create(**defaults) + + +# =========================================================================== +# Testes de get_proximo_numero – numeração por ano (padrão) +# =========================================================================== + +@pytest.mark.django_db(transaction=False) +def test_proximo_numero_primeiro_do_ano(tipo_materia_anual, regime, app_config): + """Sem matérias existentes, o primeiro número gerado deve ser 1.""" + with transaction.atomic(): + numero, ano = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_anual, ano=2024) + assert numero == 1 + assert ano == 2024 + + +@pytest.mark.django_db(transaction=False) +def test_proximo_numero_sequencial(tipo_materia_anual, regime, app_config): + """Com matérias existentes 1, 2, 3 → próximo deve ser 4.""" + for n in (1, 2, 3): + _criar_materia(tipo_materia_anual, n, 2024, regime) + + with transaction.atomic(): + numero, ano = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_anual, ano=2024) + assert numero == 4 + assert ano == 2024 + + +@pytest.mark.django_db(transaction=False) +def test_proximo_numero_ignora_outro_ano(tipo_materia_anual, regime, app_config): + """Matérias em anos diferentes não influenciam a sequência.""" + _criar_materia(tipo_materia_anual, 10, 2023, regime, + data_apresentacao=date(2023, 6, 1)) + _criar_materia(tipo_materia_anual, 1, 2024, regime) + + with transaction.atomic(): + numero, ano = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_anual, ano=2024) + assert numero == 2 + + +@pytest.mark.django_db(transaction=False) +def test_proximo_numero_ignora_outro_tipo(tipo_materia_anual, regime, app_config): + """Matérias de outro tipo não influenciam a sequência.""" + outro_tipo = baker.make(TipoMateriaLegislativa, descricao='Moção', + sigla='MOC', sequencia_numeracao='A') + _criar_materia(outro_tipo, 50, 2024, regime) + _criar_materia(tipo_materia_anual, 3, 2024, regime) + + with transaction.atomic(): + numero, _ = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_anual, ano=2024) + assert numero == 4 + + +# =========================================================================== +# Testes de get_proximo_numero – numeração única +# =========================================================================== + +@pytest.mark.django_db(transaction=False) +def test_proximo_numero_unico(tipo_materia_unico, regime, app_config): + """Numeração 'U' ignora o ano – sequência é global por tipo.""" + _criar_materia(tipo_materia_unico, 1, 2022, regime, + data_apresentacao=date(2022, 1, 1)) + _criar_materia(tipo_materia_unico, 2, 2023, regime, + data_apresentacao=date(2023, 1, 1)) + + with transaction.atomic(): + numero, _ = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_unico, ano=2024) + assert numero == 3 + + +# =========================================================================== +# Testes de get_proximo_numero – numeração por legislatura +# =========================================================================== + +@pytest.mark.django_db(transaction=False) +def test_proximo_numero_por_legislatura(tipo_materia_legislatura, regime, + app_config): + """Numeração 'L' conta apenas matérias dentro da legislatura vigente.""" + baker.make(Legislatura, numero=1, + data_inicio=date(2021, 1, 1), + data_fim=date(2024, 12, 31), + data_eleicao=date(2020, 11, 15)) + + _criar_materia(tipo_materia_legislatura, 1, 2022, regime, + data_apresentacao=date(2022, 3, 1)) + _criar_materia(tipo_materia_legislatura, 2, 2023, regime, + data_apresentacao=date(2023, 5, 1)) + + with transaction.atomic(): + numero, _ = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_legislatura, ano=2024) + assert numero == 3 + + +@pytest.mark.django_db(transaction=False) +def test_proximo_numero_legislatura_inexistente(tipo_materia_legislatura, + regime, app_config): + """Sem legislatura vigente, número inicia em 1.""" + with transaction.atomic(): + numero, _ = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_legislatura, ano=2050) + assert numero == 1 + + +# =========================================================================== +# Testes de get_proximo_numero – numero_preferido +# =========================================================================== + +@pytest.mark.django_db(transaction=False) +def test_numero_preferido_disponivel(tipo_materia_anual, regime, app_config): + """Se o número preferido está disponível, deve ser retornado.""" + _criar_materia(tipo_materia_anual, 1, 2024, regime) + + with transaction.atomic(): + numero, _ = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_anual, ano=2024, numero_preferido=5) + assert numero == 5 + + +@pytest.mark.django_db(transaction=False) +def test_numero_preferido_em_uso_retorna_sequencial(tipo_materia_anual, + regime, app_config): + """Se o número preferido já existe, retorna o sequencial.""" + _criar_materia(tipo_materia_anual, 5, 2024, regime) + + with transaction.atomic(): + numero, _ = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_anual, ano=2024, numero_preferido=5) + assert numero == 6 + + +# =========================================================================== +# Testes de get_proximo_numero – resolução de tipo por pk (int/str) +# =========================================================================== + +@pytest.mark.django_db(transaction=False) +def test_tipo_passado_como_int(tipo_materia_anual, app_config): + """O tipo pode ser passado como int (pk).""" + with transaction.atomic(): + numero, _ = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_anual.pk, ano=2024) + assert numero == 1 + + +@pytest.mark.django_db(transaction=False) +def test_tipo_passado_como_str(tipo_materia_anual, app_config): + """O tipo pode ser passado como str representando o pk.""" + with transaction.atomic(): + numero, _ = MateriaLegislativa.get_proximo_numero( + tipo=str(tipo_materia_anual.pk), ano=2024) + assert numero == 1 + + +@pytest.mark.django_db(transaction=False) +def test_tipo_inexistente_levanta_excecao(app_config): + """Pk inexistente deve levantar DoesNotExist.""" + with pytest.raises(TipoMateriaLegislativa.DoesNotExist): + with transaction.atomic(): + MateriaLegislativa.get_proximo_numero(tipo=99999, ano=2024) + + +@pytest.mark.django_db(transaction=False) +def test_tipo_invalido_levanta_validation_error(app_config): + """Tipo não conversível para int deve levantar ValidationError.""" + from django.core.exceptions import ValidationError + with pytest.raises(ValidationError): + with transaction.atomic(): + MateriaLegislativa.get_proximo_numero(tipo='abc', ano=2024) + + +# =========================================================================== +# Testes de get_proximo_numero – ano padrão +# =========================================================================== + +@pytest.mark.django_db(transaction=False) +def test_ano_none_usa_ano_atual(tipo_materia_anual, app_config): + """Se ano=None, deve usar o ano corrente.""" + from django.utils import timezone + with transaction.atomic(): + _, ano = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia_anual, ano=None) + assert ano == timezone.now().year + + +# =========================================================================== +# Testes de get_proximo_numero – select_for_update (proteção concorrência) +# =========================================================================== + +@pytest.mark.django_db(transaction=False) +def test_unique_together_protege_duplicata(tipo_materia_anual, regime, + app_config): + """O unique_together (tipo, numero, ano) impede duplicatas no banco.""" + _criar_materia(tipo_materia_anual, 1, 2024, regime) + with pytest.raises(IntegrityError): + with transaction.atomic(): + _criar_materia(tipo_materia_anual, 1, 2024, regime) + + +# =========================================================================== +# Testes de global config – tipo sem sequencia_numeracao usa global +# =========================================================================== + +@pytest.mark.django_db(transaction=False) +def test_tipo_sem_sequencia_usa_config_global(tipo_materia, regime, app_config): + """Tipo com sequencia_numeracao vazio deve usar a config global ('A').""" + _criar_materia(tipo_materia, 1, 2024, regime) + _criar_materia(tipo_materia, 2, 2024, regime) + _criar_materia(tipo_materia, 10, 2023, regime, + data_apresentacao=date(2023, 1, 1)) + + with transaction.atomic(): + numero, _ = MateriaLegislativa.get_proximo_numero( + tipo=tipo_materia, ano=2024) + # global default 'A' → sequencial por ano → MAX(2024) = 2 → próximo = 3 + assert numero == 3 + + +# =========================================================================== +# Testes do endpoint API create (MateriaLegislativaViewSet) +# =========================================================================== + +@pytest.fixture +def api_client_autenticado(db): + """APIClient autenticado como superusuário (tem todas as permissões).""" + from django.contrib.auth import get_user_model + User = get_user_model() + user = User.objects.create_superuser( + username='admin_api', password='secret123', email='a@b.com') + client = APIClient() + client.force_authenticate(user=user) + return client + + +@pytest.mark.django_db(transaction=False) +def test_api_create_sem_numero_auto_gera(api_client_autenticado, + tipo_materia_anual, regime, + app_config): + """POST sem 'numero' deve auto-gerar próximo sequencial.""" + _criar_materia(tipo_materia_anual, 1, 2024, regime) + + response = api_client_autenticado.post( + '/api/materia/materialegislativa/', + { + 'tipo': tipo_materia_anual.pk, + 'ano': 2024, + 'data_apresentacao': '2024-06-01', + 'regime_tramitacao': regime.pk, + 'ementa': 'Teste auto-gerar número', + }, + format='json', + ) + assert response.status_code == 201 + assert response.data['numero'] == 2 + + +@pytest.mark.django_db(transaction=False) +def test_api_create_com_numero_respeita_dado(api_client_autenticado, + tipo_materia_anual, regime, + app_config): + """POST com 'numero' explícito deve criar com o número informado.""" + response = api_client_autenticado.post( + '/api/materia/materialegislativa/', + { + 'tipo': tipo_materia_anual.pk, + 'numero': 42, + 'ano': 2024, + 'data_apresentacao': '2024-06-01', + 'regime_tramitacao': regime.pk, + 'ementa': 'Número exato', + }, + format='json', + ) + assert response.status_code == 201 + assert response.data['numero'] == 42 + + +@pytest.mark.django_db(transaction=False) +def test_api_create_numero_duplicado_retorna_erro(api_client_autenticado, + tipo_materia_anual, regime, + app_config): + """POST com número já existente retorna erro. + + O DRF detecta a violação de unique_together via UniqueTogetherValidator + no serializer e retorna 400 antes de chegar ao banco. + """ + _criar_materia(tipo_materia_anual, 42, 2024, regime) + + response = api_client_autenticado.post( + '/api/materia/materialegislativa/', + { + 'tipo': tipo_materia_anual.pk, + 'numero': 42, + 'ano': 2024, + 'data_apresentacao': '2024-06-01', + 'regime_tramitacao': regime.pk, + 'ementa': 'Duplicata', + }, + format='json', + ) + assert response.status_code == 400 + + +@pytest.mark.django_db(transaction=False) +def test_api_create_sem_tipo_valida_serializer(api_client_autenticado, regime): + """POST sem 'tipo' deve falhar na validação do serializer (400).""" + response = api_client_autenticado.post( + '/api/materia/materialegislativa/', + { + 'numero': 1, + 'ano': 2024, + 'data_apresentacao': '2024-06-01', + 'regime_tramitacao': regime.pk, + 'ementa': 'Sem tipo', + }, + format='json', + ) + assert response.status_code == 400 From a66c052f91d3d4ec31b0a44649568f850f85b6ed Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Mon, 16 Mar 2026 08:35:36 -0300 Subject: [PATCH 11/16] fix: altera numero_preferido para numero_candidato --- sapl/materia/models.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 641a8e259..a16f80d7a 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -387,7 +387,7 @@ class MateriaLegislativa(models.Model): update_fields=update_fields) @staticmethod - def get_proximo_numero(tipo, ano=None, numero_preferido=None): + def get_proximo_numero(tipo, ano=None, numero_candidato=None): """ Retorna o próximo número disponível para uma MateriaLegislativa baseado no tipo e nas configurações de numeração. @@ -399,7 +399,7 @@ class MateriaLegislativa(models.Model): Args: tipo: TipoMateriaLegislativa ou int/str - o tipo da matéria ano: int - o ano da matéria (default: ano atual) - numero_preferido: int - número preferido/desejado (opcional). + numero_candidato: int - número candidato/desejado (opcional). Se fornecido e disponível, será retornado. Caso contrário, retorna o próximo sequencial. @@ -407,7 +407,6 @@ class MateriaLegislativa(models.Model): tuple[int, int]: Uma tupla contendo (numero, ano) da matéria. """ - if ano is None: ano = timezone.now().year @@ -467,20 +466,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: + # Converte o número candidato para inteiro, se possível + numero_candidato_int = None + if numero_candidato is not None: try: - numero_preferido_int = int(numero_preferido) + numero_candidato_int = int(numero_candidato) except (TypeError, ValueError): - numero_preferido_int = None + numero_candidato_int = None - # Verifica se o número preferido está disponível - if numero_preferido_int is not None and not MateriaLegislativa.objects.filter( + # Verifica se o número candidato está disponível + if numero_candidato_int is not None and not MateriaLegislativa.objects.filter( tipo=tipo, ano=ano, - numero=numero_preferido_int).exists(): - return numero_preferido_int, ano + numero=numero_candidato_int).exists(): + return numero_candidato_int, ano # Retorna o próximo número sequencial max_numero = numero['numero__max'] From 00c64007e541dbfde61431917a2beacff2802434 Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Mon, 16 Mar 2026 08:39:42 -0300 Subject: [PATCH 12/16] =?UTF-8?q?fix:=20remove=20espa=C3=A7os=20entre=20cl?= =?UTF-8?q?asses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/api/views_materia.py | 1 - sapl/materia/models.py | 1 - 2 files changed, 2 deletions(-) diff --git a/sapl/api/views_materia.py b/sapl/api/views_materia.py index c8bc24422..5dd88fdb5 100644 --- a/sapl/api/views_materia.py +++ b/sapl/api/views_materia.py @@ -147,7 +147,6 @@ class _MateriaLegislativaViewSet: 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 a16f80d7a..4790f1d9a 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -486,7 +486,6 @@ class MateriaLegislativa(models.Model): return ((max_numero + 1) if max_numero else 1), ano - class Autoria(models.Model): autor = models.ForeignKey(Autor, verbose_name=_('Autor'), From fef28d26b1e59816b4b69b989c470a65c822df2e Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Mon, 16 Mar 2026 09:03:34 -0300 Subject: [PATCH 13/16] fix: corrige uso de transaction e ausencia dele --- sapl/api/views_materia.py | 1 - sapl/materia/views.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sapl/api/views_materia.py b/sapl/api/views_materia.py index 5dd88fdb5..a37036ede 100644 --- a/sapl/api/views_materia.py +++ b/sapl/api/views_materia.py @@ -94,7 +94,6 @@ class _MateriaLegislativaViewSet: _MAX_RETRIES_NUMERO = 3 - @transaction.atomic def create(self, request, *args, **kwargs): data = dict(request.data) tipo = data.get('tipo', None) diff --git a/sapl/materia/views.py b/sapl/materia/views.py index e256bd43b..d80aebae3 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -16,6 +16,7 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, ValidationError +from django.db import transaction from django.db.models import Max, Q from django.http import HttpResponse, JsonResponse from django.http.response import Http404, HttpResponseRedirect @@ -338,7 +339,7 @@ class ProposicaoTaView(IntegracaoTaView): return self.get_redirect_deactivated() -@permission_required('materia.detail_materialegislativa') +@transaction.atomic def recuperar_materia(request): tipo = TipoMateriaLegislativa.objects.get(pk=request.GET['tipo']) ano = request.GET.get('ano', None) From 17302b39d68315b36e7f0dce23d22bb8f2aa516b Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Mon, 16 Mar 2026 09:08:48 -0300 Subject: [PATCH 14/16] =?UTF-8?q?fix:=20corrige=20testes=20devido=20mudan?= =?UTF-8?q?=C3=A7a=20de=20norme=20de=20vari=C3=A1vel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/materia/tests/test_materia_proximo_numero.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sapl/materia/tests/test_materia_proximo_numero.py b/sapl/materia/tests/test_materia_proximo_numero.py index d1d08f0a1..d9ad070eb 100644 --- a/sapl/materia/tests/test_materia_proximo_numero.py +++ b/sapl/materia/tests/test_materia_proximo_numero.py @@ -171,29 +171,29 @@ def test_proximo_numero_legislatura_inexistente(tipo_materia_legislatura, # =========================================================================== -# Testes de get_proximo_numero – numero_preferido +# Testes de get_proximo_numero – numero_candidato # =========================================================================== @pytest.mark.django_db(transaction=False) -def test_numero_preferido_disponivel(tipo_materia_anual, regime, app_config): - """Se o número preferido está disponível, deve ser retornado.""" +def test_numero_candidato_disponivel(tipo_materia_anual, regime, app_config): + """Se o número candidato está disponível, deve ser retornado.""" _criar_materia(tipo_materia_anual, 1, 2024, regime) with transaction.atomic(): numero, _ = MateriaLegislativa.get_proximo_numero( - tipo=tipo_materia_anual, ano=2024, numero_preferido=5) + tipo=tipo_materia_anual, ano=2024, numero_candidato=5) assert numero == 5 @pytest.mark.django_db(transaction=False) -def test_numero_preferido_em_uso_retorna_sequencial(tipo_materia_anual, +def test_numero_candidato_em_uso_retorna_sequencial(tipo_materia_anual, regime, app_config): - """Se o número preferido já existe, retorna o sequencial.""" + """Se o número candidato já existe, retorna o sequencial.""" _criar_materia(tipo_materia_anual, 5, 2024, regime) with transaction.atomic(): numero, _ = MateriaLegislativa.get_proximo_numero( - tipo=tipo_materia_anual, ano=2024, numero_preferido=5) + tipo=tipo_materia_anual, ano=2024, numero_candidato=5) assert numero == 6 From 5899636e2b3ef07cac175d09d11581d7d89ba430 Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Mon, 16 Mar 2026 09:14:20 -0300 Subject: [PATCH 15/16] =?UTF-8?q?fix:=20altera=20sele=C3=A7=C3=A3o=20para?= =?UTF-8?q?=20select=5Ffor=5Fupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/materia/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 4790f1d9a..bdeb402e3 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -444,8 +444,9 @@ class MateriaLegislativa(models.Model): numeracao = tipo.sequencia_numeracao # Calcula o próximo número baseado no tipo de numeração + materias_select_for_update = MateriaLegislativa.objects.select_for_update() if numeracao == 'A': # Por ano - numero = MateriaLegislativa.objects.filter( + numero = materias_select_for_update.filter( ano=ano, tipo=tipo).aggregate(Max('numero')) elif numeracao == 'L': # Por legislatura legislatura = Legislatura.objects.filter( @@ -454,14 +455,14 @@ class MateriaLegislativa(models.Model): if legislatura: data_inicio = legislatura.data_inicio data_fim = legislatura.data_fim - numero = MateriaLegislativa.objects.filter( + numero = materias_select_for_update.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( + numero = materias_select_for_update.filter( tipo=tipo).aggregate(Max('numero')) else: numero = {'numero__max': 0} @@ -475,7 +476,7 @@ class MateriaLegislativa(models.Model): numero_candidato_int = None # Verifica se o número candidato está disponível - if numero_candidato_int is not None and not MateriaLegislativa.objects.filter( + if numero_candidato_int is not None and not materias_select_for_update.filter( tipo=tipo, ano=ano, numero=numero_candidato_int).exists(): From fa3d7aa40ace1629ce1ccec096a536c236c6f14c Mon Sep 17 00:00:00 2001 From: LeandroJatai Date: Mon, 16 Mar 2026 17:06:57 -0300 Subject: [PATCH 16/16] =?UTF-8?q?fix:=20mudan=C3=A7a=20de=20nome=20remanes?= =?UTF-8?q?cente=20para=20numero=5Fcandidato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/materia/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 9c39c58df..3615fee47 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -2462,7 +2462,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): max_numero, ano = MateriaLegislativa.get_proximo_numero( tipo=tipo, ano=None, - numero_preferido=cd.get('numero_materia_futuro', None) + numero_candidato=cd.get('numero_materia_futuro', None) ) # dados básicos