Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions sapl/api/views_materia.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@

from django.apps.registry import apps
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, HTTP_409_CONFLICT
from rest_framework.response import Response

from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
Expand Down Expand Up @@ -90,6 +92,60 @@ class _MateriaLegislativaViewSet:
class Meta:
ordering = ['-ano', 'tipo', 'numero']

_MAX_RETRIES_NUMERO = 3

def create(self, request, *args, **kwargs):
data = dict(request.data)
tipo = data.get('tipo', None)
numero = data.get('numero', None)
ano = data.get('ano', None)

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)
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):

Expand Down
45 changes: 5 additions & 40 deletions sapl/materia/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2458,47 +2458,12 @@ def save(self, commit=False):
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_candidato=cd.get('numero_materia_futuro', None)
)

# dados básicos
materia = MateriaLegislativa()
Expand Down
106 changes: 105 additions & 1 deletion sapl/materia/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
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 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
Expand Down Expand Up @@ -382,6 +386,106 @@ def save(self, force_insert=False, force_update=False, using=None,
using=using,
update_fields=update_fields)

@staticmethod
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.

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 ou int/str - o tipo da matéria
ano: int - o ano da matéria (default: ano atual)
numero_candidato: int - número candidato/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.
"""

if ano is None:
ano = timezone.now().year

# Obtém a configuração de numeração
numeracao = None
try:
numeracao = BaseAppConfig.objects.last(
).sequencia_numeracao_protocolo
except AttributeError:
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_id)
except TipoMateriaLegislativa.DoesNotExist:
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

# 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 = materias_select_for_update.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 = 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 = materias_select_for_update.filter(
tipo=tipo).aggregate(Max('numero'))
Comment thread
joaohortsenado marked this conversation as resolved.
else:
numero = {'numero__max': 0}

# Converte o número candidato para inteiro, se possível
numero_candidato_int = None
if numero_candidato is not None:
try:
numero_candidato_int = int(numero_candidato)
except (TypeError, ValueError):
numero_candidato_int = None

# Verifica se o número candidato está disponível
if numero_candidato_int is not None and not materias_select_for_update.filter(
tipo=tipo,
ano=ano,
numero=numero_candidato_int).exists():
return numero_candidato_int, ano

# Retorna o próximo número sequencial
max_numero = numero['numero__max']
return ((max_numero + 1) if max_numero else 1), ano


Comment thread
joaohortsenado marked this conversation as resolved.
class Autoria(models.Model):
autor = models.ForeignKey(Autor,
Expand Down
Loading