Browse Source

Feat(Proposicao): Etapa de revisão pelo Setor Legislativo

Adiciona fluxo intermediário de revisão pelo Setor Legislativo antes do
envio ao Protocolo, protegido por feature flag (desabilitado por padrão).

- Novos campos: data_envio_setor, data_retorno_setor, usuario_envio_setor,
  usuario_retorno_setor, observacao_setor em Proposicao
- Flag revisao_setor_legislativo em AppConfig (Sistema > Configurações)
- Novo grupo "Operador do Setor Legislativo" com permissões específicas
- Ações send_setor e return_from_setor no DetailView
- Views ProposicaoPendenteSetor (inbox) e RevisarProposicaoSetor
- OnlyOffice: setor pode editar documento quando proposição está com eles
- Status S (Enviada ao Setor) e V (Revisada pelo Setor) no histórico
- Templates: badges, alertas, botões condicionais e subnav
pull/3858/head
rangelbruno 7 months ago
parent
commit
4b383a8a9c
  1. 1
      sapl/base/forms.py
  2. 18
      sapl/base/migrations/0064_appconfig_revisao_setor_legislativo.py
  3. 4
      sapl/base/models.py
  4. 65
      sapl/materia/forms.py
  5. 50
      sapl/materia/migrations/0092_auto_20260223_1102.py
  6. 42
      sapl/materia/models.py
  7. 27
      sapl/materia/onlyoffice_views.py
  8. 7
      sapl/materia/urls.py
  9. 143
      sapl/materia/views.py
  10. 2
      sapl/rules/__init__.py
  11. 17
      sapl/rules/group_setor_legislativo.py
  12. 3
      sapl/rules/map_rules.py
  13. 1
      sapl/templates/base/layouts.yaml
  14. 76
      sapl/templates/materia/prop_pendentes_setor_list.html
  15. 7
      sapl/templates/materia/proposicao_base.html
  16. 84
      sapl/templates/materia/proposicao_detail.html
  17. 80
      sapl/templates/materia/revisar_proposicao_setor.html

1
sapl/base/forms.py

@ -1073,6 +1073,7 @@ class ConfiguracoesAppForm(ModelForm):
'assinatura_ata',
'estatisticas_acesso_normas',
'escolher_numero_materia_proposicao',
'revisao_setor_legislativo',
'tramitacao_origem_fixa',
'tramitacao_materia',
'ordenacao_pesquisa_materia',

18
sapl/base/migrations/0064_appconfig_revisao_setor_legislativo.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2026-02-23 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0063_merge_20260210_1053'),
]
operations = [
migrations.AddField(
model_name='appconfig',
name='revisao_setor_legislativo',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Habilitar etapa de revisão pelo Setor Legislativo?'),
),
]

4
sapl/base/models.py

@ -185,6 +185,10 @@ class AppConfig(models.Model):
verbose_name=_(
'Indicar número da matéria a ser gerada na proposição?'),
choices=YES_NO_CHOICES, default=False)
revisao_setor_legislativo = models.BooleanField(
verbose_name=_(
'Habilitar etapa de revisão pelo Setor Legislativo?'),
choices=YES_NO_CHOICES, default=False)
# MÓDULO MATÉRIA LEGISLATIVA
# Linha 1 ------------------

65
sapl/materia/forms.py

