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', 'assinatura_ata',
'estatisticas_acesso_normas', 'estatisticas_acesso_normas',
'escolher_numero_materia_proposicao', 'escolher_numero_materia_proposicao',
'revisao_setor_legislativo',
'tramitacao_origem_fixa', 'tramitacao_origem_fixa',
'tramitacao_materia', 'tramitacao_materia',
'ordenacao_pesquisa_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=_( verbose_name=_(
'Indicar número da matéria a ser gerada na proposição?'), 'Indicar número da matéria a ser gerada na proposição?'),
choices=YES_NO_CHOICES, default=False) 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 # MÓDULO MATÉRIA LEGISLATIVA
# Linha 1 ------------------ # Linha 1 ------------------

65
sapl/materia/forms.py

@ -2334,6 +2334,71 @@ class DevolverProposicaoForm(forms.ModelForm):
return self.instance 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): class ConfirmarProposicaoForm(ProposicaoForm):
tipo_readonly = forms.CharField( 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') 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( usuario_envio = models.ForeignKey(
get_settings_auth_user_model(), get_settings_auth_user_model(),
verbose_name=_('Usuário Responsável pelo Envio'), verbose_name=_('Usuário Responsável pelo Envio'),
@ -1057,6 +1093,8 @@ class Proposicao(models.Model):
_('Pode acessar detalhes de uma proposição devolvida.')), _('Pode acessar detalhes de uma proposição devolvida.')),
('detail_proposicao_incorporada', ('detail_proposicao_incorporada',
_('Pode acessar detalhes de uma proposição 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): def __str__(self):
@ -1123,7 +1161,9 @@ class HistoricoProposicao(models.Model):
STATUS_PROPOSICAO = Choices(('E', 'ENVIADA', _('Enviada')), STATUS_PROPOSICAO = Choices(('E', 'ENVIADA', _('Enviada')),
('R', 'RECEBIDA', _('Recebida')), ('R', 'RECEBIDA', _('Recebida')),
('T', 'RETORNADA', _('Retornada')), ('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, proposicao = models.ForeignKey(Proposicao,
verbose_name=_('Proposição'), 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) proposicao = get_object_or_404(Proposicao, pk=pk)
# Verifica se o usuário tem permissão para editar # 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 = ( can_edit = (
not proposicao.data_envio and 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) # 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) proposicao = get_object_or_404(Proposicao, pk=pk)
# Verifica se o usuário tem permissão # 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.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
messages.error(request, 'Você não tem permissão para editar esta proposição.') 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, NumeracaoCrud, OrgaoCrud, OrigemCrud,
PrimeiraTramitacaoEmLoteView, ProposicaoCrud, PrimeiraTramitacaoEmLoteView, ProposicaoCrud,
ProposicaoDevolvida, ProposicaoPendente, ProposicaoDevolvida, ProposicaoPendente,
ProposicaoPendenteSetor,
ProposicaoRecebida, ProposicaoTaView, ProposicaoRecebida, ProposicaoTaView,
ReceberProposicao, ReciboProposicaoView, ReceberProposicao, ReciboProposicaoView,
RegimeTramitacaoCrud, RelatoriaCrud, RegimeTramitacaoCrud, RelatoriaCrud,
@ -26,7 +27,7 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
TramitacaoEmLoteView, UnidadeTramitacaoCrud, TramitacaoEmLoteView, UnidadeTramitacaoCrud,
proposicao_texto, recuperar_materia, proposicao_texto, recuperar_materia,
ExcluirTramitacaoEmLoteView, ExcluirTramitacaoEmLoteView,
RetornarProposicao, RetornarProposicao, RevisarProposicaoSetor,
MateriaPesquisaSimplesView, MateriaPesquisaSimplesView,
DespachoInicialMultiCreateView, DespachoInicialMultiCreateView,
get_zip_docacessorios, get_pdf_docacessorios, get_zip_completo, get_pdf_completo, get_zip_docacessorios, get_pdf_docacessorios, get_zip_completo, get_pdf_completo,
@ -231,6 +232,10 @@ urlpatterns_proposicao = [
name='proposicao-recebida'), name='proposicao-recebida'),
url(r'^proposicao/devolvida/', ProposicaoDevolvida.as_view(), url(r'^proposicao/devolvida/', ProposicaoDevolvida.as_view(),
name='proposicao-devolvida'), 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) # 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$', url(r'^proposicao/confirmar/P(?P<hash>[0-9A-Fa-f]+)/(?P<pk>\d+)/onlyoffice/editor$',
onlyoffice_confirmar_editor, onlyoffice_confirmar_editor,

143
sapl/materia/views.py

@ -49,6 +49,7 @@ from sapl.materia.forms import (AnexadaForm, AutoriaForm, AutoriaMultiCreateForm
ConfirmarProposicaoForm, DevolverProposicaoForm, ConfirmarProposicaoForm, DevolverProposicaoForm,
DespachoInicialCreateForm, LegislacaoCitadaForm, DespachoInicialCreateForm, LegislacaoCitadaForm,
MateriaPesquisaSimplesForm, OrgaoForm, ProposicaoForm, MateriaPesquisaSimplesForm, OrgaoForm, ProposicaoForm,
RevisaoSetorForm,
TipoProposicaoForm, TramitacaoForm, TramitacaoUpdateForm, ConfigEtiquetaMateriaLegislativaForms) TipoProposicaoForm, TramitacaoForm, TramitacaoUpdateForm, ConfigEtiquetaMateriaLegislativaForms)
from sapl.norma.models import LegislacaoCitada from sapl.norma.models import LegislacaoCitada
from sapl.parlamentares.models import ( from sapl.parlamentares.models import (
@ -613,6 +614,73 @@ class ProposicaoRecebida(PermissionRequiredMixin, ListView):
return context 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): class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView):
app_label = sapl.protocoloadm.apps.AppConfig.label app_label = sapl.protocoloadm.apps.AppConfig.label
template_name = "materia/receber_proposicao.html" template_name = "materia/receber_proposicao.html"
@ -884,7 +952,8 @@ class ProposicaoCrud(Crud):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['subnav_template_name'] = '' 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>' % ( context['title'] = '%s <small>(%s)</small>' % (
self.object, self.object.autor) self.object, self.object.autor)
@ -893,6 +962,17 @@ class ProposicaoCrud(Crud):
context['proposicao'] = Proposicao.objects.get( context['proposicao'] = Proposicao.objects.get(
pk=self.kwargs['pk'] 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 return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -935,6 +1015,8 @@ class ProposicaoCrud(Crud):
str(p.pk)) str(p.pk))
p.data_devolucao = None p.data_devolucao = None
p.data_envio_setor = None
p.data_retorno_setor = None
p.data_envio = timezone.now() p.data_envio = timezone.now()
p.save() p.save()
HistoricoProposicao.objects.create( HistoricoProposicao.objects.create(
@ -1000,6 +1082,46 @@ class ProposicaoCrud(Crud):
messages.success(request, _( messages.success(request, _(
'Proposição Retornada com sucesso.')) '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: if msg_error:
messages.error(request, msg_error) messages.error(request, msg_error)
@ -1021,17 +1143,22 @@ class ProposicaoCrud(Crud):
return self.handle_no_permission() return self.handle_no_permission()
if not p.autor.operadores.filter(id=request.user.id).exists(): if not p.autor.operadores.filter(id=request.user.id).exists():
if not p.data_envio and not p.data_devolucao: # Allow Setor Legislativo users to access proposições sent to them
raise Http404() 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() raise Http404()
elif p.data_devolucao and not request.user.has_perm('materia.detail_proposicao_devolvida'):
if p.data_envio and not p.data_recebimento \ raise Http404()
elif p.data_envio and not p.data_recebimento \
and not request.user.has_perm('materia.detail_proposicao_enviada'): and not request.user.has_perm('materia.detail_proposicao_enviada'):
raise Http404() raise Http404()
elif p.data_envio and p.data_recebimento \
if p.data_envio and p.data_recebimento \
and not request.user.has_perm('materia.detail_proposicao_incorporada'): and not request.user.has_perm('materia.detail_proposicao_incorporada'):
raise Http404() raise Http404()

2
sapl/rules/__init__.py

@ -67,6 +67,7 @@ SGVP_GROUP_GERAL = _("Operador Geral")
SGVP_GROUP_AUTOR = _("Autor") SGVP_GROUP_AUTOR = _("Autor")
SGVP_GROUP_VOTANTE = _("Votante") SGVP_GROUP_VOTANTE = _("Votante")
SGVP_GROUP_PRESIDENTE_MESA = _("Presidente da Mesa Diretora") 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 # TODO - funcionalidade ainda não existe mas está aqui para efeito de anotação
SGVP_GROUP_LOGIN_SOCIAL = _("Usuários com Login Social") SGVP_GROUP_LOGIN_SOCIAL = _("Usuários com Login Social")
@ -88,6 +89,7 @@ SGVP_GROUPS = [
SGVP_GROUP_AUTOR, SGVP_GROUP_AUTOR,
SGVP_GROUP_VOTANTE, SGVP_GROUP_VOTANTE,
SGVP_GROUP_PRESIDENTE_MESA, SGVP_GROUP_PRESIDENTE_MESA,
SGVP_GROUP_SETOR_LEGISLATIVO,
SGVP_GROUP_LOGIN_SOCIAL, SGVP_GROUP_LOGIN_SOCIAL,
SGVP_GROUP_ANONYMOUS, 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_norma import rules_group_norma
from sapl.rules.group_painel import rules_group_painel from sapl.rules.group_painel import rules_group_painel
from sapl.rules.group_protocolo import rules_group_protocolo 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_sessao import rules_group_sessao
from sapl.rules.group_votante import rules_group_votante 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_norma['rules'] +
rules_group_sessao['rules'] + rules_group_sessao['rules'] +
rules_group_painel['rules'] + rules_group_painel['rules'] +
rules_group_setor_legislativo['rules'] +
rules_group_login_social['rules']) rules_group_login_social['rules'])
rules_patterns = [ rules_patterns = [
@ -99,6 +101,7 @@ rules_patterns = [
rules_group_autor, rules_group_autor,
rules_group_votante, rules_group_votante,
rules_group_presidente_mesa, rules_group_presidente_mesa,
rules_group_setor_legislativo,
rules_group_anonymous, # anotação para validação do teste de rules rules_group_anonymous, # anotação para validação do teste de rules
rules_group_login_social # TODO não implementado rules_group_login_social # TODO não implementado

1
sapl/templates/base/layouts.yaml

@ -34,6 +34,7 @@ AppConfig:
{% trans 'Módulo Proposições' %}: {% trans 'Módulo Proposições' %}:
- sequencia_numeracao_proposicao receber_recibo_proposicao proposicao_incorporacao_obrigatoria escolher_numero_materia_proposicao - sequencia_numeracao_proposicao receber_recibo_proposicao proposicao_incorporacao_obrigatoria escolher_numero_materia_proposicao
- revisao_setor_legislativo
{% trans 'Módulo Matéria Legislativa' %}: {% trans 'Módulo Matéria Legislativa' %}:
- tramitacao_origem_fixa:5 tramitacao_materia:4 ordenacao_pesquisa_materia - 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> <i class="fas fa-check-circle"></i>
<span>Incorporadas</span> <span>Incorporadas</span>
</a> </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' %}" <a href="{% url 'sapl.materia:historico-proposicao' %}"
class="proposicao-nav-item{% if request.path == '/proposicao/historico' %} active{% endif %}"> class="proposicao-nav-item{% if request.path == '/proposicao/historico' %} active{% endif %}">
<i class="fas fa-history"></i> <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.incorporada { background: #e8f5e9; color: #388e3c; }
.prop-status-badge.devolvida { background: #fff3e0; color: #f57c00; } .prop-status-badge.devolvida { background: #fff3e0; color: #f57c00; }
.prop-status-badge.cancelada { background: #f5f5f5; color: #999; } .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 { .prop-alert {
padding: 14px 18px; padding: 14px 18px;
@ -283,6 +285,10 @@
<span class="prop-status-badge incorporada">Incorporada</span> <span class="prop-status-badge incorporada">Incorporada</span>
{% elif object.data_envio %} {% elif object.data_envio %}
<span class="prop-status-badge aguardando">Aguardando Recebimento</span> <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 %} {% else %}
<span class="prop-status-badge elaboracao">Em Elaboração</span> <span class="prop-status-badge elaboracao">Em Elaboração</span>
{% endif %} {% endif %}
@ -306,23 +312,63 @@
</div> </div>
{% endif %} {% 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 --> <!-- Ações principais -->
<div class="prop-actions-bar"> <div class="prop-actions-bar">
{% if not object.data_envio %} {% if not object.data_envio %}
<a href="{% url 'sapl.materia:onlyoffice_editor' object.pk %}" class="btn btn-primary"> {% if object.data_envio_setor and not object.data_retorno_setor %}
<i class="fa fa-edit"></i> {% trans "Editar Documento" %} {# Proposição está com o setor - autor pode retirar #}
</a> {% if user in object.autor.operadores.all or user.is_superuser %}
{% if object.texto_original or object.texto_articulado.exists %} <a href="{{ view.detail_url }}?action=return_from_setor" class="btn btn-outline-warning">
<a href="{{ view.detail_url }}?action=send" class="btn btn-success"> <i class="fa fa-undo"></i> {% trans "Retirar do Setor" %}
<i class="fa fa-paper-plane"></i> {% trans "Enviar Proposição" %} </a>
</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 %} {% 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 %} {% 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"> <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" %} <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> <span>{{ object.data_devolucao|date:"d/m/Y H:i" }}</span>
</div> </div>
{% endif %} {% 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"> <div class="prop-meta-item">
<label>{% trans "Documento" %}</label> <label>{% trans "Documento" %}</label>
<span> <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