From 9ad216eceea9d24a0e94851808575e92bedf153f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Rodrigues?= Date: Fri, 12 Apr 2019 08:01:16 -0300 Subject: [PATCH] =?UTF-8?q?Implementa=20op=C3=A7=C3=B5es=20de=20anexar=20e?= =?UTF-8?q?m=20Documentos=20Administrativos=20(#2630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adicionar Adicionar Anexado no mód. Doc. Adm. Alterações iniciais - Anexados em Lote * Adicionar Adicionar Anexado em Lote mód. Doc. Adm. Corrigir permissão * Add classe customizada na api para classe Anexado * Update sapl/rules/map_rules.py Co-Authored-By: rjoao * Refatorar * Evitar anexo cíclico - Anexado em Lote * Evitar anexo ciclico - Anexado * Corrigir marcar/desmarcar todos * Atualizar * Corrigir mensagem de erro - ObjectDoesNotExist * Adiciona migração * Corrigir Editar Anexado * Adicionar migrations * Adicionar link documento - Docs p/ Anexar em Lote --- sapl/api/views.py | 16 +- sapl/materia/views.py | 2 +- sapl/protocoloadm/forms.py | 122 ++++++++++++++- .../migrations/0018_auto_20190314_1532.py | 35 +++++ sapl/protocoloadm/models.py | 42 +++++ sapl/protocoloadm/urls.py | 7 +- sapl/protocoloadm/views.py | 146 +++++++++++++++++- sapl/rules/map_rules.py | 1 + .../migrations/0036_auto_20190409_1452.py | 21 +++ .../migrations/0037_merge_20190411_1548.py | 16 ++ sapl/templates/protocoloadm/anexado_list.html | 13 ++ .../protocoloadm/em_lote/anexado.html | 5 +- sapl/templates/protocoloadm/layouts.yaml | 13 ++ sapl/templates/protocoloadm/subnav.yaml | 2 + 14 files changed, 431 insertions(+), 10 deletions(-) create mode 100644 sapl/protocoloadm/migrations/0018_auto_20190314_1532.py create mode 100644 sapl/sessao/migrations/0036_auto_20190409_1452.py create mode 100644 sapl/sessao/migrations/0037_merge_20190411_1548.py create mode 100644 sapl/templates/protocoloadm/anexado_list.html diff --git a/sapl/api/views.py b/sapl/api/views.py index 74a7f8d04..2dd2ef64f 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -26,7 +26,7 @@ from sapl.materia.models import Proposicao, TipoMateriaLegislativa,\ MateriaLegislativa, Tramitacao from sapl.parlamentares.models import Parlamentar from sapl.protocoloadm.models import DocumentoAdministrativo,\ - DocumentoAcessorioAdministrativo, TramitacaoAdministrativo + DocumentoAcessorioAdministrativo, TramitacaoAdministrativo, Anexado from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria @@ -488,6 +488,20 @@ class _TramitacaoAdministrativoViewSet(BusinessRulesNotImplementedMixin): return qs +@customize(Anexado) +class _AnexadoViewSet(BusinessRulesNotImplementedMixin): + + permission_classes = ( + _DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, ) + + def get_queryset(self): + qs = super().get_queryset() + + if self.request.user.is_anonymous(): + qs = qs.exclude(documento__restrito=True) + return qs + + @customize(SessaoPlenaria) class _SessaoPlenariaViewSet: diff --git a/sapl/materia/views.py b/sapl/materia/views.py index aae53ddac..464b505ee 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -2136,7 +2136,7 @@ class MateriaAnexadaEmLoteView(PermissionRequiredMixin, FilterView): anexada.data_desanexacao = data_desanexacao anexada.save() - msg = _('Materia(s) anexada(s).') + msg = _('Matéria(s) anexada(s).') messages.add_message(request, messages.SUCCESS, msg) sucess_url = reverse('sapl_index') + 'materia/' + kwargs['pk'] + '/anexada' diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index 915f62445..b47b093dc 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -29,7 +29,7 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, AnoNumeroOrderingFilter, from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo, DocumentoAdministrativo, Protocolo, TipoDocumentoAdministrativo, - TramitacaoAdministrativo) + TramitacaoAdministrativo, Anexado) TIPOS_PROTOCOLO = [('0', 'Recebido'), ('1', 'Enviado'), @@ -781,6 +781,126 @@ class TramitacaoAdmEditForm(TramitacaoAdmForm): return self.cleaned_data +class AnexadoForm(ModelForm): + + logger = logging.getLogger(__name__) + + tipo = forms.ModelChoiceField( + label='Tipo', + required=True, + queryset=TipoDocumentoAdministrativo.objects.all(), + empty_label='Selecione' + ) + + numero = forms.CharField(label='Número', required=True) + + ano = forms.CharField(label='Ano', required=True) + + def __init__(self, *args, **kwargs): + return super(AnexadoForm, self).__init__(*args, **kwargs) + + def clean(self): + super(AnexadoForm, self).clean() + + if not self.is_valid(): + return self.cleaned_data + + cleaned_data = self.cleaned_data + + data_anexacao = cleaned_data['data_anexacao'] + data_desanexacao = cleaned_data['data_desanexacao'] if cleaned_data['data_desanexacao'] else data_anexacao + + if data_anexacao > data_desanexacao: + self.logger.error("A data de anexação não pode ser posterior a data de desanexação.") + raise ValidationError(_("A data de anexação não pode ser posterior a data de desanexação.")) + try: + self.logger.info( + "Tentando obter objeto DocumentoAdministrativo (numero={}, ano={}, tipo={})." + .format(cleaned_data['numero'], cleaned_data['ano'], cleaned_data['tipo']) + ) + documento_anexado = DocumentoAdministrativo.objects.get( + numero=cleaned_data['numero'], + ano=cleaned_data['ano'], + tipo=cleaned_data['tipo'] + ) + except ObjectDoesNotExist: + msg = _('O {} {}/{} não existe no cadastro de documentos administrativos.' + .format(cleaned_data['tipo'], cleaned_data['numero'], cleaned_data['ano'])) + self.logger.error("O documento a ser anexado não existe no cadastro" + " de documentos administrativos") + raise ValidationError(msg) + + documento_principal = self.instance.documento_principal + if documento_principal == documento_anexado: + self.logger.error("O documento não pode ser anexado a si mesmo.") + raise ValidationError(_("O documento não pode ser anexado a si mesmo")) + + is_anexado = Anexado.objects.filter(documento_principal=documento_principal, + documento_anexado=documento_anexado + ).exclude(pk=self.instance.pk).exists() + + if is_anexado: + self.logger.error("Documento já se encontra anexado.") + raise ValidationError(_('Documento já se encontra anexado')) + + ciclico = False + anexados_anexado = Anexado.objects.filter(documento_principal=documento_anexado) + + while(anexados_anexado and not ciclico): + anexados = [] + + for anexo in anexados_anexado: + + if documento_principal == anexo.documento_anexado: + ciclico = True + else: + for a in Anexado.objects.filter(documento_principal=anexo.documento_anexado): + anexados.append(a) + + anexados_anexado = anexados + + if ciclico: + self.logger.error("O documento não pode ser anexado por um de seus anexados.") + raise ValidationError(_('O documento não pode ser anexado por um de seus anexados')) + + cleaned_data['documento_anexado'] = documento_anexado + + return cleaned_data + + def save(self, commit=False): + anexado = super(AnexadoForm, self).save(commit) + anexado.documento_anexado = self.cleaned_data['documento_anexado'] + anexado.save() + return anexado + + class Meta: + model = Anexado + fields = ['tipo', 'numero', 'ano', 'data_anexacao', 'data_desanexacao'] + + +class AnexadoEmLoteFilterSet(django_filters.FilterSet): + + class Meta(FilterOverridesMetaMixin): + model = DocumentoAdministrativo + fields = ['tipo', 'data'] + + def __init__(self, *args, **kwargs): + super(AnexadoEmLoteFilterSet, self).__init__(*args, **kwargs) + + self.filters['tipo'].label = 'Tipo de Documento*' + self.filters['data'].label = 'Data (Inicial - Final)*' + + row1 = to_row([('tipo', 12)]) + row2 = to_row([('data', 12)]) + + self.form.helper = SaplFormHelper() + self.form.helper.form_method = 'GET' + self.form.helper.layout = Layout( + Fieldset(_('Pesquisa de Documentos'), + row1, row2, form_actions(label='Pesquisar')) + ) + + class DocumentoAdministrativoForm(FileFieldCheckMixin, ModelForm): logger = logging.getLogger(__name__) diff --git a/sapl/protocoloadm/migrations/0018_auto_20190314_1532.py b/sapl/protocoloadm/migrations/0018_auto_20190314_1532.py new file mode 100644 index 000000000..20822400c --- /dev/null +++ b/sapl/protocoloadm/migrations/0018_auto_20190314_1532.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-14 18:32 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0017_merge_20190121_1552'), + ] + + operations = [ + migrations.CreateModel( + name='Anexado', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data_anexacao', models.DateField(verbose_name='Data Anexação')), + ('data_desanexacao', models.DateField(blank=True, null=True, verbose_name='Data Desanexação')), + ('documento_anexado', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documento_anexado_set', to='protocoloadm.DocumentoAdministrativo', verbose_name='Documento Anexado')), + ('documento_principal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documento_principal_set', to='protocoloadm.DocumentoAdministrativo', verbose_name='Documento Principal')), + ], + options={ + 'verbose_name': 'Anexado', + 'verbose_name_plural': 'Anexados', + }, + ), + migrations.AddField( + model_name='documentoadministrativo', + name='anexados', + field=models.ManyToManyField(blank=True, related_name='anexo_de', through='protocoloadm.Anexado', to='protocoloadm.DocumentoAdministrativo'), + ), + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index e335a5db1..f792a9cdb 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -170,6 +170,18 @@ class DocumentoAdministrativo(models.Model): verbose_name=_('Acesso Restrito'), blank=True) + anexados = models.ManyToManyField( + 'self', + blank=True, + through='Anexado', + symmetrical=False, + related_name='anexo_de', + through_fields=( + 'documento_principal', + 'documento_anexado' + ) + ) + class Meta: verbose_name = _('Documento Administrativo') verbose_name_plural = _('Documentos Administrativos') @@ -317,6 +329,36 @@ class TramitacaoAdministrativo(models.Model): } +@reversion.register() +class Anexado(models.Model): + documento_principal = models.ForeignKey( + DocumentoAdministrativo, related_name='documento_principal_set', + on_delete = models.CASCADE, + verbose_name=_('Documento Principal') + ) + documento_anexado = models.ForeignKey( + DocumentoAdministrativo, related_name='documento_anexado_set', + on_delete = models.CASCADE, + verbose_name=_('Documento Anexado') + ) + data_anexacao = models.DateField(verbose_name=_('Data Anexação')) + data_desanexacao = models.DateField( + blank=True, null=True, verbose_name=_('Data Desanexação') + ) + + class Meta: + verbose_name = _('Anexado') + verbose_name_plural = _('Anexados') + + def __str__(self): + return _('Anexado: %(documento_anexado_tipo)s %(documento_anexado_numero)s' + '/%(documento_anexado_ano)s\n') % { + 'documento_anexado_tipo': self.documento_anexado.tipo, + 'documento_anexado_numero': self.documento_anexado.numero, + 'documento_anexado_ano': self.documento_anexado.ano + } + + @reversion.register() class AcompanhamentoDocumento(models.Model): usuario = models.CharField(max_length=50) diff --git a/sapl/protocoloadm/urls.py b/sapl/protocoloadm/urls.py index 67d5edb65..e5925204d 100644 --- a/sapl/protocoloadm/urls.py +++ b/sapl/protocoloadm/urls.py @@ -21,7 +21,8 @@ from sapl.protocoloadm.views import (AcompanhamentoDocumentoView, atualizar_numero_documento, doc_texto_integral, DesvincularDocumentoView, - DesvincularMateriaView) + DesvincularMateriaView, + AnexadoCrud, DocumentoAnexadoEmLoteView) from .apps import AppConfig @@ -30,6 +31,7 @@ app_name = AppConfig.name urlpatterns_documento_administrativo = [ url(r'^docadm/', include(DocumentoAdministrativoCrud.get_urls() + + AnexadoCrud.get_urls() + TramitacaoAdmCrud.get_urls() + DocumentoAcessorioAdministrativoCrud.get_urls())), @@ -38,6 +40,9 @@ urlpatterns_documento_administrativo = [ url(r'^docadm/texto_integral/(?P\d+)$', doc_texto_integral, name='doc_texto_integral'), + + url(r'^docadm/(?P\d+)/anexado_em_lote', DocumentoAnexadoEmLoteView.as_view(), + name='anexado_em_lote'), ] urlpatterns_protocolo = [ diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index 1f094c7aa..f6af28206 100755 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -17,7 +17,7 @@ from django.http.response import HttpResponseRedirect from django.shortcuts import redirect from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.views.generic import ListView, CreateView +from django.views.generic import ListView, CreateView, UpdateView from django.views.generic.base import RedirectView, TemplateView from django.views.generic.edit import FormView from django_filters.views import FilterView @@ -27,7 +27,8 @@ from sapl.base.email_utils import do_envia_email_confirmacao from sapl.base.models import Autor, CasaLegislativa from sapl.base.signals import tramitacao_signal from sapl.comissoes.models import Comissao -from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination +from sapl.crud.base import (Crud, CrudAux, MasterDetailCrud, make_pagination, + RP_LIST, RP_DETAIL) from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.materia.views import gerar_pdf_impressos from sapl.parlamentares.models import Legislatura, Parlamentar @@ -44,10 +45,11 @@ from .forms import (AcompanhamentoDocumentoForm, AnularProcoloAdmForm, TramitacaoAdmEditForm, TramitacaoAdmForm, DesvincularDocumentoForm, DesvincularMateriaForm, filtra_tramitacao_adm_destino_and_status, - filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status) + filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status, + AnexadoForm, AnexadoEmLoteFilterSet) from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo, DocumentoAdministrativo, StatusTramitacaoAdministrativo, - TipoDocumentoAdministrativo, TramitacaoAdministrativo) + TipoDocumentoAdministrativo, TramitacaoAdministrativo, Anexado) TipoDocumentoAdministrativoCrud = CrudAux.build( @@ -941,6 +943,142 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin, return self.render_to_response(context) +class AnexadoCrud(MasterDetailCrud): + model = Anexado + parent_field = 'documento_principal' + help_topic = 'documento_anexado' + public = [RP_LIST, RP_DETAIL] + + class BaseMixin(MasterDetailCrud.BaseMixin): + list_field_names = ['documento_anexado', 'data_anexacao'] + + class CreateView(MasterDetailCrud.CreateView): + form_class = AnexadoForm + + class UpdateView(MasterDetailCrud.UpdateView): + form_class = AnexadoForm + + def get_initial(self): + initial = super(UpdateView, self).get_initial() + initial['tipo'] = self.object.documento_anexado.tipo.id + initial['numero'] = self.object.documento_anexado.numero + initial['ano'] = self.object.documento_anexado.ano + return initial + + class DetailView(MasterDetailCrud.DetailView): + + @property + def layout_key(self): + return 'AnexadoDetail' + + +class DocumentoAnexadoEmLoteView(PermissionRequiredMixin, FilterView): + filterset_class = AnexadoEmLoteFilterSet + template_name = 'protocoloadm/em_lote/anexado.html' + permission_required = ('protocoloadm.add_anexado', ) + + def get_context_data(self, **kwargs): + context = super( + DocumentoAnexadoEmLoteView,self + ).get_context_data(**kwargs) + + context['root_pk'] = self.kwargs['pk'] + + context['subnav_template_name'] = 'protocoloadm/subnav.yaml' + + context['title'] = _('Documentos Anexados em Lote') + + # Verifica se os campos foram preenchidos + if not self.request.GET.get('tipo', " "): + msg =_('Por favor, selecione um tipo de documento.') + messages.add_message(self.request, messages.ERROR, msg) + + if not self.request.GET.get('data_0', " ") or not self.request.GET.get('data_1', " "): + msg =_('Por favor, preencha as datas.') + messages.add_message(self.request, messages.ERROR, msg) + + return context + + if not self.request.GET.get('data_0', " ") or not self.request.GET.get('data_1', " "): + msg =_('Por favor, preencha as datas.') + messages.add_message(self.request, messages.ERROR, msg) + return context + + qr = self.request.GET.copy() + context['temp_object_list'] = context['object_list'].order_by( + 'numero', '-ano' + ) + + context['object_list'] = [] + for obj in context['temp_object_list']: + if not obj.pk == int(context['root_pk']): + documento_principal = DocumentoAdministrativo.objects.get(id=context['root_pk']) + documento_anexado = obj + is_anexado = Anexado.objects.filter(documento_principal=documento_principal, + documento_anexado=documento_anexado).exists() + if not is_anexado: + ciclico = False + anexados_anexado = Anexado.objects.filter(documento_principal=documento_anexado) + + while anexados_anexado and not ciclico: + anexados = [] + + for anexo in anexados_anexado: + + if documento_principal == anexo.documento_anexado: + ciclico = True + else: + for a in Anexado.objects.filter(documento_principal=anexo.documento_anexado): + anexados.append(a) + + anexados_anexado = anexados + + if not ciclico: + context['object_list'].append(obj) + + context['numero_res'] = len(context['object_list']) + + context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + + return context + + def post(self, request, *args, **kwargs): + marcados = request.POST.getlist('documento_id') + + if len(marcados) == 0: + msg =_('Nenhum documento foi selecionado') + messages.add_message(request, messages.ERROR, msg) + return self.get(request, self.kwargs) + + data_anexacao = datetime.strptime( + request.POST['data_anexacao'], "%d/%m/%Y" + ).date() + + if request.POST['data_desanexacao'] == '': + data_desanexacao = None + else: + data_desanexacao = datetime.strptime( + request.POST['data_desanexacao'], "%d/%m/%Y" + ).date() + + principal = DocumentoAdministrativo.objects.get(pk = kwargs['pk']) + for documento in DocumentoAdministrativo.objects.filter(id__in = marcados): + anexado = Anexado() + anexado.documento_principal = principal + anexado.documento_anexado = documento + anexado.data_anexacao = data_anexacao + anexado.data_desanexacao = data_desanexacao + anexado.save() + + msg = _('Documento(s) anexado(s).') + messages.add_message(request, messages.SUCCESS, msg) + + success_url = reverse('sapl_index') + 'docadm/' + kwargs['pk'] + '/anexado' + return HttpResponseRedirect(success_url) + + class TramitacaoAdmCrud(MasterDetailCrud): model = TramitacaoAdministrativo parent_field = 'documento' diff --git a/sapl/rules/map_rules.py b/sapl/rules/map_rules.py index d7f3e0b48..a82ef2c3f 100644 --- a/sapl/rules/map_rules.py +++ b/sapl/rules/map_rules.py @@ -60,6 +60,7 @@ rules_group_administrativo = { 'can_access_impressos'], __perms_publicas__), # TODO: tratar em sapl.api a questão de ostencivo e restritivo (protocoloadm.DocumentoAdministrativo, __base__, set()), + (protocoloadm.Anexado, __base__, set()), (protocoloadm.DocumentoAcessorioAdministrativo, __base__, set()), (protocoloadm.TramitacaoAdministrativo, __base__, set()), ] diff --git a/sapl/sessao/migrations/0036_auto_20190409_1452.py b/sapl/sessao/migrations/0036_auto_20190409_1452.py new file mode 100644 index 000000000..b174dc877 --- /dev/null +++ b/sapl/sessao/migrations/0036_auto_20190409_1452.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-09 17:52 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0035_resumoordenacao_decimo_quarto'), + ] + + operations = [ + migrations.AlterField( + model_name='expedientesessao', + name='sessao_plenaria', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expedientesessao_set', to='sessao.SessaoPlenaria'), + ), + ] diff --git a/sapl/sessao/migrations/0037_merge_20190411_1548.py b/sapl/sessao/migrations/0037_merge_20190411_1548.py new file mode 100644 index 000000000..2478af6b7 --- /dev/null +++ b/sapl/sessao/migrations/0037_merge_20190411_1548.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-11 18:48 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0036_auto_20190410_0836'), + ('sessao', '0036_auto_20190409_1452'), + ] + + operations = [ + ] diff --git a/sapl/templates/protocoloadm/anexado_list.html b/sapl/templates/protocoloadm/anexado_list.html new file mode 100644 index 000000000..1dfe166c6 --- /dev/null +++ b/sapl/templates/protocoloadm/anexado_list.html @@ -0,0 +1,13 @@ +{% extends "crud/list.html" %} +{% load i18n %} +{% load common_tags %} + +{% block more_buttons %} + + {% if perms|get_add_perm:view %} + + {% trans "Adicionar Anexado em Lote" %} + + {% endif %} + +{% endblock more_buttons %} \ No newline at end of file diff --git a/sapl/templates/protocoloadm/em_lote/anexado.html b/sapl/templates/protocoloadm/em_lote/anexado.html index a8ddf6712..dfb0714ca 100644 --- a/sapl/templates/protocoloadm/em_lote/anexado.html +++ b/sapl/templates/protocoloadm/em_lote/anexado.html @@ -54,7 +54,7 @@ - {{documento.tipo.sigla}} {{documento.numero}}/{{documento.ano}} - {{documento.tipo.descricao}} + {{documento.tipo.sigla}} {{documento.numero}}/{{documento.ano}} - {{documento.tipo.descricao}} {% endfor %} @@ -63,6 +63,7 @@ +
{% else %} @@ -75,7 +76,7 @@ {% block extra_js %}