@ -2334,6 +2334,71 @@ class DevolverProposicaoForm(forms.ModelForm):
return self.instance
class RevisaoSetorForm(forms.ModelForm):
observacao_setor = forms.CharField(
required=False,
label=_('Observações da revisão'),
widget=widgets.Textarea(attrs={'rows': 5}))
logger = logging.getLogger(__name__)
class Meta:
model = Proposicao
fields = [
'observacao_setor',
'user',
'ip'
]
widgets = {
'user': forms.HiddenInput(),
'ip': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super(RevisaoSetorForm, self).__init__(*args, **kwargs)
fields = []
fields.append(
Fieldset(
_('Revisão do Setor Legislativo'),
to_column(('observacao_setor', 12)),
to_column(
(form_actions(label=_('Devolver ao Autor'),
name='devolver_setor',
css_class='btn-success float-right'), 12)
)
)
)
self.helper = SaplFormHelper()
self.helper.layout = Layout(*fields)
@transaction.atomic
def save(self, commit=False):
cd = self.cleaned_data
self.instance.data_retorno_setor = timezone.now()
self.instance.usuario_retorno_setor = self.initial['user']
self.instance.save()
observacao = cd.get('observacao_setor', '')
HistoricoProposicao.objects.create(
proposicao=self.instance,
status='V',
user=self.initial['user'],
ip=self.initial['ip'],
observacao=observacao)
self.instance.results = {
'messages': {
'success': [_('Proposição revisada e devolvida ao autor com sucesso.'), ]
},
'url': reverse('sapl.materia:proposicao-pendente-setor')
}
return self.instance
class ConfirmarProposicaoForm(ProposicaoForm):
tipo_readonly = forms.CharField(

50
sapl/materia/migrations/0092_auto_20260223_1102.py

@ -0,0 +1,50 @@
# Generated by Django 2.2.28 on 2026-02-23 14:02
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('materia', '0091_add_codigo_autenticacao'),
]
operations = [
migrations.AlterModelOptions(
name='proposicao',
options={'ordering': ['-data_recebimento'], 'permissions': (('detail_proposicao_enviada', 'Pode acessar detalhes de uma proposição enviada.'), ('detail_proposicao_devolvida', 'Pode acessar detalhes de uma proposição devolvida.'), ('detail_proposicao_incorporada', 'Pode acessar detalhes de uma proposição incorporada.'), ('detail_proposicao_em_revisao_setor', 'Pode acessar detalhes de uma proposição em revisão pelo setor.')), 'verbose_name': 'Proposição', 'verbose_name_plural': 'Proposições'},
),
migrations.AddField(
model_name='proposicao',
name='data_envio_setor',
field=models.DateTimeField(blank=True, null=True, verbose_name='Data de Envio ao Setor Legislativo'),
),
migrations.AddField(
model_name='proposicao',
name='data_retorno_setor',
field=models.DateTimeField(blank=True, null=True, verbose_name='Data de Retorno do Setor Legislativo'),
),
migrations.AddField(
model_name='proposicao',
name='observacao_setor',
field=models.TextField(blank=True, default='', verbose_name='Observação do Setor Legislativo'),
),
migrations.AddField(
model_name='proposicao',
name='usuario_envio_setor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='proposicoes_enviadas_setor', to=settings.AUTH_USER_MODEL, verbose_name='Usuário Responsável pelo Envio ao Setor'),
),
migrations.AddField(
model_name='proposicao',
name='usuario_retorno_setor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='proposicoes_retornadas_setor', to=settings.AUTH_USER_MODEL, verbose_name='Usuário Responsável pelo Retorno do Setor'),
),
migrations.AlterField(
model_name='historicoproposicao',
name='status',
field=models.CharField(choices=[('E', 'Enviada'), ('R', 'Recebida'), ('T', 'Retornada'), ('D', 'Devolvida'), ('S', 'Enviada ao Setor Legislativo'), ('V', 'Revisada pelo Setor Legislativo')], db_index=True, max_length=1, verbose_name='Status de Proposição'),
),
]

42
sapl/materia/models.py

@ -855,6 +855,42 @@ class Proposicao(models.Model):
verbose_name=_('Data de Devolução')
)
data_envio_setor = models.DateTimeField(
blank=True,
null=True,
verbose_name=_('Data de Envio ao Setor Legislativo')
)
data_retorno_setor = models.DateTimeField(
blank=True,
null=True,
verbose_name=_('Data de Retorno do Setor Legislativo')
)
usuario_envio_setor = models.ForeignKey(
get_settings_auth_user_model(),
verbose_name=_('Usuário Responsável pelo Envio ao Setor'),
on_delete=models.PROTECT,
related_name='proposicoes_enviadas_setor',
blank=True,
null=True
)
usuario_retorno_setor = models.ForeignKey(
get_settings_auth_user_model(),
verbose_name=_('Usuário Responsável pelo Retorno do Setor'),
on_delete=models.PROTECT,
related_name='proposicoes_retornadas_setor',
blank=True,
null=True
)
observacao_setor = models.TextField(
blank=True,
default='',
verbose_name=_('Observação do Setor Legislativo')
)
usuario_envio = models.ForeignKey(
get_settings_auth_user_model(),
verbose_name=_('Usuário Responsável pelo Envio'),
@ -1057,6 +1093,8 @@ class Proposicao(models.Model):
_('Pode acessar detalhes de uma proposição devolvida.')),
('detail_proposicao_incorporada',
_('Pode acessar detalhes de uma proposição incorporada.')),
('detail_proposicao_em_revisao_setor',
_('Pode acessar detalhes de uma proposição em revisão pelo setor.')),
)
def __str__(self):
@ -1123,7 +1161,9 @@ class HistoricoProposicao(models.Model):
STATUS_PROPOSICAO = Choices(('E', 'ENVIADA', _('Enviada')),
('R', 'RECEBIDA', _('Recebida')),
('T', 'RETORNADA', _('Retornada')),
('D', 'DEVOLVIDA', _('Devolvida')))
('D', 'DEVOLVIDA', _('Devolvida')),
('S', 'ENVIADA_SETOR', _('Enviada ao Setor Legislativo')),
('V', 'REVISADA_SETOR', _('Revisada pelo Setor Legislativo')))
proposicao = models.ForeignKey(Proposicao,
verbose_name=_('Proposição'),

27
sapl/materia/onlyoffice_views.py

@ -40,9 +40,21 @@ def onlyoffice_config(request, pk):
proposicao = get_object_or_404(Proposicao, pk=pk)
# Verifica se o usuário tem permissão para editar
is_autor_operador = proposicao.autor.operadores.filter(id=request.user.id).exists()
# Setor Legislativo pode editar quando a proposição está com o setor
from sapl.rules import SGVP_GROUP_SETOR_LEGISLATIVO
is_setor = request.user.groups.filter(name=SGVP_GROUP_SETOR_LEGISLATIVO).exists()
setor_can_edit = (
is_setor and
proposicao.data_envio_setor is not None and
proposicao.data_retorno_setor is None and
proposicao.data_envio is None
)
can_edit = (
not proposicao.data_envio and
proposicao.autor.operadores.filter(id=request.user.id).exists()
(is_autor_operador or setor_can_edit)
)
# URLs para o OnlyOffice acessar (dentro da rede Docker)
@ -358,7 +370,18 @@ def onlyoffice_editor(request, pk):
proposicao = get_object_or_404(Proposicao, pk=pk)
# Verifica se o usuário tem permissão
if not proposicao.autor.operadores.filter(id=request.user.id).exists():
is_autor_operador = proposicao.autor.operadores.filter(id=request.user.id).exists()
from sapl.rules import SGVP_GROUP_SETOR_LEGISLATIVO
is_setor = request.user.groups.filter(name=SGVP_GROUP_SETOR_LEGISLATIVO).exists()
setor_can_edit = (
is_setor and
proposicao.data_envio_setor is not None and
proposicao.data_retorno_setor is None and
proposicao.data_envio is None
)
if not is_autor_operador and not setor_can_edit:
from django.contrib import messages
from django.shortcuts import redirect
messages.error(request, 'Você não tem permissão para editar esta proposição.')

7
sapl/materia/urls.py

@ -17,6 +17,7 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
NumeracaoCrud, OrgaoCrud, OrigemCrud,
PrimeiraTramitacaoEmLoteView, ProposicaoCrud,
ProposicaoDevolvida, ProposicaoPendente,
ProposicaoPendenteSetor,
ProposicaoRecebida, ProposicaoTaView,
ReceberProposicao, ReciboProposicaoView,
RegimeTramitacaoCrud, RelatoriaCrud,
@ -26,7 +27,7 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
TramitacaoEmLoteView, UnidadeTramitacaoCrud,
proposicao_texto, recuperar_materia,
ExcluirTramitacaoEmLoteView,
RetornarProposicao,
RetornarProposicao, RevisarProposicaoSetor,
MateriaPesquisaSimplesView,
DespachoInicialMultiCreateView,
get_zip_docacessorios, get_pdf_docacessorios, get_zip_completo, get_pdf_completo,
@ -231,6 +232,10 @@ urlpatterns_proposicao = [
name='proposicao-recebida'),
url(r'^proposicao/devolvida/', ProposicaoDevolvida.as_view(),
name='proposicao-devolvida'),
url(r'^proposicao/pendente-setor/', ProposicaoPendenteSetor.as_view(),
name='proposicao-pendente-setor'),
url(r'^proposicao/(?P<pk>\d+)/revisao-setor/', RevisarProposicaoSetor.as_view(),
name='revisao-proposicao-setor'),
# OnlyOffice endpoints para confirmação de proposição (DEVE vir antes de proposicao-confirmar)
url(r'^proposicao/confirmar/P(?P<hash>[0-9A-Fa-f]+)/(?P<pk>\d+)/onlyoffice/editor$',
onlyoffice_confirmar_editor,

143
sapl/materia/views.py

@ -49,6 +49,7 @@ from sapl.materia.forms import (AnexadaForm, AutoriaForm, AutoriaMultiCreateForm
ConfirmarProposicaoForm, DevolverProposicaoForm,
DespachoInicialCreateForm, LegislacaoCitadaForm,
MateriaPesquisaSimplesForm, OrgaoForm, ProposicaoForm,
RevisaoSetorForm,
TipoProposicaoForm, TramitacaoForm, TramitacaoUpdateForm, ConfigEtiquetaMateriaLegislativaForms)
from sapl.norma.models import LegislacaoCitada
from sapl.parlamentares.models import (
@ -613,6 +614,73 @@ class ProposicaoRecebida(PermissionRequiredMixin, ListView):
return context
class ProposicaoPendenteSetor(PermissionRequiredMixin, ListView):
template_name = 'materia/prop_pendentes_setor_list.html'
model = Proposicao
ordering = ['data_envio_setor', 'autor', 'tipo', 'descricao']
paginate_by = 10
permission_required = ('materia.detail_proposicao_em_revisao_setor', )
def get_queryset(self):
return Proposicao.objects.filter(
data_envio_setor__isnull=False,
data_retorno_setor__isnull=True,
data_envio__isnull=True)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['AppConfig'] = sapl.base.models.AppConfig.objects.all().last()
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhuma proposição pendente de revisão pelo setor.'
qr = self.request.GET.copy()
context['filter_url'] = ('&o=' + qr['o']) if 'o' in qr.keys() else ''
return context
class RevisarProposicaoSetor(PermissionRequiredMixin, UpdateView):
template_name = "materia/revisar_proposicao_setor.html"
model = Proposicao
form_class = RevisaoSetorForm
permission_required = ('materia.detail_proposicao_em_revisao_setor', )
logger = logging.getLogger(__name__)
def get_initial(self):
initial = super().get_initial()
initial['ip'] = get_client_ip(self.request)
initial['user'] = self.request.user
return initial
def get_success_url(self):
msgs = self.object.results['messages']
for key, value in msgs.items():
for item in value:
getattr(messages, key)(self.request, item)
return self.object.results['url']
def dispatch(self, request, *args, **kwargs):
username = request.user.username
try:
p = Proposicao.objects.get(id=kwargs['pk'])
except Exception:
raise Http404()
if not p.data_envio_setor or p.data_envio:
messages.error(request,
_('Esta proposição não está pendente de revisão pelo setor.'))
return redirect(reverse('sapl.materia:proposicao-pendente-setor'))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['subnav_template_name'] = ''
context['proposicao'] = self.object
return context
class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView):
app_label = sapl.protocoloadm.apps.AppConfig.label
template_name = "materia/receber_proposicao.html"
@ -884,7 +952,8 @@ class ProposicaoCrud(Crud):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['subnav_template_name'] = ''
context['AppConfig'] = sapl.base.models.AppConfig.objects.all().last()
app_config = sapl.base.models.AppConfig.objects.all().last()
context['AppConfig'] = app_config
context['title'] = '%s <small>(%s)</small>' % (
self.object, self.object.autor)
@ -893,6 +962,17 @@ class ProposicaoCrud(Crud):
context['proposicao'] = Proposicao.objects.get(
pk=self.kwargs['pk']
)
context['revisao_setor_legislativo'] = (
app_config.revisao_setor_legislativo if app_config else False
)
# Check if current user is member of the Setor Legislativo group
from sapl.rules import SGVP_GROUP_SETOR_LEGISLATIVO
context['is_setor_legislativo'] = (
self.request.user.groups.filter(
name=SGVP_GROUP_SETOR_LEGISLATIVO
).exists()
)
return context
def get(self, request, *args, **kwargs):
@ -935,6 +1015,8 @@ class ProposicaoCrud(Crud):
str(p.pk))
p.data_devolucao = None
p.data_envio_setor = None
p.data_retorno_setor = None
p.data_envio = timezone.now()
p.save()
HistoricoProposicao.objects.create(
@ -1000,6 +1082,46 @@ class ProposicaoCrud(Crud):
messages.success(request, _(
'Proposição Retornada com sucesso.'))
elif action == 'send_setor':
app_config = sapl.base.models.AppConfig.objects.all().last()
if 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.')
elif not p.texto_original and not p.texto_articulado.exists():
msg_error = _(
'Proposição não possui texto. Crie o documento antes de enviar ao setor.')
else:
p.data_envio_setor = timezone.now()
p.data_retorno_setor = None
p.usuario_envio_setor = request.user
p.save()
HistoricoProposicao.objects.create(
proposicao=p,
status='S',
ip=get_client_ip(self.request),
user=self.request.user)
self.logger.info("User={}. Proposição (id={}) enviada ao Setor Legislativo."
.format(username, p.pk))
messages.success(request, _(
'Proposição enviada ao Setor Legislativo com sucesso.'))
elif action == 'return_from_setor':
app_config = sapl.base.models.AppConfig.objects.all().last()
if not app_config or not app_config.revisao_setor_legislativo:
msg_error = _('Revisão pelo Setor Legislativo não está habilitada.')
elif not p.data_envio_setor:
msg_error = _('Proposição não está com o Setor Legislativo.')
elif p.data_retorno_setor:
msg_error = _('Proposição já foi devolvida pelo setor.')
else:
p.data_envio_setor = None
p.save()
self.logger.info("User={}. Proposição (id={}) retornada do Setor Legislativo."
.format(username, p.pk))
messages.success(request, _(
'Proposição retornada do Setor Legislativo com sucesso.'))
if msg_error:
messages.error(request, msg_error)
@ -1021,17 +1143,22 @@ class ProposicaoCrud(Crud):
return self.handle_no_permission()
if not p.autor.operadores.filter(id=request.user.id).exists():
if not p.data_envio and not p.data_devolucao:
raise Http404()
# Allow Setor Legislativo users to access proposições sent to them
from sapl.rules import SGVP_GROUP_SETOR_LEGISLATIVO
is_setor = request.user.groups.filter(
name=SGVP_GROUP_SETOR_LEGISLATIVO).exists()
if p.data_devolucao and not request.user.has_perm('materia.detail_proposicao_devolvida'):
if is_setor and p.data_envio_setor and not p.data_envio:
# Setor Legislativo can view proposições sent to them
pass
elif not p.data_envio and not p.data_devolucao:
raise Http404()
if p.data_envio and not p.data_recebimento \
elif p.data_devolucao and not request.user.has_perm('materia.detail_proposicao_devolvida'):
raise Http404()
elif p.data_envio and not p.data_recebimento \
and not request.user.has_perm('materia.detail_proposicao_enviada'):
raise Http404()
if p.data_envio and p.data_recebimento \
elif p.data_envio and p.data_recebimento \
and not request.user.has_perm('materia.detail_proposicao_incorporada'):
raise Http404()

2
sapl/rules/__init__.py

@ -67,6 +67,7 @@ SGVP_GROUP_GERAL = _("Operador Geral")
SGVP_GROUP_AUTOR = _("Autor")
SGVP_GROUP_VOTANTE = _("Votante")
SGVP_GROUP_PRESIDENTE_MESA = _("Presidente da Mesa Diretora")
SGVP_GROUP_SETOR_LEGISLATIVO = _("Operador do Setor Legislativo")
# TODO - funcionalidade ainda não existe mas está aqui para efeito de anotação
SGVP_GROUP_LOGIN_SOCIAL = _("Usuários com Login Social")
@ -88,6 +89,7 @@ SGVP_GROUPS = [
SGVP_GROUP_AUTOR,
SGVP_GROUP_VOTANTE,
SGVP_GROUP_PRESIDENTE_MESA,
SGVP_GROUP_SETOR_LEGISLATIVO,
SGVP_GROUP_LOGIN_SOCIAL,
SGVP_GROUP_ANONYMOUS,
]

17
sapl/rules/group_setor_legislativo.py

@ -0,0 +1,17 @@
from sapl.compilacao import models as compilacao
from sapl.materia import models as materia
from sapl.rules import SGVP_GROUP_SETOR_LEGISLATIVO, RP_LIST, RP_DETAIL, RP_CHANGE, \
__listdetailchange__, __perms_publicas__
rules_group_setor_legislativo = {
'group': SGVP_GROUP_SETOR_LEGISLATIVO,
'rules': [
(materia.Proposicao, __listdetailchange__ +
['detail_proposicao_enviada',
'detail_proposicao_devolvida',
'detail_proposicao_incorporada',
'detail_proposicao_em_revisao_setor'], set()),
(materia.HistoricoProposicao, [RP_LIST, RP_DETAIL], set()),
(compilacao.TextoArticulado, ['view_restricted_textoarticulado'], __perms_publicas__),
]
}

3
sapl/rules/map_rules.py

@ -9,6 +9,7 @@ from sapl.rules.group_materia import rules_group_materia
from sapl.rules.group_norma import rules_group_norma
from sapl.rules.group_painel import rules_group_painel
from sapl.rules.group_protocolo import rules_group_protocolo
from sapl.rules.group_setor_legislativo import rules_group_setor_legislativo
from sapl.rules.group_sessao import rules_group_sessao
from sapl.rules.group_votante import rules_group_votante
@ -84,6 +85,7 @@ rules_group_geral['rules'] = (rules_group_geral['rules'] +
rules_group_norma['rules'] +
rules_group_sessao['rules'] +
rules_group_painel['rules'] +
rules_group_setor_legislativo['rules'] +
rules_group_login_social['rules'])
rules_patterns = [
@ -99,6 +101,7 @@ rules_patterns = [
rules_group_autor,
rules_group_votante,
rules_group_presidente_mesa,
rules_group_setor_legislativo,
rules_group_anonymous, # anotação para validação do teste de rules
rules_group_login_social # TODO não implementado

1
sapl/templates/base/layouts.yaml

@ -34,6 +34,7 @@ AppConfig:
{% trans 'Módulo Proposições' %}:
- sequencia_numeracao_proposicao receber_recibo_proposicao proposicao_incorporacao_obrigatoria escolher_numero_materia_proposicao
- revisao_setor_legislativo
{% trans 'Módulo Matéria Legislativa' %}:
- tramitacao_origem_fixa:5 tramitacao_materia:4 ordenacao_pesquisa_materia

76
sapl/templates/materia/prop_pendentes_setor_list.html

@ -0,0 +1,76 @@
{% extends "materia/proposicao_base.html" %}
{% load i18n %}
{% load tz %}
{% load common_tags %}
{% block base_content %}
<div class="proposicao-content">
<div class="proposicao-header">
<h2>{% trans "Revisão do Setor Legislativo" %}</h2>
<p class="text-muted">{% trans "Proposições enviadas ao setor aguardando revisão" %}</p>
</div>
{% if not object_list %}
<p>{{ NO_ENTRIES_MSG }}</p>
{% else %}
<table class="table table-striped table-hover">
<thead>
<tr>
{% with 'Data Envio ao Setor,Tipo,Descrição,Autor' as list %}
{% for name in list|split:"," %}
<th>
<a title="{% trans 'Clique para alterar a ordem a listagem'%}" href="?o={% if 'o' not in request.GET and forloop.counter == 1 or 'o' in request.GET and forloop.counter|safe == request.GET.o %}-{%endif%}{{forloop.counter}}">
{{ name }}
{% if 'o' in request.GET %}
{% if 'o' not in request.GET and forloop.counter == 1 or 'o' in request.GET and forloop.counter|safe == request.GET.o %}
<span class="caret top" title="{% trans 'Listado na Ordem Ascendente'%}"></span>
{% elif 'o' in request.GET and forloop.counter == request.GET.o|str2intabs %}
<span class="caret" title="{% trans 'Listado na Ordem Descendente'%}"></span>
{%endif%}
{%endif%}
</a>
</th>
{% endfor %}
<th>{% trans "Ações" %}</th>
{% endwith %}
</tr>
</thead>
<tbody>
{% if 'o' in request.GET %}
{% define object_list|sort_by_keys:request.GET.o as list %}
{% else %}
{% define object_list as list %}
{% endif %}
{% if 'page' in request.GET %}
{% define request.GET.page as pagina %}
{% else %}
{% define '1' as pagina %}
{% endif %}
{% for prop in list %}
{% if forloop.counter > pagina|paginacao_limite_inferior and forloop.counter <= pagina|paginacao_limite_superior %}
<tr>
<td>
<a href="{% url 'sapl.materia:proposicao_detail' prop.pk %}">{{ prop.data_envio_setor|localtime|date:"d/m/Y H:i:s" }}</a>
</td>
<td>{{ prop.tipo.descricao }}</td>
<td>{{ prop.descricao }}</td>
<td>{{ prop.autor }}</td>
<td>
<a href="{% url 'sapl.materia:revisao-proposicao-setor' prop.pk %}" class="btn btn-sm btn-primary" title="{% trans 'Revisar Proposição' %}">
<i class="fas fa-pencil-alt"></i> {% trans "Revisar" %}
</a>
<a href="{% url 'sapl.materia:proposicao_detail' prop.pk %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Ver Detalhes' %}">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% include 'paginacao.html'%}
{% endblock %}

7
sapl/templates/materia/proposicao_base.html

@ -27,6 +27,13 @@
<i class="fas fa-check-circle"></i>
<span>Incorporadas</span>
</a>
{% if perms.materia.detail_proposicao_em_revisao_setor %}
<a href="{% url 'sapl.materia:proposicao-pendente-setor' %}"
class="proposicao-nav-item{% if '/proposicao/pendente-setor/' in request.path %} active{% endif %}">
<i class="fas fa-building"></i>
<span>Revisão Setor</span>
</a>
{% endif %}
<a href="{% url 'sapl.materia:historico-proposicao' %}"
class="proposicao-nav-item{% if request.path == '/proposicao/historico' %} active{% endif %}">
<i class="fas fa-history"></i>

84
sapl/templates/materia/proposicao_detail.html

@ -64,6 +64,8 @@
.prop-status-badge.incorporada { background: #e8f5e9; color: #388e3c; }
.prop-status-badge.devolvida { background: #fff3e0; color: #f57c00; }
.prop-status-badge.cancelada { background: #f5f5f5; color: #999; }
.prop-status-badge.aguardando-setor { background: #f3e5f5; color: #7b1fa2; }
.prop-status-badge.revisada-setor { background: #e0f2f1; color: #00695c; }
.prop-alert {
padding: 14px 18px;
@ -283,6 +285,10 @@
<span class="prop-status-badge incorporada">Incorporada</span>
{% elif object.data_envio %}
<span class="prop-status-badge aguardando">Aguardando Recebimento</span>
{% elif revisao_setor_legislativo and object.data_envio_setor and not object.data_retorno_setor %}
<span class="prop-status-badge aguardando-setor">Aguardando Revisão do Setor</span>
{% elif revisao_setor_legislativo and object.data_retorno_setor %}
<span class="prop-status-badge revisada-setor">Revisada pelo Setor</span>
{% else %}
<span class="prop-status-badge elaboracao">Em Elaboração</span>
{% endif %}
@ -306,23 +312,63 @@
</div>
{% endif %}
{% if revisao_setor_legislativo and object.data_retorno_setor and not object.data_envio %}
<div class="prop-alert info">
<strong><i class="fa fa-check-circle"></i> {% trans "Proposição revisada pelo Setor Legislativo" %}</strong>
{% if object.observacao_setor %}
{{ object.observacao_setor }}
{% else %}
{% trans "O setor legislativo revisou esta proposição. Você pode agora enviar ao protocolo ou reenviar ao setor." %}
{% endif %}
</div>
{% endif %}
{% if revisao_setor_legislativo and object.data_envio_setor and not object.data_retorno_setor and not object.data_envio %}
<div class="prop-alert info">
<i class="fa fa-clock-o"></i> {% trans "Proposição enviada ao Setor Legislativo e aguardando revisão." %}
</div>
{% endif %}
<!-- Ações principais -->
<div class="prop-actions-bar">
{% if not object.data_envio %}
<a href="{% url 'sapl.materia:onlyoffice_editor' object.pk %}" class="btn btn-primary">
<i class="fa fa-edit"></i> {% trans "Editar Documento" %}
</a>
{% if object.texto_original or object.texto_articulado.exists %}
<a href="{{ view.detail_url }}?action=send" class="btn btn-success">
<i class="fa fa-paper-plane"></i> {% trans "Enviar Proposição" %}
</a>
{% if object.data_envio_setor and not object.data_retorno_setor %}
{# Proposição está com o setor - autor pode retirar #}
{% if user in object.autor.operadores.all or user.is_superuser %}
<a href="{{ view.detail_url }}?action=return_from_setor" class="btn btn-outline-warning">
<i class="fa fa-undo"></i> {% trans "Retirar do Setor" %}
</a>
{% endif %}
{# Setor Legislativo pode revisar #}
{% if is_setor_legislativo %}
<a href="{% url 'sapl.materia:revisao-proposicao-setor' object.pk %}" class="btn btn-primary">
<i class="fa fa-pencil-square-o"></i> {% trans "Revisar Proposição" %}
</a>
{% endif %}
{% else %}
{# Proposição em elaboração ou revisada pelo setor #}
{% if user in object.autor.operadores.all or user.is_superuser %}
<a href="{% url 'sapl.materia:onlyoffice_editor' object.pk %}" class="btn btn-primary">
<i class="fa fa-edit"></i> {% trans "Editar Documento" %}
</a>
{% if object.texto_original or object.texto_articulado.exists %}
<a href="{{ view.detail_url }}?action=send" class="btn btn-success">
<i class="fa fa-paper-plane"></i> {% trans "Enviar Proposição" %}
</a>
{% if revisao_setor_legislativo %}
<a href="{{ view.detail_url }}?action=send_setor" class="btn btn-info">
<i class="fa fa-building"></i> {% trans "Enviar ao Setor Legislativo" %}
</a>
{% endif %}
{% endif %}
<a href="{{ view.update_url }}" class="btn btn-outline-secondary">
<i class="fa fa-pencil"></i> {% trans "Editar Dados" %}
</a>
<a href="{{ view.delete_url }}" class="btn btn-outline-danger">
<i class="fa fa-trash"></i> {% trans "Excluir" %}
</a>
{% endif %}
{% endif %}
<a href="{{ view.update_url }}" class="btn btn-outline-secondary">
<i class="fa fa-pencil"></i> {% trans "Editar Dados" %}
</a>
<a href="{{ view.delete_url }}" class="btn btn-outline-danger">
<i class="fa fa-trash"></i> {% trans "Excluir" %}
</a>
{% else %}
<a href="#" onclick="window.open('{% url 'sapl.materia:recibo-proposicao' object.pk %}','Recibo','width=1100, height=600, scrollbars=yes'); return false;" class="btn btn-outline-primary">
<i class="fa fa-file-text-o"></i> {% trans "Ver Recibo" %}
@ -379,6 +425,18 @@
<span>{{ object.data_devolucao|date:"d/m/Y H:i" }}</span>
</div>
{% endif %}
{% if revisao_setor_legislativo and object.data_envio_setor %}
<div class="prop-meta-item">
<label>{% trans "Envio ao Setor" %}</label>
<span>{{ object.data_envio_setor|date:"d/m/Y H:i" }}</span>
</div>
{% endif %}
{% if revisao_setor_legislativo and object.data_retorno_setor %}
<div class="prop-meta-item">
<label>{% trans "Retorno do Setor" %}</label>
<span>{{ object.data_retorno_setor|date:"d/m/Y H:i" }}</span>
</div>
{% endif %}
<div class="prop-meta-item">
<label>{% trans "Documento" %}</label>
<span>

80
sapl/templates/materia/revisar_proposicao_setor.html

@ -0,0 +1,80 @@
{% extends "materia/proposicao_base.html" %}
{% load i18n %}
{% load tz %}
{% load crispy_forms_tags %}
{% block base_content %}
<div class="proposicao-content">
<div class="proposicao-header">
<h2>{% trans "Revisar Proposição" %}</h2>
<p class="text-muted">{{ proposicao.tipo }} — {{ proposicao.descricao|truncatechars:80 }}</p>
</div>
<!-- Informações da proposição -->
<div class="card mb-3">
<div class="card-body">
<div class="row">
<div class="col-md-3">
<strong>{% trans "Tipo" %}</strong><br>
{{ proposicao.tipo }}
</div>
<div class="col-md-3">
<strong>{% trans "Autor" %}</strong><br>
{{ proposicao.autor }}
</div>
<div class="col-md-3">
<strong>{% trans "Envio ao Setor" %}</strong><br>
{{ proposicao.data_envio_setor|date:"d/m/Y H:i" }}
</div>
<div class="col-md-3">
<strong>{% trans "Documento" %}</strong><br>
{% if proposicao.texto_original %}
<i class="fa fa-check-circle" style="color: #28a745;"></i> {% trans "Criado" %}
{% else %}
<i class="fa fa-times-circle" style="color: #dc3545;"></i> {% trans "Não criado" %}
{% endif %}
</div>
</div>
</div>
</div>
<!-- Ementa -->
<div class="card mb-3">
<div class="card-header">
<strong>{% trans "Ementa" %}</strong>
</div>
<div class="card-body">
{{ proposicao.descricao }}
</div>
</div>
<!-- Ações do documento -->
<div class="card mb-3">
<div class="card-header">
<strong>{% trans "Documento" %}</strong>
</div>
<div class="card-body text-center">
{% if proposicao.texto_original %}
<a href="{% url 'sapl.materia:onlyoffice_editor' proposicao.pk %}" class="btn btn-primary mr-2">
<i class="fa fa-edit"></i> {% trans "Editar com OnlyOffice" %}
</a>
<a href="{% url 'sapl.materia:proposicao_texto' proposicao.pk %}" class="btn btn-outline-secondary" target="_blank">
<i class="fa fa-download"></i> {% trans "Baixar Documento" %}
</a>
{% else %}
<p class="text-muted">{% trans "Nenhum documento anexado." %}</p>
{% endif %}
</div>
</div>
<!-- Formulário de revisão -->
<div class="card mb-3">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% crispy form %}
</form>
</div>
</div>
</div>
{% endblock %}
Loading…
Cancel
Save