diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index edbeed810..5af4ef880 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -798,8 +798,8 @@ class AnexadaForm(ModelForm): ano=cleaned_data['ano'], tipo=cleaned_data['tipo']) except ObjectDoesNotExist: - msg = _('A {} {}/{} não existe no cadastro de matérias legislativas.' - .format(cleaned_data['tipo'], cleaned_data['numero'], cleaned_data['ano'])) + msg = _('A Matéria Legislativa a ser anexada (numero={}, ano={}, tipo={}) não existe no cadastro' + ' de matérias legislativas.'.format(cleaned_data['numero'], cleaned_data['ano'], cleaned_data['tipo'])) self.logger.error("A matéria a ser anexada não existe no cadastro" " de matérias legislativas.") raise ValidationError(msg) diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 8d7278e5f..eb827f7f9 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -2108,7 +2108,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) return self.get(request, self.kwargs) diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index 915f62445..d8dac6393 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,102 @@ 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 + + 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 Documento Administrativo a ser anexado (numero={}, ano={}, tipo={}) não existe no cadastro' + ' de documentos administrativos.'.format(cleaned_data['numero'], cleaned_data['ano'], cleaned_data['tipo'])) + 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 + ).exists() + + if is_anexado: + self.logger.error("Documento já se encontra anexado.") + raise ValidationError(_('Documento já se encontra anexado')) + + 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)' + self.form.fields['tipo'].required = True + self.form.fields['data'].required = True + + 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..14d76ff21 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,35 @@ 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 _('Principal: %(documento_principal)s' + ' - Anexada: %(documento_anexado)s') % { + 'documento_principal': self.documento_principal, + 'documento_anexado': self.documento_anexado + } + + @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 c400cf9a2..4835ab167 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,98 @@ 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.list_documentoadministrativo', ) ##d + + 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') + + if not self.filterset.form.is_valid(): + return context + + qr = self.request.GET.copy() + context['object_List'] = context['object_list'].order_by( + 'numero', '-ano' + ) + + 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) + return self.get(request, self.kwargs) + + 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/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/layouts.yaml b/sapl/templates/protocoloadm/layouts.yaml index b1bb2f31b..2ada51382 100644 --- a/sapl/templates/protocoloadm/layouts.yaml +++ b/sapl/templates/protocoloadm/layouts.yaml @@ -10,6 +10,8 @@ DocumentoAdministrativo: - assunto - interessado tramitacao - texto_integral + - documento_anexado_set__documento_principal|m2m_urlize_for_detail + - documento_principal_set__documento_anexado|m2m_urlize_for_detail {% trans 'Outras Informações' %}: - numero_externo - dias_prazo data_fim_prazo @@ -33,6 +35,17 @@ TramitacaoAdministrativo: - data_encaminhamento data_fim_prazo - texto +Anexado: + {% trans 'Documento Anexado' %}: + - tipo numero ano + - data_anexacao data_desanexacao + +AnexadoDetail: + {% trans 'Documento Anexado' %}: + - documento_principal|fk_urlize_for_detail + - documento_anexado|fk_urlize_for_detail + - data_anexacao data_desanexacao + Protocolo: {% trans 'Indentificação Documento' %}: - tipo_protocolo diff --git a/sapl/templates/protocoloadm/subnav.yaml b/sapl/templates/protocoloadm/subnav.yaml index ff4feb42e..5e0c3021b 100644 --- a/sapl/templates/protocoloadm/subnav.yaml +++ b/sapl/templates/protocoloadm/subnav.yaml @@ -1,6 +1,8 @@ {% load i18n common_tags %} - title: {% trans 'Início' %} url: documentoadministrativo_detail +- title: {% trans 'Anexado' %} + url: anexado_list - title: {% trans 'Tramitação' %} url: tramitacaoadministrativo_list - title: {% trans 'Documento Acessório' %}