Browse Source

Feat(Proposicao): tipo "Documento de gabinete" que não passa pelo Protocolo

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 <noreply@anthropic.com>
pull/3858/head
Gustavo 1 month ago
parent
commit
007ca6f01c
  1. 4
      sapl/materia/forms.py
  2. 19
      sapl/materia/migrations/0097_add_dispensa_protocolo_tipoproposicao.py
  3. 9
      sapl/materia/models.py
  4. 32
      sapl/materia/views.py
  5. 8
      sapl/templates/materia/proposicao_detail.html
  6. 3
      sapl/templates/materia/proposicao_list.html

4
sapl/materia/forms.py

@ -1597,7 +1597,8 @@ class TipoProposicaoForm(ModelForm):
'content_type', 'content_type',
'tipo_conteudo_related_radio', 'tipo_conteudo_related_radio',
'tipo_conteudo_related', 'tipo_conteudo_related',
'perfis'] 'perfis',
'dispensa_protocolo']
widgets = {'tipo_conteudo_related': forms.HiddenInput(), widgets = {'tipo_conteudo_related': forms.HiddenInput(),
'perfis': widgets.CheckboxSelectMultiple()} 'perfis': widgets.CheckboxSelectMultiple()}
@ -1612,6 +1613,7 @@ class TipoProposicaoForm(ModelForm):
Row( Row(
to_column(('descricao', 12)), to_column(('descricao', 12)),
to_column(('perfis', 12)), to_column(('perfis', 12)),
to_column(('dispensa_protocolo', 12)),
), ),
5 5
) )

19
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'),
),
]

9
sapl/materia/models.py

@ -71,6 +71,15 @@ class TipoProposicao(models.Model):
menos um Perfil Estrutural de Texto Articulado. 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: class Meta:
verbose_name = _('Tipo de Proposição') verbose_name = _('Tipo de Proposição')
verbose_name_plural = _('Tipos de Proposições') verbose_name_plural = _('Tipos de Proposições')

32
sapl/materia/views.py

@ -483,7 +483,8 @@ class TipoProposicaoCrud(CrudAux):
class BaseMixin(CrudAux.BaseMixin): class BaseMixin(CrudAux.BaseMixin):
list_field_names = [ list_field_names = [
"descricao", "content_type", 'tipo_conteudo_related'] "descricao", "content_type", 'tipo_conteudo_related',
'dispensa_protocolo']
class CreateView(CrudAux.CreateView): class CreateView(CrudAux.CreateView):
form_class = TipoProposicaoForm form_class = TipoProposicaoForm
@ -1033,6 +1034,13 @@ class UnidadeTramitacaoCrud(CrudAux):
form_class = UnidadeTramitacaoForm 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): class ProposicaoCrud(Crud):
model = Proposicao model = Proposicao
help_topic = 'proposicao' help_topic = 'proposicao'
@ -1118,7 +1126,9 @@ class ProposicaoCrud(Crud):
msg_error = '' msg_error = ''
if p and p.autor.operadores.filter(id=request.user.id).exists(): if p and p.autor.operadores.filter(id=request.user.id).exists():
if action == 'send': 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.') msg_error = _('Proposição já foi enviada e recebida.')
elif p.data_envio: elif p.data_envio:
msg_error = _('Proposição já foi enviada.') msg_error = _('Proposição já foi enviada.')
@ -1214,7 +1224,9 @@ class ProposicaoCrud(Crud):
elif action == 'send_setor': elif action == 'send_setor':
app_config = sapl.base.models.AppConfig.objects.all().last() 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.') msg_error = _('Revisão pelo Setor Legislativo não está habilitada.')
elif p.data_envio: elif p.data_envio:
msg_error = _('Proposição já foi enviada ao protocolo.') msg_error = _('Proposição já foi enviada ao protocolo.')
@ -1469,7 +1481,12 @@ class ProposicaoCrud(Crud):
status_filter = self.request.GET.get('status', '') status_filter = self.request.GET.get('status', '')
if status_filter == 'elaboracao': 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': elif status_filter == 'aguardando':
qs = qs.filter(data_envio__isnull=False, data_recebimento__isnull=True, data_devolucao__isnull=True, cancelado=False) qs = qs.filter(data_envio__isnull=False, data_recebimento__isnull=True, data_devolucao__isnull=True, cancelado=False)
elif status_filter == 'incorporada': elif status_filter == 'incorporada':
@ -1486,7 +1503,8 @@ class ProposicaoCrud(Crud):
qs_base = super().get_queryset() qs_base = super().get_queryset()
stats = { stats = {
'total': qs_base.count(), '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(), '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(), 'incorporada': qs_base.filter(data_recebimento__isnull=False, cancelado=False).count(),
'devolvida': qs_base.filter(data_devolucao__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 = 'aguardando'
status_label = 'Aguardando Recebimento' status_label = 'Aguardando Recebimento'
status_icon = 'fa-clock-o' 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: else:
status = 'elaboracao' status = 'elaboracao'
status_label = 'Em Elaboração' status_label = 'Em Elaboração'

8
sapl/templates/materia/proposicao_detail.html

@ -351,7 +351,13 @@
<a href="{% url 'sapl.materia:onlyoffice_editor' object.pk %}" class="btn btn-primary"> <a href="{% url 'sapl.materia:onlyoffice_editor' object.pk %}" class="btn btn-primary">
<i class="fa fa-edit"></i> {% trans "Editar Documento" %} <i class="fa fa-edit"></i> {% trans "Editar Documento" %}
</a> </a>
{% 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 #}
<span class="badge badge-secondary align-middle p-2">
<i class="fa fa-briefcase"></i>
{% trans "Documento de gabinete — não passa pelo Protocolo" %}
</span>
{% elif object.texto_original or object.texto_articulado.exists %}
<a href="{{ view.detail_url }}?action=send" class="btn btn-success"> <a href="{{ view.detail_url }}?action=send" class="btn btn-success">
<i class="fa fa-paper-plane"></i> {% trans "Enviar Proposição" %} <i class="fa fa-paper-plane"></i> {% trans "Enviar Proposição" %}
</a> </a>

3
sapl/templates/materia/proposicao_list.html

@ -213,6 +213,9 @@
<a href="?status=elaboracao" class="{% if status_filter == 'elaboracao' %}active{% endif %}"> <a href="?status=elaboracao" class="{% if status_filter == 'elaboracao' %}active{% endif %}">
<strong>{{ stats.elaboracao|default:"0" }}</strong> em elaboração <strong>{{ stats.elaboracao|default:"0" }}</strong> em elaboração
</a> </a>
<a href="?status=gabinete" class="{% if status_filter == 'gabinete' %}active{% endif %}">
<strong>{{ stats.gabinete|default:"0" }}</strong> de gabinete
</a>
<a href="?status=aguardando" class="{% if status_filter == 'aguardando' %}active{% endif %}"> <a href="?status=aguardando" class="{% if status_filter == 'aguardando' %}active{% endif %}">
<strong>{{ stats.aguardando|default:"0" }}</strong> enviadas <strong>{{ stats.aguardando|default:"0" }}</strong> enviadas
</a> </a>

Loading…
Cancel
Save