From 007ca6f01cb8acee3dd37ed45fc612bc7b6f526e Mon Sep 17 00:00:00 2001 From: Gustavo Date: Thu, 6 Aug 2026 01:56:05 -0300 Subject: [PATCH] =?UTF-8?q?Feat(Proposicao):=20tipo=20"Documento=20de=20ga?= =?UTF-8?q?binete"=20que=20n=C3=A3o=20passa=20pelo=20Protocolo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ofícios são documentos de uso próprio do gabinete do vereador e não precisam de validação da Casa, mas hoje caem na fila de Proposições Pendentes como qualquer outro tipo — a fila é definida apenas por data_envio/data_recebimento/ data_devolucao, sem considerar o tipo. O protocolo vinha devolvendo esses ofícios na mão. Adiciona TipoProposicao.dispensa_protocolo, configurável em Tabelas Auxiliares -> Tipo de Proposição, em vez de fixar a regra no tipo "Ofício". Para os tipos marcados: - As ações 'send' e 'send_setor' são recusadas, então data_envio nunca é gravada e a proposição não entra na fila do Protocolo nem do Setor Legislativo. - O botão "Enviar Proposição" dá lugar a um selo explicativo no detalhe. - A listagem do vereador ganha o status "Documento de Gabinete" e o filtro correspondente; esses documentos saem da contagem de "em elaboração". A visibilidade já é resolvida pelo container_field='autor__operadores' do ProposicaoCrud: a proposição só é acessível ao gabinete do autor. Proposições já existentes não são tocadas — as que estão enviadas, devolvidas ou já incorporadas seguem com o status atual. Co-Authored-By: Claude Opus 5 --- sapl/materia/forms.py | 4 ++- ...7_add_dispensa_protocolo_tipoproposicao.py | 19 +++++++++++ sapl/materia/models.py | 9 ++++++ sapl/materia/views.py | 32 ++++++++++++++++--- sapl/templates/materia/proposicao_detail.html | 8 ++++- sapl/templates/materia/proposicao_list.html | 3 ++ 6 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 sapl/materia/migrations/0097_add_dispensa_protocolo_tipoproposicao.py diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 0ba1ca7f0..b9a11ac92 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1597,7 +1597,8 @@ class TipoProposicaoForm(ModelForm): 'content_type', 'tipo_conteudo_related_radio', 'tipo_conteudo_related', - 'perfis'] + 'perfis', + 'dispensa_protocolo'] widgets = {'tipo_conteudo_related': forms.HiddenInput(), 'perfis': widgets.CheckboxSelectMultiple()} @@ -1612,6 +1613,7 @@ class TipoProposicaoForm(ModelForm): Row( to_column(('descricao', 12)), to_column(('perfis', 12)), + to_column(('dispensa_protocolo', 12)), ), 5 ) diff --git a/sapl/materia/migrations/0097_add_dispensa_protocolo_tipoproposicao.py b/sapl/materia/migrations/0097_add_dispensa_protocolo_tipoproposicao.py new file mode 100644 index 000000000..44cbcec1d --- /dev/null +++ b/sapl/materia/migrations/0097_add_dispensa_protocolo_tipoproposicao.py @@ -0,0 +1,19 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0096_add_permite_remover_assinatura_e_permissoes'), + ] + + operations = [ + migrations.AddField( + model_name='tipoproposicao', + name='dispensa_protocolo', + field=models.BooleanField( + default=False, + help_text='Quando marcado, proposições deste tipo não são enviadas ao Protocolo e não se tornam Matéria Legislativa. Ficam restritas ao gabinete do autor. Use para documentos de uso próprio do gabinete, como Ofícios, que não precisam de validação da Casa.', + verbose_name='Documento de gabinete'), + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 0d490ded0..1ac78ce2b 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -71,6 +71,15 @@ class TipoProposicao(models.Model): menos um Perfil Estrutural de Texto Articulado. """)) + dispensa_protocolo = models.BooleanField( + default=False, + verbose_name=_('Documento de gabinete'), + help_text=_('Quando marcado, proposições deste tipo não são enviadas ' + 'ao Protocolo e não se tornam Matéria Legislativa. Ficam ' + 'restritas ao gabinete do autor. Use para documentos de ' + 'uso próprio do gabinete, como Ofícios, que não precisam ' + 'de validação da Casa.')) + class Meta: verbose_name = _('Tipo de Proposição') verbose_name_plural = _('Tipos de Proposições') diff --git a/sapl/materia/views.py b/sapl/materia/views.py index d621006ee..9430ca909 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -483,7 +483,8 @@ class TipoProposicaoCrud(CrudAux): class BaseMixin(CrudAux.BaseMixin): list_field_names = [ - "descricao", "content_type", 'tipo_conteudo_related'] + "descricao", "content_type", 'tipo_conteudo_related', + 'dispensa_protocolo'] class CreateView(CrudAux.CreateView): form_class = TipoProposicaoForm @@ -1033,6 +1034,13 @@ class UnidadeTramitacaoCrud(CrudAux): form_class = UnidadeTramitacaoForm +# Tipos de proposição marcados como "Documento de gabinete" não saem do +# gabinete do autor: não vão ao Protocolo nem ao Setor Legislativo. +MSG_DISPENSA_PROTOCOLO = _( + 'Este é um documento de gabinete e não passa pelo Protocolo. ' + 'Ele fica restrito ao seu gabinete.') + + class ProposicaoCrud(Crud): model = Proposicao help_topic = 'proposicao' @@ -1118,7 +1126,9 @@ class ProposicaoCrud(Crud): msg_error = '' if p and p.autor.operadores.filter(id=request.user.id).exists(): if action == 'send': - if p.data_envio and p.data_recebimento: + if p.tipo and p.tipo.dispensa_protocolo: + msg_error = MSG_DISPENSA_PROTOCOLO + elif p.data_envio and p.data_recebimento: msg_error = _('Proposição já foi enviada e recebida.') elif p.data_envio: msg_error = _('Proposição já foi enviada.') @@ -1214,7 +1224,9 @@ class ProposicaoCrud(Crud): elif action == 'send_setor': app_config = sapl.base.models.AppConfig.objects.all().last() - if not app_config or not app_config.revisao_setor_legislativo: + if p.tipo and p.tipo.dispensa_protocolo: + msg_error = MSG_DISPENSA_PROTOCOLO + elif not app_config or not app_config.revisao_setor_legislativo: msg_error = _('Revisão pelo Setor Legislativo não está habilitada.') elif p.data_envio: msg_error = _('Proposição já foi enviada ao protocolo.') @@ -1469,7 +1481,12 @@ class ProposicaoCrud(Crud): status_filter = self.request.GET.get('status', '') if status_filter == 'elaboracao': - qs = qs.filter(data_envio__isnull=True, cancelado=False) + qs = qs.filter( + data_envio__isnull=True, cancelado=False + ).exclude(tipo__dispensa_protocolo=True) + elif status_filter == 'gabinete': + qs = qs.filter(tipo__dispensa_protocolo=True, + data_envio__isnull=True, cancelado=False) elif status_filter == 'aguardando': qs = qs.filter(data_envio__isnull=False, data_recebimento__isnull=True, data_devolucao__isnull=True, cancelado=False) elif status_filter == 'incorporada': @@ -1486,7 +1503,8 @@ class ProposicaoCrud(Crud): qs_base = super().get_queryset() stats = { 'total': qs_base.count(), - 'elaboracao': qs_base.filter(data_envio__isnull=True, cancelado=False).count(), + 'elaboracao': qs_base.filter(data_envio__isnull=True, cancelado=False).exclude(tipo__dispensa_protocolo=True).count(), + 'gabinete': qs_base.filter(tipo__dispensa_protocolo=True, data_envio__isnull=True, cancelado=False).count(), 'aguardando': qs_base.filter(data_envio__isnull=False, data_recebimento__isnull=True, data_devolucao__isnull=True, cancelado=False).count(), 'incorporada': qs_base.filter(data_recebimento__isnull=False, cancelado=False).count(), 'devolvida': qs_base.filter(data_devolucao__isnull=False, cancelado=False).count(), @@ -1521,6 +1539,10 @@ class ProposicaoCrud(Crud): status = 'aguardando' status_label = 'Aguardando Recebimento' status_icon = 'fa-clock-o' + elif obj.tipo and obj.tipo.dispensa_protocolo: + status = 'gabinete' + status_label = 'Documento de Gabinete' + status_icon = 'fa-briefcase' else: status = 'elaboracao' status_label = 'Em Elaboração' diff --git a/sapl/templates/materia/proposicao_detail.html b/sapl/templates/materia/proposicao_detail.html index a84a23e63..ad7bfb789 100644 --- a/sapl/templates/materia/proposicao_detail.html +++ b/sapl/templates/materia/proposicao_detail.html @@ -351,7 +351,13 @@ {% trans "Editar Documento" %} - {% if object.texto_original or object.texto_articulado.exists %} + {% if object.tipo.dispensa_protocolo %} + {# Documento de gabinete: não vai ao Protocolo nem ao Setor Legislativo #} + + + {% trans "Documento de gabinete — não passa pelo Protocolo" %} + + {% elif object.texto_original or object.texto_articulado.exists %} {% trans "Enviar Proposição" %} diff --git a/sapl/templates/materia/proposicao_list.html b/sapl/templates/materia/proposicao_list.html index b42372ddf..b40fd85b1 100644 --- a/sapl/templates/materia/proposicao_list.html +++ b/sapl/templates/materia/proposicao_list.html @@ -213,6 +213,9 @@ {{ stats.elaboracao|default:"0" }} em elaboração + + {{ stats.gabinete|default:"0" }} de gabinete + {{ stats.aguardando|default:"0" }} enviadas