diff --git a/requirements/base.txt b/requirements/base.txt index 94b4b40ce..26cd98e63 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,6 +1,6 @@ # Django # ------------------------------------------------------------------------------ -django==4.2.10 +django==4.2.11 django-admin-interface==0.26.1 django-admin-rangefilter==0.5.4 django-allauth==0.56.1 diff --git a/sme_ptrf_apps/__init__.py b/sme_ptrf_apps/__init__.py index 881140f56..037a4c6c7 100644 --- a/sme_ptrf_apps/__init__.py +++ b/sme_ptrf_apps/__init__.py @@ -1,4 +1,4 @@ -__version__ = "9.1.0" +__version__ = "9.2.0" __version_info__ = tuple( [ diff --git a/sme_ptrf_apps/conftest.py b/sme_ptrf_apps/conftest.py index 132cb053c..e07034427 100644 --- a/sme_ptrf_apps/conftest.py +++ b/sme_ptrf_apps/conftest.py @@ -34,7 +34,7 @@ AnalisePrestacaoContaFactory, AnaliseLancamentoPrestacaoContaFactory, SolicitacaoAcertoLancamentoFactory, ProcessoAssociacaoFactory, PrestacaoContaReprovadaNaoApresentacaoFactory, DemonstrativoFinanceiroFactory, - ItemResumoPorAcaoFactory, ItemDespesaFactory, ItemCreditoFactory, + ItemResumoPorAcaoFactory, ItemDespesaFactory, ItemCreditoFactory, ArquivoDownloadFactory ) from sme_ptrf_apps.users.fixtures.factories import ( UsuarioFactory, UnidadeEmSuporteFactory, GrupoAcessoFactory, VisaoFactory, @@ -63,8 +63,8 @@ TipoTransacaoFactory, ProcessoAssociacaoFactory, OcupanteCargoFactory, CargoComposicaoFactory, DemonstrativoFinanceiroFactory, ItemResumoPorAcaoFactory, ItemDespesaFactory, ItemCreditoFactory, TipoReceitaFactory, RelacaoBensFactory, - RelatorioRelacaoBensFactory, ItemRelatorioRelacaoDeBensFactory, - SolicitacaoEncerramentoContaAssociacaoFactory + RelatorioRelacaoBensFactory, ItemRelatorioRelacaoDeBensFactory, + SolicitacaoEncerramentoContaAssociacaoFactory, ArquivoDownloadFactory ] for factory in factories_to_register: @@ -834,8 +834,20 @@ def periodo_2021_2(periodo_2021_1): ) + @pytest.fixture -def periodo_2019_2(periodo): +def periodo_2019_1(): + return baker.make( + 'Periodo', + referencia='2019.1', + data_inicio_realizacao_despesas=date(2019, 1, 1), + data_fim_realizacao_despesas=date(2019, 5, 31), + periodo_anterior=None, + ) + + +@pytest.fixture +def periodo_2019_2(periodo_2019_1): return baker.make( 'Periodo', referencia='2019.2', @@ -844,7 +856,7 @@ def periodo_2019_2(periodo): data_prevista_repasse=date(2019, 6, 1), data_inicio_prestacao_contas=date(2020, 1, 1), data_fim_prestacao_contas=date(2020, 1, 10), - periodo_anterior=periodo + periodo_anterior=periodo_2019_1 ) @@ -2182,12 +2194,13 @@ def tag_ativa(): @pytest.fixture -def processo_associacao_123456_2019(associacao): +def processo_associacao_123456_2019(associacao, periodo_2019_1, periodo_2019_2): return baker.make( 'ProcessoAssociacao', associacao=associacao, numero_processo='123456', - ano='2019' + ano='2019', + periodos=[periodo_2019_1, periodo_2019_2], ) diff --git a/sme_ptrf_apps/core/admin.py b/sme_ptrf_apps/core/admin.py index 673662625..69fe59d14 100644 --- a/sme_ptrf_apps/core/admin.py +++ b/sme_ptrf_apps/core/admin.py @@ -510,11 +510,20 @@ class TagAdmin(admin.ModelAdmin): @admin.register(ProcessoAssociacao) class ProcessoAssociacaoAdmin(admin.ModelAdmin): - list_display = ('associacao', 'numero_processo', 'ano') + list_display = ('associacao', 'numero_processo', 'ano', 'periodos_str') search_fields = ('uuid', 'numero_processo', 'associacao__nome') list_filter = ('ano', 'associacao', 'associacao__unidade__tipo_unidade', 'associacao__unidade__dre') readonly_fields = ('uuid', 'id') + filter_horizontal = ('periodos',) + raw_id_fields = ('associacao',) + + def periodos_str(self, obj): + """ + Retorna uma string com as referências dos períodos associados ao ProcessoAssociacao. + """ + return ", ".join([periodo.referencia for periodo in obj.periodos.all()]) + periodos_str.short_description = "Períodos" @admin.register(ObservacaoConciliacao) class ObservacaoConciliacaoAdmin(admin.ModelAdmin): @@ -1050,7 +1059,7 @@ class AmbienteAdmin(admin.ModelAdmin): @admin.register(ArquivoDownload) class ArquivoDownloadAdmin(admin.ModelAdmin): - list_display = ('identificador', 'status', 'alterado_em', 'lido') + list_display = ('identificador', 'status', 'alterado_em', 'lido', 'informacoes') readonly_fields = ('uuid', 'id',) list_display_links = ('identificador',) diff --git a/sme_ptrf_apps/core/api/serializers/prestacao_conta_serializer.py b/sme_ptrf_apps/core/api/serializers/prestacao_conta_serializer.py index 8dd4d2e8d..b116d7b48 100644 --- a/sme_ptrf_apps/core/api/serializers/prestacao_conta_serializer.py +++ b/sme_ptrf_apps/core/api/serializers/prestacao_conta_serializer.py @@ -1,3 +1,5 @@ +from waffle import flag_is_active + from rest_framework import serializers from sme_ptrf_apps.core.models import PrestacaoConta, ObservacaoConciliacao @@ -41,7 +43,8 @@ def get_unidade_nome(self, obj): return obj.associacao.unidade.nome if obj.associacao and obj.associacao.unidade else '' def get_processo_sei(self, obj): - return get_processo_sei_da_prestacao(prestacao_contas=obj) + request = self.context.get('request', None) + return get_processo_sei_da_prestacao(prestacao_contas=obj, periodos_processo_sei=flag_is_active(request, 'periodos-processo-sei')) def get_periodo_uuid(self, obj): return obj.periodo.uuid if obj.periodo else '' @@ -119,7 +122,8 @@ def get_periodo_referencia(self, obj): return obj.periodo.referencia def get_processo_sei(self, obj): - return get_processo_sei_da_prestacao(prestacao_contas=obj) + request = self.context.get('request', None) + return get_processo_sei_da_prestacao(prestacao_contas=obj, periodos_processo_sei=flag_is_active(request, 'periodos-processo-sei')) def get_devolucao_ao_tesouro(self, obj): return obj.total_devolucao_ao_tesouro_str diff --git a/sme_ptrf_apps/core/api/serializers/processo_associacao_serializer.py b/sme_ptrf_apps/core/api/serializers/processo_associacao_serializer.py index 2085bbe10..efd3aa690 100644 --- a/sme_ptrf_apps/core/api/serializers/processo_associacao_serializer.py +++ b/sme_ptrf_apps/core/api/serializers/processo_associacao_serializer.py @@ -1,7 +1,9 @@ +from waffle import flag_is_active + from rest_framework import serializers -from sme_ptrf_apps.core.api.serializers import AssociacaoLookupSerializer -from sme_ptrf_apps.core.models import ProcessoAssociacao, Associacao +from sme_ptrf_apps.core.api.serializers import AssociacaoLookupSerializer, PeriodoLookUpSerializer +from sme_ptrf_apps.core.models import ProcessoAssociacao, Associacao, Periodo class ProcessoAssociacaoCreateSerializer(serializers.ModelSerializer): @@ -10,26 +12,105 @@ class ProcessoAssociacaoCreateSerializer(serializers.ModelSerializer): required=False, queryset=Associacao.objects.all() ) + periodos = serializers.SlugRelatedField( + many=True, + slug_field='uuid', + queryset=Periodo.objects.all(), + required=False + ) + + def validate(self, data): + request = self.context.get('request') + + flag_ativa = flag_is_active(request, 'periodos-processo-sei') + + if not flag_ativa and 'periodos' in data: + raise serializers.ValidationError({ + "periodos": "A feature flag 'periodos-processo-sei' está desligada. Não é possível fornecer 'periodos'." + }) + + if flag_ativa: + periodos = data.get('periodos', []) + if not periodos: + raise serializers.ValidationError({ + "periodos": "É necessário informar ao menos um período quando a feature 'periodos-processo-sei' está ativa." + }) + + ano_processo = data.get('ano') + if periodos and ano_processo: + for periodo in periodos: + ano_periodo = periodo.referencia.split('.')[0] + if ano_periodo != ano_processo: + raise serializers.ValidationError({ + "periodos": f"Todos os períodos devem estar no mesmo ano do campo 'ano' ({ano_processo})." + }) + + associacao = data.get('associacao') + periodos = data.get('periodos', []) + + # Verifica se os períodos estão sendo reutilizados na mesma associação + for periodo in periodos: + processos_existentes = ProcessoAssociacao.objects.filter( + associacao=associacao, periodos=periodo + ) + + # Se estamos atualizando, excluímos o objeto atual da verificação para evitar falso-positivo + if self.instance: + processos_existentes = processos_existentes.exclude(pk=self.instance.pk) + + if processos_existentes.exists(): + raise serializers.ValidationError({ + "periodos": f"O período {periodo.referencia} já está associado a outro ProcessoAssociacao para a associação {associacao}." + }) + + return data class Meta: model = ProcessoAssociacao - fields = ('uuid', 'associacao', 'numero_processo', 'ano',) + fields = ('uuid', 'associacao', 'numero_processo', 'ano', 'periodos') + + def create(self, validated_data): + periodos_data = validated_data.pop('periodos', []) + processo_associacao = ProcessoAssociacao.objects.create(**validated_data) + processo_associacao.periodos.set(periodos_data) + return processo_associacao + + def update(self, instance, validated_data): + periodos_data = validated_data.pop('periodos', None) + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + if periodos_data is not None: + instance.periodos.set(periodos_data) + return instance + class ProcessoAssociacaoRetrieveSerializer(serializers.ModelSerializer): + associacao = AssociacaoLookupSerializer() permite_exclusao = serializers.SerializerMethodField('get_permite_exclusao') tooltip_exclusao = serializers.SerializerMethodField('get_tooltip_exclusao') + periodos = PeriodoLookUpSerializer(many=True, read_only=True) class Meta: model = ProcessoAssociacao fields = ('uuid', 'associacao', 'numero_processo', 'ano', 'criado_em', 'alterado_em', - 'permite_exclusao', 'tooltip_exclusao') + 'permite_exclusao', 'tooltip_exclusao', 'periodos',) def get_tooltip_exclusao(self, obj): - if obj.e_o_ultimo_processo_do_ano_com_pcs_vinculada: - return "Não é possível excluir o número desse processo SEI, pois este já está vinculado a uma prestação de contas. Caso necessário, é possível editá-lo." + request = self.context.get('request', None) + + msg = "Não é possível excluir o número desse processo SEI, pois este já está vinculado a uma prestação de contas. Caso necessário, é possível editá-lo." + + if flag_is_active(request, 'periodos-processo-sei'): + return msg if obj.prestacoes_vinculadas_aos_periodos.exists() else "" else: - return "" + return msg if obj.e_o_ultimo_processo_do_ano_com_pcs_vinculada else "" def get_permite_exclusao(self, obj): - return not obj.e_o_ultimo_processo_do_ano_com_pcs_vinculada + request = self.context.get('request', None) + + if flag_is_active(request, 'periodos-processo-sei'): + return not obj.prestacoes_vinculadas_aos_periodos.exists() + else: + return not obj.e_o_ultimo_processo_do_ano_com_pcs_vinculada diff --git a/sme_ptrf_apps/core/api/views/analises_prestacoes_contas_viewset.py b/sme_ptrf_apps/core/api/views/analises_prestacoes_contas_viewset.py index c77e769be..ae446a3f4 100644 --- a/sme_ptrf_apps/core/api/views/analises_prestacoes_contas_viewset.py +++ b/sme_ptrf_apps/core/api/views/analises_prestacoes_contas_viewset.py @@ -523,3 +523,56 @@ def regerar_relatorio_apos_acertos(self, request): ) return Response({'mensagem': 'Arquivo na fila para processamento.'}, status=status.HTTP_200_OK) + + @action(detail=False, methods=['get'], url_path='regerar-previa-relatorio-apos-acertos', + permission_classes=[IsAuthenticated & PermissaoAPITodosComLeituraOuGravacao]) + def regerar_previa_relatorio_apos_acertos(self, request): + from sme_ptrf_apps.core.tasks import ( + gerar_previa_relatorio_apos_acertos_v2_async, + ) + + # Define a análise da prestação de contas + analise_prestacao_uuid = self.request.query_params.get('analise_prestacao_uuid') + + if analise_prestacao_uuid: + try: + analise_prestacao = AnalisePrestacaoConta.objects.get(uuid=analise_prestacao_uuid) + except AnalisePrestacaoConta.DoesNotExist: + erro = { + 'erro': 'Objeto não encontrado.', + 'mensagem': f"O objeto analise-prestacao-conta para o uuid {analise_prestacao_uuid} não foi encontrado na base." + } + logger.info('Erro: %r', erro) + return Response(erro, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + erro = { + 'erro': 'Ocorreu um erro!', + 'mensagem': f"{e}" + } + logger.info('Erro: %r', erro) + return Response(erro, status=status.HTTP_400_BAD_REQUEST) + else: + erro = { + 'erro': 'parametros_requeridos', + 'mensagem': 'É necessário enviar o uuid da analise.' + } + return Response(erro, status=status.HTTP_400_BAD_REQUEST) + + task_celery_geracao_relatorio_apos_acerto = TaskCelery.objects.create( + nome_task="regerar_previa_relatorio_apos_acertos_v2_async", + associacao=analise_prestacao.prestacao_conta.associacao, + periodo=analise_prestacao.prestacao_conta.periodo, + usuario=request.user + ) + + gerar_previa_relatorio_apos_acertos_v2_async.apply_async( + ( + task_celery_geracao_relatorio_apos_acerto.uuid, + analise_prestacao.prestacao_conta.associacao.uuid, + analise_prestacao.prestacao_conta.periodo.uuid, + request.user.username, + analise_prestacao.uuid + ), countdown=1 + ) + + return Response({'mensagem': 'Arquivo na fila para processamento.'}, status=status.HTTP_200_OK) diff --git a/sme_ptrf_apps/core/api/views/arquivos_download_viewset.py b/sme_ptrf_apps/core/api/views/arquivos_download_viewset.py index 7f76f539e..5366d4167 100644 --- a/sme_ptrf_apps/core/api/views/arquivos_download_viewset.py +++ b/sme_ptrf_apps/core/api/views/arquivos_download_viewset.py @@ -11,6 +11,7 @@ import mimetypes from django.http import HttpResponse from rest_framework.permissions import IsAuthenticated +from django.db.models import Q class ArquivosDownloadViewSet(mixins.ListModelMixin, @@ -35,7 +36,10 @@ def get_queryset(self): identificador = self.request.query_params.get('identificador') if identificador is not None: - qs = qs.filter(identificador__unaccent__icontains=identificador) + qs = qs.filter( + Q(identificador__unaccent__icontains=identificador) | + Q(informacoes__unaccent__icontains=identificador) + ) return qs diff --git a/sme_ptrf_apps/core/api/views/prestacoes_contas_viewset.py b/sme_ptrf_apps/core/api/views/prestacoes_contas_viewset.py index 4a723b958..dbdbfe275 100644 --- a/sme_ptrf_apps/core/api/views/prestacoes_contas_viewset.py +++ b/sme_ptrf_apps/core/api/views/prestacoes_contas_viewset.py @@ -392,7 +392,10 @@ def reabrir(self, request, uuid): @action(detail=True, methods=['patch'], permission_classes=[IsAuthenticated & PermissaoAPIApenasDreComGravacao]) def receber(self, request, uuid): - from sme_ptrf_apps.core.services.processos_services import trata_processo_sei_ao_receber_pc + from sme_ptrf_apps.core.services.processos_services import ( + trata_processo_sei_ao_receber_pc, + trata_processo_sei_ao_receber_pc_v2, + ) prestacao_conta = self.get_object() @@ -437,8 +440,12 @@ def receber(self, request, uuid): } return Response(response, status=status.HTTP_400_BAD_REQUEST) - trata_processo_sei_ao_receber_pc(prestacao_conta=prestacao_conta, - processo_sei=processo_sei, acao_processo_sei=acao_processo_sei) + if flag_is_active(request, "periodos-processo-sei"): + trata_processo_sei_ao_receber_pc_v2(prestacao_conta=prestacao_conta, + processo_sei=processo_sei, acao_processo_sei=acao_processo_sei) + else: + trata_processo_sei_ao_receber_pc(prestacao_conta=prestacao_conta, + processo_sei=processo_sei, acao_processo_sei=acao_processo_sei) prestacao_recebida = prestacao_conta.receber(data_recebimento=data_recebimento) @@ -900,7 +907,7 @@ def tabelas(self, _): @action(detail=False, url_path='nao-recebidas', permission_classes=[IsAuthenticated & PermissaoAPITodosComLeituraOuGravacao]) - def nao_recebidas(self, _): + def nao_recebidas(self, request): # Determina a DRE dre_uuid = self.request.query_params.get('associacao__unidade__dre__uuid') @@ -965,13 +972,14 @@ def nao_recebidas(self, _): periodo=periodo, filtro_nome=nome, filtro_tipo_unidade=tipo_unidade, - filtro_status=status_pc_list - ) + filtro_status=status_pc_list, + periodos_processo_sei=flag_is_active(request,'periodos-processo-sei') + ) return Response(result) @action(detail=False, url_path='todos-os-status', permission_classes=[IsAuthenticated & PermissaoAPITodosComLeituraOuGravacao]) - def todos_os_status(self, _): + def todos_os_status(self, request): # Determina a DRE dre_uuid = self.request.query_params.get('associacao__unidade__dre__uuid') @@ -1041,7 +1049,8 @@ def todos_os_status(self, _): filtro_nome=nome, filtro_tipo_unidade=tipo_unidade, filtro_por_status=status_pc_list, - filtro_por_devolucao_tesouro=devolucao_tesouro + filtro_por_devolucao_tesouro=devolucao_tesouro, + periodos_processo_sei=flag_is_active(request, 'periodos-processo-sei') ) return Response(result) diff --git a/sme_ptrf_apps/core/api/views/processos_associacao_viewset.py b/sme_ptrf_apps/core/api/views/processos_associacao_viewset.py index 5afe52918..0ad17a3ca 100644 --- a/sme_ptrf_apps/core/api/views/processos_associacao_viewset.py +++ b/sme_ptrf_apps/core/api/views/processos_associacao_viewset.py @@ -1,10 +1,14 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema, OpenApiParameter from rest_framework import mixins +from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import GenericViewSet from rest_framework.response import Response from rest_framework import status -from sme_ptrf_apps.core.api.serializers import ProcessoAssociacaoCreateSerializer, ProcessoAssociacaoRetrieveSerializer -from sme_ptrf_apps.core.models import ProcessoAssociacao +from sme_ptrf_apps.core.api.serializers import ProcessoAssociacaoCreateSerializer, ProcessoAssociacaoRetrieveSerializer, \ + PeriodoLookUpSerializer +from sme_ptrf_apps.core.models import ProcessoAssociacao, Periodo from sme_ptrf_apps.users.permissoes import PermissaoApiDre @@ -36,3 +40,42 @@ def destroy(self, request, *args, **kwargs): self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) + + @extend_schema( + # Define os parâmetros que a action aceita + parameters=[ + OpenApiParameter(name='associacao_uuid', description='UUID da Associação', required=True, + type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY), + OpenApiParameter(name='processo_uuid', + description='UUID do Processo de Associação (opcional para novos processos)', + required=False, type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY), + OpenApiParameter(name='ano', description='Ano dos Períodos a serem retornados', required=True, + type=OpenApiTypes.STR, location=OpenApiParameter.QUERY), + ], + responses={200: PeriodoLookUpSerializer(many=True)}, + description="Retorna todos os períodos do ano que podem ser vinculados ao processo, " + "considerando se é uma inclusão de novo processo ou uma atualização." + ) + @action(detail=False, methods=['get'], url_path='periodos-disponiveis') + def periodos_disponiveis(self, request): + associacao_uuid = request.query_params.get('associacao_uuid') + processo_uuid = request.query_params.get('processo_uuid') + ano = request.query_params.get('ano') + + if not associacao_uuid or not ano: + return Response({"erro": "Os parâmetros 'associacao_uuid' e 'ano' são obrigatórios."}, status=400) + + periodos_query = Periodo.objects.filter(referencia__startswith=ano) + + # Identifica todos os períodos já vinculados a processos para a associação especificada, + # Excluindo o próprio processo se um UUID foi fornecido. + processos_associacao_query = ProcessoAssociacao.objects.filter(associacao__uuid=associacao_uuid) + if processo_uuid: + processos_associacao_query = processos_associacao_query.exclude(uuid=processo_uuid) + + # Exclui os períodos já vinculados a outros processos da associação. + periodos_ja_vinculados = [uuid for uuid in processos_associacao_query.values_list('periodos__uuid', flat=True).distinct() if uuid is not None] + periodos_disponiveis = periodos_query.exclude(uuid__in=periodos_ja_vinculados) + + serializer = PeriodoLookUpSerializer(periodos_disponiveis, many=True) + return Response(serializer.data) diff --git a/sme_ptrf_apps/core/api/views/tipos_acerto_lancamento_viewset.py b/sme_ptrf_apps/core/api/views/tipos_acerto_lancamento_viewset.py index 35e17f5f6..ae45c7b9a 100644 --- a/sme_ptrf_apps/core/api/views/tipos_acerto_lancamento_viewset.py +++ b/sme_ptrf_apps/core/api/views/tipos_acerto_lancamento_viewset.py @@ -57,10 +57,12 @@ def destroy(self, request, *args, **kwargs): permission_classes=[IsAuthenticated & PermissaoApiDre]) def tabelas(self, request): aplicavel_despesas_periodos_anteriores = request.query_params.get('aplicavel_despesas_periodos_anteriores') + is_repasse = request.query_params.get('is_repasse') result = { "categorias": TipoAcertoLancamento.categorias(), - "agrupado_por_categorias": TipoAcertoLancamento.agrupado_por_categoria(aplicavel_despesas_periodos_anteriores) + "agrupado_por_categorias": TipoAcertoLancamento.agrupado_por_categoria( + aplicavel_despesas_periodos_anteriores, is_repasse) } return Response(result, status=status.HTTP_200_OK) diff --git a/sme_ptrf_apps/core/fixtures/factories/__init__.py b/sme_ptrf_apps/core/fixtures/factories/__init__.py index 64b99e5eb..abb55954d 100644 --- a/sme_ptrf_apps/core/fixtures/factories/__init__.py +++ b/sme_ptrf_apps/core/fixtures/factories/__init__.py @@ -18,4 +18,5 @@ from .solicitacao_acerto_lancamento_factory import * from .prestacao_conta_reprovada_nao_apresentacao_factory import * from .demonstrativo_financeiro_factory import * -from .tipo_conta_factory import * \ No newline at end of file +from .tipo_conta_factory import * +from .arquivo_download_factory import * diff --git a/sme_ptrf_apps/core/fixtures/factories/arquivo_download_factory.py b/sme_ptrf_apps/core/fixtures/factories/arquivo_download_factory.py new file mode 100644 index 000000000..cc9985067 --- /dev/null +++ b/sme_ptrf_apps/core/fixtures/factories/arquivo_download_factory.py @@ -0,0 +1,21 @@ +from factory import DjangoModelFactory, SubFactory, Sequence +from faker import Faker +from sme_ptrf_apps.core.models import ( + ArquivoDownload +) +from sme_ptrf_apps.users.fixtures.factories import UsuarioFactory + +fake = Faker("pt_BR") + + +class ArquivoDownloadFactory(DjangoModelFactory): + class Meta: + model = ArquivoDownload + + identificador = Sequence(lambda n: fake.unique.word()) + informacoes = Sequence(lambda n: fake.name()) + arquivo = None + status = ArquivoDownload.STATUS_CONCLUIDO + msg_erro = '' + lido = False + usuario = SubFactory(UsuarioFactory) diff --git a/sme_ptrf_apps/core/migrations/0382_arquivodownload_informacoes.py b/sme_ptrf_apps/core/migrations/0382_arquivodownload_informacoes.py new file mode 100644 index 000000000..ee2b87001 --- /dev/null +++ b/sme_ptrf_apps/core/migrations/0382_arquivodownload_informacoes.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.10 on 2024-03-18 09:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0381_alter_notificacao_categoria"), + ] + + operations = [ + migrations.AddField( + model_name="arquivodownload", + name="informacoes", + field=models.TextField(blank=True, default="", verbose_name="Informações"), + ), + ] diff --git a/sme_ptrf_apps/core/migrations/0382_processoassociacao_periodos.py b/sme_ptrf_apps/core/migrations/0382_processoassociacao_periodos.py new file mode 100644 index 000000000..7eb2ccc05 --- /dev/null +++ b/sme_ptrf_apps/core/migrations/0382_processoassociacao_periodos.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.10 on 2024-03-19 06:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0381_alter_notificacao_categoria"), + ] + + operations = [ + migrations.AddField( + model_name="processoassociacao", + name="periodos", + field=models.ManyToManyField( + blank=True, related_name="processos", to="core.periodo" + ), + ), + ] diff --git a/sme_ptrf_apps/core/migrations/0383_merge_20240322_1517.py b/sme_ptrf_apps/core/migrations/0383_merge_20240322_1517.py new file mode 100644 index 000000000..a4d8bd291 --- /dev/null +++ b/sme_ptrf_apps/core/migrations/0383_merge_20240322_1517.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.11 on 2024-03-22 15:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0382_arquivodownload_informacoes"), + ("core", "0382_processoassociacao_periodos"), + ] + + operations = [] diff --git a/sme_ptrf_apps/core/models/arquivos_download.py b/sme_ptrf_apps/core/models/arquivos_download.py index 072611440..619272e5a 100644 --- a/sme_ptrf_apps/core/models/arquivos_download.py +++ b/sme_ptrf_apps/core/models/arquivos_download.py @@ -22,6 +22,7 @@ class ArquivoDownload(ModeloBase): ) identificador = models.CharField("Nome do arquivo", max_length=200, default='') + informacoes = models.TextField('Informações', blank=True, default='') arquivo = models.FileField(null=True, verbose_name='Arquivo') status = models.CharField( 'status', diff --git a/sme_ptrf_apps/core/models/proccessos_associacao.py b/sme_ptrf_apps/core/models/proccessos_associacao.py index 7ac1ee570..db70bacdd 100644 --- a/sme_ptrf_apps/core/models/proccessos_associacao.py +++ b/sme_ptrf_apps/core/models/proccessos_associacao.py @@ -15,6 +15,8 @@ class ProcessoAssociacao(ModeloBase): ano = models.CharField('Ano', max_length=4, blank=True, default="") + periodos = models.ManyToManyField('Periodo', related_name='processos', blank=True) + class Meta: verbose_name = "Processo de prestação de contas" verbose_name_plural = "07.1) Processos de prestação de contas" @@ -28,6 +30,14 @@ def by_associacao_periodo(cls, associacao, periodo): processos = cls.objects.filter(associacao=associacao, ano=ano) return processos.last().numero_processo if processos.exists() else "" + @classmethod + def by_associacao_periodo_v2(cls, associacao, periodo): + """Usado quando a feature flag periodos-processo-sei estiver ativa. + Retorna o primeiro processo da associação que tenha o periodo dentre seus periodos vinculados.""" + processos = cls.objects.filter(associacao=associacao, periodos=periodo) + return processos.first().numero_processo if processos.exists() else "" + + @classmethod def ultimo_processo_do_ano_por_associacao(cls, associacao, ano): processos = cls.objects.filter(associacao=associacao, ano=ano).order_by('criado_em') @@ -42,10 +52,18 @@ def prestacoes_vinculadas(self): periodo__data_inicio_realizacao_despesas__range=[inicio, fim]) return pcs + @property + def prestacoes_vinculadas_aos_periodos(self): + from sme_ptrf_apps.core.models import PrestacaoConta + pcs = PrestacaoConta.objects.filter(associacao=self.associacao, + periodo__in=self.periodos.all()) + return pcs + @property def e_o_ultimo_processo_do_ano_com_pcs_vinculada(self): ultimo_processo = ProcessoAssociacao.ultimo_processo_do_ano_por_associacao( associacao=self.associacao, ano=self.ano) return (self == ultimo_processo) and self.prestacoes_vinculadas.exists() + auditlog.register(ProcessoAssociacao) diff --git a/sme_ptrf_apps/core/models/tipo_acerto_lancamento.py b/sme_ptrf_apps/core/models/tipo_acerto_lancamento.py index 94f123420..5bc63f4f1 100644 --- a/sme_ptrf_apps/core/models/tipo_acerto_lancamento.py +++ b/sme_ptrf_apps/core/models/tipo_acerto_lancamento.py @@ -47,16 +47,28 @@ class TipoAcertoLancamento(ModeloIdNome): ativo = models.BooleanField('Ativo', default=True) @classmethod - def agrupado_por_categoria(cls, aplicavel_despesas_periodos_anteriores=False): + def agrupado_por_categoria(cls, aplicavel_despesas_periodos_anteriores=False, is_repasse=False): from sme_ptrf_apps.core.services import TipoAcertoLancamentoService + + categorias_a_ignorar = None + + if is_repasse: + categorias_a_ignorar = [ + TipoAcertoLancamento.CATEGORIA_EXCLUSAO_LANCAMENTO + ] + if aplicavel_despesas_periodos_anteriores: - FILTERED_CATEGORIA_CHOICES = ( - (cls.CATEGORIA_CONCILIACAO_LANCAMENTO, cls.CATEGORIA_NOMES[cls.CATEGORIA_CONCILIACAO_LANCAMENTO]), - (cls.CATEGORIA_DESCONCILIACAO_LANCAMENTO, cls.CATEGORIA_NOMES[cls.CATEGORIA_DESCONCILIACAO_LANCAMENTO]), - ) - return TipoAcertoLancamentoService.agrupado_por_categoria(FILTERED_CATEGORIA_CHOICES) - else: - return TipoAcertoLancamentoService.agrupado_por_categoria(cls.CATEGORIA_CHOICES) + # Apenas as categorias de conciliacao e desconciliacao devem ser retornadas + + categorias_a_ignorar = [ + TipoAcertoLancamento.CATEGORIA_DEVOLUCAO, + TipoAcertoLancamento.CATEGORIA_EDICAO_LANCAMENTO, + TipoAcertoLancamento.CATEGORIA_EXCLUSAO_LANCAMENTO, + TipoAcertoLancamento.CATEGORIA_AJUSTES_EXTERNOS, + TipoAcertoLancamento.CATEGORIA_SOLICITACAO_ESCLARECIMENTO + ] + + return TipoAcertoLancamentoService.agrupado_por_categoria(cls.CATEGORIA_CHOICES, categorias_a_ignorar) @classmethod def categorias(cls): diff --git a/sme_ptrf_apps/core/services/arquivo_download_service.py b/sme_ptrf_apps/core/services/arquivo_download_service.py index 328597957..4af62bb13 100644 --- a/sme_ptrf_apps/core/services/arquivo_download_service.py +++ b/sme_ptrf_apps/core/services/arquivo_download_service.py @@ -4,10 +4,11 @@ from django.core.files import File -def gerar_arquivo_download(username, identificador): +def gerar_arquivo_download(username, identificador, informacoes=''): usuario = get_user_model().objects.get(username=username) obj_arquivo_download = ArquivoDownload.objects.create( identificador=identificador, + informacoes=informacoes, arquivo=None, status=ArquivoDownload.STATUS_EM_PROCESSAMENTO, msg_erro="", diff --git a/sme_ptrf_apps/core/services/prestacao_contas_services.py b/sme_ptrf_apps/core/services/prestacao_contas_services.py index 2af917faf..10e925331 100644 --- a/sme_ptrf_apps/core/services/prestacao_contas_services.py +++ b/sme_ptrf_apps/core/services/prestacao_contas_services.py @@ -360,8 +360,10 @@ def totaliza_info_acoes(info_acoes): def lista_prestacoes_de_conta_nao_recebidas( dre, periodo, - filtro_nome=None, filtro_tipo_unidade=None, filtro_status=[] + filtro_nome=None, filtro_tipo_unidade=None, filtro_status=[], + periodos_processo_sei=False ): + associacoes_da_dre = Associacao.get_associacoes_ativas_no_periodo(periodo=periodo, dre=dre).order_by( 'unidade__tipo_unidade', 'unidade__nome') @@ -394,9 +396,9 @@ def lista_prestacoes_de_conta_nao_recebidas( continue if prestacao_conta: - processo_sei = get_processo_sei_da_prestacao(prestacao_contas=prestacao_conta) + processo_sei = get_processo_sei_da_prestacao(prestacao_contas=prestacao_conta, periodos_processo_sei=periodos_processo_sei) else: - processo_sei = get_processo_sei_da_associacao_no_periodo(associacao=associacao, periodo=periodo) + processo_sei = get_processo_sei_da_associacao_no_periodo(associacao=associacao, periodo=periodo, periodos_processo_sei=periodos_processo_sei) info_prestacao = { 'periodo_uuid': f'{periodo.uuid}', @@ -425,7 +427,8 @@ def lista_prestacoes_de_conta_todos_os_status( filtro_nome=None, filtro_tipo_unidade=None, filtro_por_devolucao_tesouro=None, - filtro_por_status=[] + filtro_por_status=[], + periodos_processo_sei=False ): associacoes_da_dre = Associacao.get_associacoes_ativas_no_periodo( periodo=periodo, dre=dre).order_by('unidade__tipo_unidade', 'unidade__nome') @@ -460,11 +463,17 @@ def lista_prestacoes_de_conta_todos_os_status( if prestacao_conta and prestacao_conta.devolucoes_ao_tesouro_da_prestacao.exists(): continue + if prestacao_conta: + processo_sei = get_processo_sei_da_prestacao(prestacao_contas=prestacao_conta, periodos_processo_sei=periodos_processo_sei) + else: + processo_sei = get_processo_sei_da_associacao_no_periodo(associacao=associacao, periodo=periodo, periodos_processo_sei=periodos_processo_sei) + + info_prestacao = { 'periodo_uuid': f'{periodo.uuid}', 'data_recebimento': prestacao_conta.data_recebimento if prestacao_conta else None, 'data_ultima_analise': prestacao_conta.data_ultima_analise if prestacao_conta else None, - 'processo_sei': get_processo_sei_da_prestacao(prestacao_contas=prestacao_conta) if prestacao_conta else '', + 'processo_sei': processo_sei, 'status': prestacao_conta.status if prestacao_conta else PrestacaoConta.STATUS_NAO_APRESENTADA, 'tecnico_responsavel': prestacao_conta.tecnico_responsavel.nome if prestacao_conta and prestacao_conta.tecnico_responsavel else '', 'unidade_eol': associacao.unidade.codigo_eol, @@ -898,7 +907,8 @@ def documentos_de_despesa_por_conta_e_acao_no_periodo( 'despesas_impostos': DespesaImpostoSerializer(despesas_impostos, many=True, required=False).data if despesas_impostos else None, 'informacoes': despesa.tags_de_informacao, - 'informacoes_ordenamento': despesa.tags_de_informacao_concatenadas + 'informacoes_ordenamento': despesa.tags_de_informacao_concatenadas, + 'is_repasse': False } if com_ajustes: @@ -949,7 +959,8 @@ def documentos_de_despesa_por_conta_e_acao_no_periodo( 'houve_considerados_corretos_automaticamente': analise_lancamento.houve_considerados_corretos_automaticamente, } if analise_lancamento else None, 'informacoes': receita.tags_de_informacao, - 'informacoes_ordenamento': receita.tags_de_informacao_concatenadas + 'informacoes_ordenamento': receita.tags_de_informacao_concatenadas, + 'is_repasse': receita.tipo_receita.e_repasse if receita.tipo_receita else False } if com_ajustes: @@ -1041,22 +1052,6 @@ def marca_gasto_correto(gasto_uuid): minhas_solicitacoes = minha_analise_lancamento.solicitacoes_de_ajuste_da_analise.all() for solicitacao_acerto in minhas_solicitacoes: - # TODO Remover o bloco comentado após conclusão da mudança em solicitações de dev.tesouro - # if solicitacao_acerto.copiado: - # devolucao_ao_tesouro = solicitacao_acerto.devolucao_ao_tesouro - # if devolucao_ao_tesouro: - # logging.info(f'A solicitação de acerto {solicitacao_acerto.uuid} ' - # f'foi copiada de uma analise anterior, a devolucao ao tesouro ' - # f'{devolucao_ao_tesouro} NÃO será apagada.') - # else: - # devolucao_ao_tesouro = solicitacao_acerto.devolucao_ao_tesouro - # if devolucao_ao_tesouro: - # logging.info(f'A solicitação de acerto {solicitacao_acerto.uuid} ' - # f'NÃO foi copiada de uma analise anterior, a devolucao ao tesouro ' - # f'{devolucao_ao_tesouro} SERÁ apagada.') - # - # devolucao_ao_tesouro.delete() - logging.info(f'Apagando solicitação de acerto {solicitacao_acerto.uuid}.') solicitacao_acerto.delete() @@ -1181,20 +1176,6 @@ def __analisa_solicitacoes_acerto(solicitacoes_acerto, analise_lancamento, atual tipo_acerto = TipoAcertoLancamento.objects.get(uuid=solicitacao_acerto['tipo_acerto']) - # TODO Remover o bloco comentado após conclusão da mudança em solicitações de dev.tesouro - # devolucao_tesouro = None - # if analise_lancamento.tipo_lancamento == 'GASTO' and solicitacao_acerto['devolucao_tesouro']: - # logging.info(f'Criando devolução ao tesouro para a análise de lançamento {analise_lancamento.uuid}.') - # devolucao_tesouro = DevolucaoAoTesouro.objects.create( - # prestacao_conta=analise_lancamento.analise_prestacao_conta.prestacao_conta, - # tipo=TipoDevolucaoAoTesouro.objects.get(uuid=solicitacao_acerto['devolucao_tesouro']['tipo']), - # data=solicitacao_acerto['devolucao_tesouro']['data'], - # despesa=analise_lancamento.despesa, - # devolucao_total=solicitacao_acerto['devolucao_tesouro']['devolucao_total'], - # valor=solicitacao_acerto['devolucao_tesouro']['valor'], - # motivo=solicitacao_acerto['detalhamento'] - # ) - if not solicitacao_acerto['devolucao_tesouro'] or analise_lancamento.tipo_lancamento == 'GASTO': # Em atualizações em lote Apenas lançamentos do tipo gasto recebem ajustes de devolução ao tesouro logging.info(f'Criando solicitação de acerto para a análise de lançamento {analise_lancamento.uuid}.') @@ -1227,17 +1208,6 @@ def __analisa_solicitacoes_acerto(solicitacoes_acerto, analise_lancamento, atual for solicitacao_existente in analise_lancamento.solicitacoes_de_ajuste_da_analise.all(): if solicitacao_existente.uuid not in keep_solicitacoes: logging.info(f"A solicitação: {solicitacao_existente} será apagada.") - - # TODO Remover o bloco comentado após conclusão da mudança em solicitações de dev.tesouro - # devolucao_ao_tesouro = solicitacao_existente.devolucao_ao_tesouro - # if devolucao_ao_tesouro: - # if solicitacao_existente.copiado: - # logging.info(f"A solicitação: {solicitacao_existente} foi copiada de uma analise anterior, " - # f"a devolução NÃO será apagada") - # else: - # logging.info(f'Apagando devolução ao tesouro {devolucao_ao_tesouro.uuid}.') - # devolucao_ao_tesouro.delete() - solicitacao_existente.delete() diff --git a/sme_ptrf_apps/core/services/processos_services.py b/sme_ptrf_apps/core/services/processos_services.py index 558d90ff1..c8ac541f4 100644 --- a/sme_ptrf_apps/core/services/processos_services.py +++ b/sme_ptrf_apps/core/services/processos_services.py @@ -1,15 +1,28 @@ from ..models import ProcessoAssociacao -def get_processo_sei_da_prestacao(prestacao_contas): - return ProcessoAssociacao.by_associacao_periodo(associacao=prestacao_contas.associacao, - periodo=prestacao_contas.periodo) +def get_processo_sei_da_prestacao(prestacao_contas, periodos_processo_sei=False): + """Retorna o número do processo SEI vinculado a uma prestação de contas. + Se a feature flag periodos-processo-sei estiver ativa usa versão v2 do método.""" + if periodos_processo_sei: + return ProcessoAssociacao.by_associacao_periodo_v2(associacao=prestacao_contas.associacao, + periodo=prestacao_contas.periodo) + else: + return ProcessoAssociacao.by_associacao_periodo(associacao=prestacao_contas.associacao, + periodo=prestacao_contas.periodo) + + +def get_processo_sei_da_associacao_no_periodo(associacao, periodo, periodos_processo_sei=False): + """Retorna o número do processo SEI vinculado a uma associação e um período. + Se a feature flag periodos-processo-sei estiver ativa usa versão v2 do método.""" + if periodos_processo_sei: + return ProcessoAssociacao.by_associacao_periodo_v2(associacao=associacao, + periodo=periodo) + else: + return ProcessoAssociacao.by_associacao_periodo(associacao=associacao, + periodo=periodo) -def get_processo_sei_da_associacao_no_periodo(associacao, periodo): - return ProcessoAssociacao.by_associacao_periodo(associacao=associacao, - periodo=periodo) - def trata_processo_sei_ao_receber_pc(prestacao_conta, processo_sei, acao_processo_sei): ano = prestacao_conta.periodo.referencia[0:4] if acao_processo_sei == 'editar': @@ -19,4 +32,44 @@ def trata_processo_sei_ao_receber_pc(prestacao_conta, processo_sei, acao_process elif acao_processo_sei == 'incluir': ProcessoAssociacao.objects.create(associacao=prestacao_conta.associacao, ano=ano, numero_processo=processo_sei) - + +def trata_processo_sei_ao_receber_pc_v2(prestacao_conta, processo_sei, acao_processo_sei): + """ + Trata o número do processo SEI ao receber uma prestação de contas. + Versão 2 do método, chamada se feature flag periodos-processo-sei estiver ativa. + """ + + if acao_processo_sei == 'incluir': + # Verifica se já existe um processo associado a associação com o número do processo SEI informado. + processo_associacao = ProcessoAssociacao.objects.filter(associacao=prestacao_conta.associacao, + numero_processo=processo_sei).first() + if processo_associacao: + # O processo já existe então basta adicionar o período a ele. + if prestacao_conta.periodo in processo_associacao.periodos.all(): + # O período não deveria estar vinculado ao processo. + raise Exception(f"O período {prestacao_conta.periodo.referencia} já está vinculado ao processo {processo_sei}.") + processo_associacao.periodos.add(prestacao_conta.periodo) + else: + # O processo não existe então é criado um novo. + # Porém é necessário verificar se o período já está vinculado a outro processo. + processo_associacao = ProcessoAssociacao.objects.filter(associacao=prestacao_conta.associacao, + periodos=prestacao_conta.periodo).first() + if processo_associacao: + raise Exception(f"O período {prestacao_conta.periodo.referencia} já está vinculado ao processo {processo_associacao.numero_processo}.") + + processo_associacao = ProcessoAssociacao.objects.create( + associacao=prestacao_conta.associacao, + numero_processo=processo_sei, + ano=prestacao_conta.periodo.referencia[0:4] + ) + processo_associacao.periodos.add(prestacao_conta.periodo) + + return + + if acao_processo_sei == 'editar': + processo_associacao = ProcessoAssociacao.objects.filter(associacao=prestacao_conta.associacao, + periodos=prestacao_conta.periodo).first() + + processo_associacao.numero_processo = processo_sei + processo_associacao.save() + diff --git a/sme_ptrf_apps/core/services/tipos_acerto_lancamento_service.py b/sme_ptrf_apps/core/services/tipos_acerto_lancamento_service.py index a5b6746ec..82df50637 100644 --- a/sme_ptrf_apps/core/services/tipos_acerto_lancamento_service.py +++ b/sme_ptrf_apps/core/services/tipos_acerto_lancamento_service.py @@ -4,8 +4,9 @@ class TipoAcertoLancamentoAgrupadoPorCategoria: - def __init__(self, choices): + def __init__(self, choices, categorias_a_ignorar): self.choices = choices + self.categorias_a_ignorar = categorias_a_ignorar if categorias_a_ignorar is not None else [] self.__set_agrupamento() def __set_agrupamento(self): @@ -15,6 +16,9 @@ def __set_agrupamento(self): categoria_id = choice[0] categoria_nome = choice[1] + if categoria_id in self.categorias_a_ignorar: + continue + tipos_acertos_lancamentos = TipoAcertoLancamento.objects.filter( categoria=categoria_id).filter(ativo=True).values("id", "nome", "categoria", "ativo", "uuid") @@ -88,8 +92,9 @@ def __set_choices_json(self): class TipoAcertoLancamentoService: @classmethod - def agrupado_por_categoria(cls, choices): - return TipoAcertoLancamentoAgrupadoPorCategoria(choices=choices).agrupamento + def agrupado_por_categoria(cls, choices, categorias_a_ignorar=None): + return TipoAcertoLancamentoAgrupadoPorCategoria( + choices=choices, categorias_a_ignorar=categorias_a_ignorar).agrupamento @classmethod def categorias(cls, choices): diff --git a/sme_ptrf_apps/core/tasks/__init__.py b/sme_ptrf_apps/core/tasks/__init__.py index e4a9dd5b7..4551ec9a0 100644 --- a/sme_ptrf_apps/core/tasks/__init__.py +++ b/sme_ptrf_apps/core/tasks/__init__.py @@ -19,4 +19,4 @@ from .gerar_relacao_de_bens import gerar_relacao_bens_async from .calcular_prestacao_de_contas import calcular_prestacao_de_contas_async from .terminar_processo_pc import terminar_processo_pc_async - +from .gerar_previa_relatorio_apos_acertos_v2 import gerar_previa_relatorio_apos_acertos_v2_async diff --git a/sme_ptrf_apps/core/tasks/gerar_previa_relatorio_apos_acertos_v2.py b/sme_ptrf_apps/core/tasks/gerar_previa_relatorio_apos_acertos_v2.py new file mode 100644 index 000000000..e9510025c --- /dev/null +++ b/sme_ptrf_apps/core/tasks/gerar_previa_relatorio_apos_acertos_v2.py @@ -0,0 +1,105 @@ +from celery import shared_task, current_task +from celery.exceptions import MaxRetriesExceededError + +from sme_ptrf_apps.core.models.tasks_celery import TaskCelery +from sme_ptrf_apps.core.models.analise_prestacao_conta import AnalisePrestacaoConta +from sme_ptrf_apps.core.services.prestacao_conta_service import PrestacaoContaService +from sme_ptrf_apps.logging.loggers import ContextualLogger + +MAX_RETRIES = 3 + + +@shared_task( + bind=True, + autoretry_for=(Exception,), + retry_kwargs={'max_retries': MAX_RETRIES}, + retry_backoff=True, + retry_backoff_max=600, + retry_jitter=True, + time_limit=600, + soft_time_limit=300 +) +def gerar_previa_relatorio_apos_acertos_v2_async( + self, + id_task, + associacao_uuid, + periodo_uuid, + username="", + analise_pc_uuid=None +): + """ + Task para gerar o relatório após acertos da prestação de contas. + Versão 2: Para rodar com o novo processo de PC. + """ + # TODO: Após a migração para o novo processo de PC, renomear removendo o v2 e apagar o método antigo. + from sme_ptrf_apps.core.services.analise_prestacao_conta_service import criar_previa_relatorio_apos_acertos + + logger_rel_pos_acerto = ContextualLogger.get_logger( + __name__, + operacao='Prestação de Contas', + username=username, + task_id=str(id_task), + ) + tentativa = current_task.request.retries + 1 + + task = None + try: + task = TaskCelery.objects.get(uuid=id_task) + + task.registra_task_assincrona(self.request.id) + + # Apenas para informar que os logs não mais ficarão registrados na task. + task.grava_log_concatenado( + 'Iniciando a task gerar_previa_relatorio_apos_acertos_v2_async. Logs registrados apenas no Kibana.') + + pc_service = PrestacaoContaService( + periodo_uuid=periodo_uuid, + associacao_uuid=associacao_uuid, + username=username, + logger=logger_rel_pos_acerto + ) + + logger_rel_pos_acerto.info(f'Iniciando a geração prévia relatório após acertos da associacao.') + + if analise_pc_uuid: + """ + Em casos de regeração do PDF, a analise pc uuid sera passada como parametro + """ + + analise_prestacao_conta = AnalisePrestacaoConta.by_uuid(uuid=analise_pc_uuid) + else: + """ + Em casos de geração do PDF pelo fluxo normal, sera pego a ultima analise da PC + """ + + analise_prestacao_conta = pc_service.prestacao.analises_da_prestacao.order_by('id').last() + + if not analise_prestacao_conta: + raise Exception('Não foi possível encontrar a análise da prestação de conta.') + + logger_rel_pos_acerto.info(f'Análise da prestação de conta: {analise_prestacao_conta}') + + logger_rel_pos_acerto.info(f'Criando a prévia do relatório após acertos.') + criar_previa_relatorio_apos_acertos( + analise_prestacao_conta=analise_prestacao_conta, + usuario=pc_service.usuario.name + ) + logger_rel_pos_acerto.info(f'Prévia do relatório após acertos criado com sucesso.') + + task.registra_data_hora_finalizacao(f'Finalizada com sucesso a geração da prévia do relatório após acertos.') + except Exception as exc: + logger_rel_pos_acerto.error( + f'A tentativa {tentativa} de gerar a prévia do relatório após acerto falhou.', + exc_info=True, + stack_info=True + ) + if tentativa >= MAX_RETRIES: + mensagem_tentativas_excedidas = 'Tentativas de reprocessamento com falha excedidas para a prévia do relatório após acertos.' + logger_rel_pos_acerto.error(mensagem_tentativas_excedidas, exc_info=True, stack_info=True) + + if task: + task.registra_data_hora_finalizacao(mensagem_tentativas_excedidas) + + raise MaxRetriesExceededError(mensagem_tentativas_excedidas) + else: + raise exc diff --git a/sme_ptrf_apps/core/tests/tests_api_analises_prestacoes_contas/test_list_lancamentos.py b/sme_ptrf_apps/core/tests/tests_api_analises_prestacoes_contas/test_list_lancamentos.py index 2f9156554..9288e02ac 100644 --- a/sme_ptrf_apps/core/tests/tests_api_analises_prestacoes_contas/test_list_lancamentos.py +++ b/sme_ptrf_apps/core/tests/tests_api_analises_prestacoes_contas/test_list_lancamentos.py @@ -264,6 +264,7 @@ def monta_result_esperado(lancamentos_esperados, periodo, conta, inativa=False): } if lancamento["analise_lancamento"] else None, 'informacoes': retorna_tags_de_informacao(lancamento=lancamento), 'informacoes_ordenamento': retorna_tags_de_informacao_concatenadas(lancamento=lancamento), + 'is_repasse': False, } ) diff --git a/sme_ptrf_apps/core/tests/tests_api_analises_prestacoes_contas/test_regerar_previa_relatorio_apos_acertos.py b/sme_ptrf_apps/core/tests/tests_api_analises_prestacoes_contas/test_regerar_previa_relatorio_apos_acertos.py new file mode 100644 index 000000000..c4ed974e9 --- /dev/null +++ b/sme_ptrf_apps/core/tests/tests_api_analises_prestacoes_contas/test_regerar_previa_relatorio_apos_acertos.py @@ -0,0 +1,29 @@ +import json +import pytest +from rest_framework import status +from sme_ptrf_apps.core.models.tasks_celery import TaskCelery + +pytestmark = pytest.mark.django_db + + +def test_regerar_previa_relatorio_apos_acertos( + jwt_authenticated_client_a, + analise_prestacao_conta_2020_1_teste_analises_sem_versao, + conta_associacao_cartao, + conta_associacao_cheque +): + + analise_prestacao = analise_prestacao_conta_2020_1_teste_analises_sem_versao.uuid + url = f'/api/analises-prestacoes-contas/regerar-previa-relatorio-apos-acertos/?analise_prestacao_uuid={analise_prestacao}' + + response = jwt_authenticated_client_a.get(url, content_type='application/json') + + result = json.loads(response.content) + + resultado_esperado = { + "mensagem": "Arquivo na fila para processamento." + } + + assert response.status_code == status.HTTP_200_OK + assert resultado_esperado == result + assert TaskCelery.objects.filter(nome_task='regerar_previa_relatorio_apos_acertos_v2_async').exists() diff --git a/sme_ptrf_apps/core/tests/tests_api_arquivos_download/__init__.py b/sme_ptrf_apps/core/tests/tests_api_arquivos_download/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/core/tests/tests_api_arquivos_download/test_endpoints.py b/sme_ptrf_apps/core/tests/tests_api_arquivos_download/test_endpoints.py new file mode 100644 index 000000000..a86f6ca9a --- /dev/null +++ b/sme_ptrf_apps/core/tests/tests_api_arquivos_download/test_endpoints.py @@ -0,0 +1,157 @@ +import datetime +import json +import pytest +from rest_framework import status + +from sme_ptrf_apps.core.models import ArquivoDownload + +pytestmark = pytest.mark.django_db + + +def test_lista_arquivos_download(jwt_authenticated_client, arquivo_download_factory, usuario): + + arquivo_download_factory.create(usuario=usuario) + arquivo_download_factory.create(usuario=usuario) + + response = jwt_authenticated_client.get('/api/arquivos-download/') + result = response.json() + + assert len(result) == 2 + assert response.status_code == status.HTTP_200_OK + + +def test_lista_arquivos_download_do_usuario(jwt_authenticated_client, arquivo_download_factory, usuario): + + arquivo_download_factory.create(usuario=usuario) + arquivo_download_factory.create() + + response = jwt_authenticated_client.get('/api/arquivos-download/') + result = response.json() + + assert len(result) == 1 + assert response.status_code == status.HTTP_200_OK + + +def test_lista_arquivos_download_filtro_identificador(jwt_authenticated_client, arquivo_download_factory, usuario): + + arquivo_download_factory.create(usuario=usuario, identificador="teste") + arquivo_download_factory.create(usuario=usuario) + + response = jwt_authenticated_client.get('/api/arquivos-download/?identificador=tes') + result = response.json() + + assert len(result) == 1 + assert response.status_code == status.HTTP_200_OK + + +def test_lista_arquivos_download_filtro_informacoes(jwt_authenticated_client, arquivo_download_factory, usuario): + + arquivo_download_factory.create(usuario=usuario, informacoes="teste") + arquivo_download_factory.create(usuario=usuario) + + response = jwt_authenticated_client.get('/api/arquivos-download/?identificador=tes') + result = response.json() + + assert len(result) == 1 + assert response.status_code == status.HTTP_200_OK + + +def test_lista_arquivos_download_filtro_identificador_ou_informacoes(jwt_authenticated_client, arquivo_download_factory, usuario): + + arquivo_download_factory.create(usuario=usuario, informacoes="teste") + arquivo_download_factory.create(usuario=usuario, identificador="teste") + + response = jwt_authenticated_client.get('/api/arquivos-download/?identificador=tes') + result = response.json() + + assert len(result) == 2 + assert response.status_code == status.HTTP_200_OK + + +def test_lista_arquivos_download_filtro_status(jwt_authenticated_client, arquivo_download_factory, usuario): + + arquivo_download_factory.create(usuario=usuario, status='CONCLUIDO') + arquivo_download_factory.create(usuario=usuario, status='EM_PROCESSAMENTO') + + response = jwt_authenticated_client.get('/api/arquivos-download/?status=CONCLUIDO') + result = response.json() + + assert len(result) == 1 + assert response.status_code == status.HTTP_200_OK + + +def test_lista_arquivos_download_filtro_atualizacao(jwt_authenticated_client, arquivo_download_factory, usuario): + + arquivo_download_factory.create(usuario=usuario, status='CONCLUIDO') + arquivo_download_factory.create(usuario=usuario, status='EM_PROCESSAMENTO') + + response = jwt_authenticated_client.get(f'/api/arquivos-download/?ultima_atualizacao={datetime.date.today()}') + result = response.json() + + assert len(result) == 2 + assert response.status_code == status.HTTP_200_OK + + +def test_lista_arquivos_download_filtro_lido(jwt_authenticated_client, arquivo_download_factory, usuario): + + arquivo_download_factory.create(usuario=usuario, lido=True) + arquivo_download_factory.create(usuario=usuario, lido=False) + + response = jwt_authenticated_client.get(f'/api/arquivos-download/?lido=true') + result = response.json() + + assert len(result) == 1 + assert response.status_code == status.HTTP_200_OK + + +def test_deve_marcar_como_lido(jwt_authenticated_client, arquivo_download_factory, usuario): + arquivo = arquivo_download_factory.create(usuario=usuario) + assert arquivo.lido is False + + payload = { + "lido": True, + "uuid": f"{arquivo.uuid}" + } + + response = jwt_authenticated_client.put( + f'/api/arquivos-download/marcar-lido/', data=json.dumps(payload), content_type='application/json') + result = response.json() + + arquivo_alterado = ArquivoDownload.by_uuid(arquivo.uuid) + assert result["mensagem"] == "Arquivo atualizado com sucesso" + assert arquivo_alterado.lido is True + assert response.status_code == status.HTTP_200_OK + + +def test_deve_marcar_como_nao_lido(jwt_authenticated_client, arquivo_download_factory, usuario): + arquivo = arquivo_download_factory.create(usuario=usuario, lido=True) + + assert arquivo.lido is True + + payload = { + "lido": False, + "uuid": f"{arquivo.uuid}" + } + + response = jwt_authenticated_client.put( + f'/api/arquivos-download/marcar-lido/', data=json.dumps(payload), content_type='application/json') + result = response.json() + + arquivo_alterado = ArquivoDownload.by_uuid(arquivo.uuid) + assert result["mensagem"] == "Arquivo atualizado com sucesso" + assert arquivo_alterado.lido is False + assert response.status_code == status.HTTP_200_OK + + +def test_quantidade_nao_lidos(jwt_authenticated_client, arquivo_download_factory, usuario): + + arquivo_download_factory.create(usuario=usuario, status='CONCLUIDO') + arquivo_download_factory.create(usuario=usuario, status='CONCLUIDO') + arquivo_download_factory.create(usuario=usuario, status='CONCLUIDO', lido=True) + arquivo_download_factory.create(usuario=usuario, status='EM_PROCESSAMENTO') + + response = jwt_authenticated_client.get(f'/api/arquivos-download/quantidade-nao-lidos/') + result = response.json() + + assert result["quantidade_nao_lidos"] == 2 + assert response.status_code == status.HTTP_200_OK diff --git a/sme_ptrf_apps/core/tests/tests_api_associacoes/test_list_proccessos_associacao.py b/sme_ptrf_apps/core/tests/tests_api_associacoes/test_list_proccessos_associacao.py index f8bc66c9b..9f7514592 100644 --- a/sme_ptrf_apps/core/tests/tests_api_associacoes/test_list_proccessos_associacao.py +++ b/sme_ptrf_apps/core/tests/tests_api_associacoes/test_list_proccessos_associacao.py @@ -23,29 +23,10 @@ def test_list_processos_da_associacao(jwt_authenticated_client_a, associacao, pr content_type='application/json') result = json.loads(response.content) - esperado = [ - { - 'uuid': f'{processo_associacao_123456_2019.uuid}', - 'associacao': - { - 'id': processo_associacao_123456_2019.associacao.id, - 'nome': processo_associacao_123456_2019.associacao.nome, - 'data_de_encerramento': - { - 'data': None, - 'help_text': 'A associação deixará de ser exibida nos períodos posteriores à data de ' - 'encerramento informada.', - 'pode_editar_dados_associacao_encerrada': True - }, - }, - 'criado_em': processo_associacao_123456_2019.criado_em.isoformat("T"), - 'alterado_em': processo_associacao_123456_2019.alterado_em.isoformat("T"), - 'numero_processo': processo_associacao_123456_2019.numero_processo, - 'ano': processo_associacao_123456_2019.ano, - 'tooltip_exclusao': '', - 'permite_exclusao': True - }, - ] - assert response.status_code == status.HTTP_200_OK - assert result == esperado + assert len(result) == 1 + assert result[0]['associacao']['id'] == processo_associacao_123456_2019.associacao.id + assert result[0]['numero_processo'] == processo_associacao_123456_2019.numero_processo + assert result[0]['ano'] == processo_associacao_123456_2019.ano + assert any(periodo['referencia'] == '2019.1' for periodo in result[0]['periodos']) + assert any(periodo['referencia'] == '2019.2' for periodo in result[0]['periodos']) diff --git a/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_create.py b/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_create.py index 6c00adfd5..a8e4b8213 100644 --- a/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_create.py +++ b/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_create.py @@ -1,14 +1,16 @@ import json - import pytest + +from model_bakery import baker from rest_framework import status +from waffle.testutils import override_flag from sme_ptrf_apps.core.models import ProcessoAssociacao pytestmark = pytest.mark.django_db @pytest.fixture -def payload_processo_associacao(associacao): +def payload_processo_associacao_sem_periodos(associacao): payload = { 'associacao': str(associacao.uuid), 'numero_processo': "271170", @@ -17,12 +19,110 @@ def payload_processo_associacao(associacao): return payload -def test_create_processo_associacao_servidor(jwt_authenticated_client_a, associacao, payload_processo_associacao): +@pytest.fixture +def payload_processo_associacao_com_periodos(associacao, periodo): + payload = { + 'associacao': str(associacao.uuid), + 'numero_processo': "271170", + 'ano': '2019', + 'periodos': [str(periodo.uuid)] + } + return payload + + +@pytest.fixture +def payload_processo_associacao_com_periodos_de_outro_ano(associacao, periodo, periodo_2020_1): + payload = { + 'associacao': str(associacao.uuid), + 'numero_processo': "271170", + 'ano': '2019', + 'periodos': [str(periodo.uuid), str(periodo_2020_1.uuid)] + } + return payload + +@pytest.fixture +def processo_associacao_usando_o_mesmo_periodo(associacao, periodo): + return baker.make( + 'ProcessoAssociacao', + associacao=associacao, + numero_processo='123456', + ano='2019', + periodos=[periodo,], + ) + + +def test_create_processo_associacao_servidor_sem_periodos_com_flag_desligada(jwt_authenticated_client_a, associacao, + payload_processo_associacao_sem_periodos): response = jwt_authenticated_client_a.post( - '/api/processos-associacao/', data=json.dumps(payload_processo_associacao), content_type='application/json') + '/api/processos-associacao/', data=json.dumps(payload_processo_associacao_sem_periodos), content_type='application/json') assert response.status_code == status.HTTP_201_CREATED result = json.loads(response.content) assert ProcessoAssociacao.objects.filter(uuid=result['uuid']).exists() + + +def test_create_processo_associacao_servidor_sem_periodos_com_flag_ligada(jwt_authenticated_client_a, associacao, + payload_processo_associacao_sem_periodos): + with override_flag('periodos-processo-sei', active=True): + response = jwt_authenticated_client_a.post( + '/api/processos-associacao/', data=json.dumps(payload_processo_associacao_sem_periodos), content_type='application/json') + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +def test_create_processo_associacao_servidor_com_periodos_com_flag_ligada(jwt_authenticated_client_a, associacao, + payload_processo_associacao_com_periodos): + with override_flag('periodos-processo-sei', active=True): + response = jwt_authenticated_client_a.post( + '/api/processos-associacao/', data=json.dumps(payload_processo_associacao_com_periodos), + content_type='application/json') + + assert response.status_code == status.HTTP_201_CREATED + + result = json.loads(response.content) + + assert ProcessoAssociacao.objects.filter(uuid=result['uuid']).exists() + + # Assert se o processo foi criado com os periodos vinculados + processo = ProcessoAssociacao.objects.get(uuid=result['uuid']) + assert processo.periodos.count() == 1 + assert str(processo.periodos.first().uuid) == payload_processo_associacao_com_periodos['periodos'][0] + + +def test_create_processo_associacao_servidor_com_periodos_com_flag_desligada(jwt_authenticated_client_a, associacao, + payload_processo_associacao_com_periodos): + response = jwt_authenticated_client_a.post( + '/api/processos-associacao/', data=json.dumps(payload_processo_associacao_com_periodos), + content_type='application/json') + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +def test_create_processo_associacao_servidor_com_periodos_de_outro_ano(jwt_authenticated_client_a, associacao, + payload_processo_associacao_com_periodos_de_outro_ano): + with override_flag('periodos-processo-sei', active=True): + response = jwt_authenticated_client_a.post( + '/api/processos-associacao/', data=json.dumps(payload_processo_associacao_com_periodos_de_outro_ano), content_type='application/json') + + assert response.status_code == status.HTTP_400_BAD_REQUEST + result = json.loads(response.content) + assert result == { + 'periodos': ["Todos os períodos devem estar no mesmo ano do campo 'ano' (2019)."]} + + +def test_create_processo_associacao_servidor_com_periodo_ja_usado( + jwt_authenticated_client_a, + associacao, + payload_processo_associacao_com_periodos, + processo_associacao_usando_o_mesmo_periodo +): + with override_flag('periodos-processo-sei', active=True): + response = jwt_authenticated_client_a.post( + '/api/processos-associacao/', data=json.dumps(payload_processo_associacao_com_periodos), content_type='application/json') + + assert response.status_code == status.HTTP_400_BAD_REQUEST + result = json.loads(response.content) + assert result == {'periodos': ['O período 2019.2 já está associado a outro ProcessoAssociacao ' + 'para a associação Escola Teste.']} diff --git a/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_retrieve.py b/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_retrieve.py index c77f154d3..d0409cba6 100644 --- a/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_retrieve.py +++ b/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_retrieve.py @@ -5,6 +5,7 @@ pytestmark = pytest.mark.django_db + def test_retrieve_processo_associacao( jwt_authenticated_client_a, processo_associacao_123456_2019): @@ -12,28 +13,17 @@ def test_retrieve_processo_associacao( response = jwt_authenticated_client_a.get( f'/api/processos-associacao/{processo_associacao_123456_2019.uuid}/', content_type='application/json') result = json.loads(response.content) - esperado = { - 'uuid': f'{processo_associacao_123456_2019.uuid}', - 'associacao': - { - 'id': processo_associacao_123456_2019.associacao.id, - 'nome': processo_associacao_123456_2019.associacao.nome, - 'data_de_encerramento': { - 'data': None, - 'help_text': 'A associação deixará de ser exibida nos períodos posteriores à data de encerramento informada.', - 'pode_editar_dados_associacao_encerrada': True - }, - }, - 'criado_em': processo_associacao_123456_2019.criado_em.isoformat("T"), - 'alterado_em': processo_associacao_123456_2019.alterado_em.isoformat("T"), - 'numero_processo': processo_associacao_123456_2019.numero_processo, - 'ano': processo_associacao_123456_2019.ano, - 'tooltip_exclusao': '', - 'permite_exclusao': True - } + assert response.status_code == status.HTTP_200_OK - assert result == esperado + assert result['associacao']['id'] == processo_associacao_123456_2019.associacao.id + assert result['numero_processo'] == processo_associacao_123456_2019.numero_processo + assert result['ano'] == processo_associacao_123456_2019.ano + assert any(periodo['referencia'] == '2019.1' for periodo in result['periodos']) + assert any(periodo['referencia'] == '2019.2' for periodo in result['periodos']) + + # assert result == esperado + def test_retrieve_processo_associacao_processo_sem_pc_vinculada(jwt_authenticated_client_a, processo_associacao_factory): @@ -45,6 +35,7 @@ def test_retrieve_processo_associacao_processo_sem_pc_vinculada(jwt_authenticate assert result['permite_exclusao'] == True assert result['tooltip_exclusao'] == '' + def test_retrieve_processo_associacao_processo_com_pc_vinculada(jwt_authenticated_client_a, periodo_factory, processo_associacao_factory, prestacao_conta_factory): processo_com_pc_vinculada = processo_associacao_factory.create(ano='2023') @@ -58,6 +49,7 @@ def test_retrieve_processo_associacao_processo_com_pc_vinculada(jwt_authenticate assert result['permite_exclusao'] == False assert result['tooltip_exclusao'] == 'Não é possível excluir o número desse processo SEI, pois este já está vinculado a uma prestação de contas. Caso necessário, é possível editá-lo.' + def test_retrieve_processo_associacao_multiplos_processos_por_ano(jwt_authenticated_client_a, processo_associacao_factory, periodo_factory, prestacao_conta_factory): processo_1 = processo_associacao_factory.create(ano='2023') diff --git a/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_update.py b/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_update.py index dc9a5f165..cb8e61ca3 100644 --- a/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_update.py +++ b/sme_ptrf_apps/core/tests/tests_api_processos_associacoes/test_processo_associacao_update.py @@ -2,6 +2,7 @@ import pytest from rest_framework import status +from waffle.testutils import override_flag from sme_ptrf_apps.core.models import ProcessoAssociacao @@ -17,6 +18,16 @@ def payload_processo_associacao(associacao): } return payload +@pytest.fixture +def payload_processo_associacao_com_periodo(associacao, periodo_2020_1): + payload = { + 'associacao': str(associacao.uuid), + 'numero_processo': "271170", + 'ano': '2020', + 'periodos': [str(periodo_2020_1.uuid)] + } + return payload + def test_update_processo_associacao(jwt_authenticated_client_a, associacao, processo_associacao_123456_2019, payload_processo_associacao): @@ -36,3 +47,19 @@ def test_update_processo_associacao(jwt_authenticated_client_a, associacao, proc processo = ProcessoAssociacao.objects.filter(uuid=result['uuid']).get() assert processo.numero_processo == numero_processo_novo + + +def test_update_processo_associacao_sem_periodos_com_flag_ligada(jwt_authenticated_client_a, associacao, processo_associacao_123456_2019, + payload_processo_associacao): + with override_flag('periodos-processo-sei', active=True): + numero_processo_novo = "190889" + payload_processo_associacao['numero_processo'] = numero_processo_novo + response = jwt_authenticated_client_a.put( + f'/api/processos-associacao/{processo_associacao_123456_2019.uuid}/', + data=json.dumps(payload_processo_associacao), + content_type='application/json') + + assert response.status_code == status.HTTP_400_BAD_REQUEST + result = json.loads(response.content) + assert result == {'periodos': ["É necessário informar ao menos um período quando a feature 'periodos-processo-sei' está ativa."]} + diff --git a/sme_ptrf_apps/core/tests/tests_arquivo_download_service/__init__.py b/sme_ptrf_apps/core/tests/tests_arquivo_download_service/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/core/tests/tests_arquivo_download_service/test_arquivo_download_service.py b/sme_ptrf_apps/core/tests/tests_arquivo_download_service/test_arquivo_download_service.py new file mode 100644 index 000000000..6336d1a1a --- /dev/null +++ b/sme_ptrf_apps/core/tests/tests_arquivo_download_service/test_arquivo_download_service.py @@ -0,0 +1,24 @@ +import pytest +from sme_ptrf_apps.core.models import ArquivoDownload +from sme_ptrf_apps.users.fixtures.factories import UsuarioFactory +from sme_ptrf_apps.core.services.arquivo_download_service import gerar_arquivo_download + +pytestmark = pytest.mark.django_db + + +def test_gerar_arquivo_download(): + usr = UsuarioFactory.create() + + arquivo_download_gerado = gerar_arquivo_download( + username=usr.username, + identificador="teste", + informacoes="info_teste" + ) + + arquivo_download_criado = ArquivoDownload.by_id(arquivo_download_gerado.id) + + assert arquivo_download_criado + assert arquivo_download_criado.identificador == "teste" + assert arquivo_download_criado.informacoes == "info_teste" + assert arquivo_download_criado.status == ArquivoDownload.STATUS_EM_PROCESSAMENTO + diff --git a/sme_ptrf_apps/core/tests/tests_processo_associacao/test_processo_associacao_model.py b/sme_ptrf_apps/core/tests/tests_processo_associacao/test_processo_associacao_model.py index 68b96143f..6c6e235a6 100644 --- a/sme_ptrf_apps/core/tests/tests_processo_associacao/test_processo_associacao_model.py +++ b/sme_ptrf_apps/core/tests/tests_processo_associacao/test_processo_associacao_model.py @@ -1,6 +1,6 @@ import pytest -from ...models import ProcessoAssociacao, Associacao +from ...models import ProcessoAssociacao, Associacao, Periodo pytestmark = pytest.mark.django_db @@ -12,6 +12,7 @@ def test_instance_model(processo_associacao_123456_2019): assert model.numero_processo assert model.ano assert model.uuid + assert isinstance(model.periodos.first(), Periodo) def test_admin(): diff --git a/sme_ptrf_apps/core/tests/tests_services/tests_tipos_acertos_lancamentos_service/test_service_endpoint_tabelas_lancamentos.py b/sme_ptrf_apps/core/tests/tests_services/tests_tipos_acertos_lancamentos_service/test_service_endpoint_tabelas_lancamentos.py index 8a32da278..fe1da783a 100644 --- a/sme_ptrf_apps/core/tests/tests_services/tests_tipos_acertos_lancamentos_service/test_service_endpoint_tabelas_lancamentos.py +++ b/sme_ptrf_apps/core/tests/tests_services/tests_tipos_acertos_lancamentos_service/test_service_endpoint_tabelas_lancamentos.py @@ -100,3 +100,50 @@ def test_service_tabelas_agrupado_por_categorias( assert resultado_esperado == resultado +def test_service_tabelas_agrupado_por_categorias_deve_ignorar_categoria( + tipo_acerto_lancamento_agrupa_categoria_01, + tipo_acerto_lancamento_agrupa_categoria_02, + tipo_acerto_lancamento_agrupa_categoria_03, + tipo_acerto_lancamento_agrupa_categoria_04, # Não deve entrar na listagem +): + + resultado_esperado = [ + { + "id": "DEVOLUCAO", + "nome": "Devolução ao tesouro", + "texto": "Esse tipo de acerto demanda informação da data de pagamento da devolução.", + "cor": 1, + "tipos_acerto_lancamento": [ + { + "id": tipo_acerto_lancamento_agrupa_categoria_01.id, + "nome": "Teste", + "categoria": "DEVOLUCAO", + "ativo": tipo_acerto_lancamento_agrupa_categoria_01.ativo, + "uuid": tipo_acerto_lancamento_agrupa_categoria_01.uuid + }, + { + "id": tipo_acerto_lancamento_agrupa_categoria_02.id, + "nome": "Teste 2", + "categoria": "DEVOLUCAO", + "ativo": tipo_acerto_lancamento_agrupa_categoria_02.ativo, + "uuid": tipo_acerto_lancamento_agrupa_categoria_02.uuid + }, + { + "id": tipo_acerto_lancamento_agrupa_categoria_03.id, + "nome": "Teste 3", + "categoria": "DEVOLUCAO", + "ativo": tipo_acerto_lancamento_agrupa_categoria_03.ativo, + "uuid": tipo_acerto_lancamento_agrupa_categoria_03.uuid + } + ] + }, + ] + + categorias_a_ignorar = { + TipoAcertoLancamento.CATEGORIA_EDICAO_LANCAMENTO + } + + resultado = TipoAcertoLancamentoService.agrupado_por_categoria( + TipoAcertoLancamento.CATEGORIA_CHOICES, categorias_a_ignorar) + + assert resultado_esperado == resultado diff --git a/sme_ptrf_apps/despesas/admin.py b/sme_ptrf_apps/despesas/admin.py index 5367df1ed..036522860 100644 --- a/sme_ptrf_apps/despesas/admin.py +++ b/sme_ptrf_apps/despesas/admin.py @@ -64,8 +64,7 @@ def associacao(self, obj): def acao(self, obj): return obj.acao_associacao.acao.nome if obj.acao_associacao else '' - # autocomplete_fields = ['associacao', 'despesa', 'conta_associacao', 'acao_associacao'] - readonly_fields = ('uuid', 'id') + readonly_fields = ('uuid', 'id', 'criado_em', 'alterado_em') class RateioDespesaInLine(admin.TabularInline): diff --git a/sme_ptrf_apps/mandatos/api/serializers/cargo_composicao_serializer.py b/sme_ptrf_apps/mandatos/api/serializers/cargo_composicao_serializer.py index 7b73e864a..f3d65cdef 100644 --- a/sme_ptrf_apps/mandatos/api/serializers/cargo_composicao_serializer.py +++ b/sme_ptrf_apps/mandatos/api/serializers/cargo_composicao_serializer.py @@ -136,7 +136,6 @@ def validate(self, data): raise serializers.ValidationError( "Não é permitido informar a data de saída do cargo anterior a data final da composição anterior") - ocupante_do_cargo_data = data['ocupante_do_cargo'] """ @@ -198,7 +197,8 @@ def create(self, validated_data): associacao=composicao.associacao, mandato=composicao.mandato ) - composicao_anterior = servico_composicao_vigente.get_composicao_anterior(validated_data['data_inicio_no_cargo']) + composicao_anterior = servico_composicao_vigente.get_composicao_anterior( + validated_data['data_inicio_no_cargo']) if composicao_anterior: cargo_substituido = composicao_anterior.cargos_da_composicao_da_composicao.filter( diff --git a/sme_ptrf_apps/mandatos/services/cargo_composicao_service.py b/sme_ptrf_apps/mandatos/services/cargo_composicao_service.py index fde0a5ddf..2aa5d8b97 100644 --- a/sme_ptrf_apps/mandatos/services/cargo_composicao_service.py +++ b/sme_ptrf_apps/mandatos/services/cargo_composicao_service.py @@ -45,6 +45,24 @@ def retorna_se_composicao_vigente(self): else: return False + def get_cargos_por_ocupante_e_mandato(self, cargo): + mandato = self.composicao.mandato + cargos = CargoComposicao.objects.filter( + composicao__mandato=mandato, + ocupante_do_cargo=cargo.ocupante_do_cargo, + ).order_by('composicao__data_final') + return cargos + + def get_data_fim_no_cargo_composicao_mais_recente(self, cargo): + ''' Caso seja uma composição passada, retorna informação de + data final da composição mais recente do ocupante.''' + + if cargo and not self.retorna_se_composicao_vigente(): + cargos_composicao = self.get_cargos_por_ocupante_e_mandato(cargo) + if cargos_composicao.exists(): + return cargos_composicao.last().composicao.data_final.strftime("%Y-%m-%d") + return None + def monta_cargos(self, cargo, indice, valor): obj = { @@ -69,6 +87,7 @@ def monta_cargos(self, cargo, indice, valor): "cargo_associacao_label": valor.split(" ")[0], "data_inicio_no_cargo": cargo.data_inicio_no_cargo if cargo else None, "data_fim_no_cargo": cargo.data_fim_no_cargo if cargo else None, + "data_fim_no_cargo_composicao_mais_recente": self.get_data_fim_no_cargo_composicao_mais_recente(cargo), "eh_composicao_vigente": self.retorna_se_composicao_vigente(), "substituto": cargo.substituto if cargo else None, "tag_substituto": f'Novo membro em {cargo.data_inicio_no_cargo.strftime("%d/%m/%Y")}' if cargo and cargo.substituto else None, @@ -166,13 +185,13 @@ def retorna_se_tem_pendencia(self): class ServicoCargosOcupantesComposicao: def get_ocupantes_ordenados_por_cargo(self, composicao): ocupantes = OcupanteCargo.objects.filter( - cargos_da_composicao_do_ocupante__composicao=composicao - ) - + cargos_da_composicao_do_ocupante__composicao=composicao + ) + if not ocupantes: # Nao existem ocupantes na composicao return [] - + temp_diretoria_executiva = {} temp_conselho_fiscal = {} @@ -232,5 +251,5 @@ def get_ocupantes_ordenados_por_cargo(self, composicao): 'diretoria_executiva': diretoria_executiva, 'conselho_fiscal': conselho_fiscal } - - return objeto \ No newline at end of file + + return objeto diff --git a/sme_ptrf_apps/mandatos/services/composicao_service.py b/sme_ptrf_apps/mandatos/services/composicao_service.py index c3195de22..456ec2c6b 100644 --- a/sme_ptrf_apps/mandatos/services/composicao_service.py +++ b/sme_ptrf_apps/mandatos/services/composicao_service.py @@ -17,7 +17,6 @@ def associacao(self): def mandato(self): return self.__mandato - def get_composicao_vigente(self): data_atual = date.today() @@ -30,7 +29,7 @@ def get_composicao_vigente(self): # Filtrar as composições da associacao qs = qs.filter(associacao=self.associacao) - #Filtrar as composições do mandato + # Filtrar as composições do mandato qs = qs.filter(mandato=self.mandato) # Verificar se há mais de uma composicao vigente, e caso haja, retornar o último (o mais recente) @@ -111,6 +110,7 @@ def cria_nova_composicao_atraves_de_alteracao_membro( minha_composicao_atual = cargo_composicao_sendo_editado.composicao + # cargos encerrados na mesma data não devem gerar composições diferentes. composicao, created = Composicao.objects.get_or_create( associacao=self.associacao, mandato=self.mandato, @@ -139,8 +139,14 @@ def cria_nova_composicao_atraves_de_alteracao_membro( data_inicio_no_cargo=cargo.data_inicio_no_cargo, data_fim_no_cargo=cargo.data_fim_no_cargo ) + # a data fim do cargo inicia com a data fim do mandato, porém deve acompanhar a data fim da composição. + cargo.data_fim_no_cargo = minha_composicao_atual.data_final + cargo.save() else: - composicao.cargos_da_composicao_da_composicao.filter(ocupante_do_cargo=cargo_composicao_sendo_editado.ocupante_do_cargo).delete() + # caso a composição exista, então mais de um cargo foi finalizado com a mesma data. + # sendo assim, o cargo é apenas deletado e o cargo da composição anterior atribuído como substituido. + composicao.cargos_da_composicao_da_composicao.filter( + ocupante_do_cargo=cargo_composicao_sendo_editado.ocupante_do_cargo).delete() try: composicao_anterior = Composicao.objects.get( @@ -157,8 +163,6 @@ def cria_nova_composicao_atraves_de_alteracao_membro( ).update(substituido=True) - - class ServicoRecuperaComposicaoPorData: @staticmethod def get_composicao_por_data_e_associacao(data, associacao_id): diff --git a/sme_ptrf_apps/mandatos/tests/tests_api_cargo_composicao/test_cargo_composicao_list.py b/sme_ptrf_apps/mandatos/tests/tests_api_cargo_composicao/test_cargo_composicao_list.py index f3d3dcaf2..df91c8498 100644 --- a/sme_ptrf_apps/mandatos/tests/tests_api_cargo_composicao/test_cargo_composicao_list.py +++ b/sme_ptrf_apps/mandatos/tests/tests_api_cargo_composicao/test_cargo_composicao_list.py @@ -1,7 +1,9 @@ import json import pytest +from datetime import datetime, timedelta from rest_framework import status from waffle.testutils import override_flag +from sme_ptrf_apps.mandatos.models.cargo_composicao import CargoComposicao pytestmark = pytest.mark.django_db @@ -41,3 +43,86 @@ def teste_get_cargo_composicao( assert response.status_code == status.HTTP_200_OK assert result['cargo_associacao'] == 'Presidente da diretoria executiva' assert result['ocupante_do_cargo']['nome'] == 'Ollyver Ottoboni' + + +@override_flag('historico-de-membros', active=True) +def teste_get_cargos_composicao_data_fim_no_cargo_composicao_mais_recente( + cargo_composicao_01, + cargo_composicao_02, + jwt_authenticated_client_sme, +): + response = jwt_authenticated_client_sme.get( + f'/api/cargos-composicao/', + content_type='application/json' + ) + + result = json.loads(response.content) + + assert response.status_code == status.HTTP_200_OK + # ['results'] por causa da paginação na viewset + assert len(result['results']) == 2 + + +@override_flag('historico-de-membros', active=True) +def teste_get_cargos_composicao_cargos_da_composicao_data_fim_no_cargo_composicao_mais_recente( + mandato_factory, + composicao_factory, + cargo_composicao_factory, + ocupante_cargo_factory, + jwt_authenticated_client_sme, +): + mandato_2024 = mandato_factory.create(data_inicial=datetime.now( + ) - timedelta(days=91), data_final=datetime.now() + timedelta(days=365)) + + finaliza_apos_30_dias = mandato_2024.data_inicial + timedelta(days=30) + finaliza_apos_60_dias = mandato_2024.data_inicial + timedelta(days=60) + finaliza_apos_90_dias = mandato_2024.data_inicial + timedelta(days=90) + + composicao_1 = composicao_factory.create( + mandato=mandato_2024, data_inicial=mandato_2024.data_inicial, data_final=finaliza_apos_30_dias) + composicao_2 = composicao_factory.create( + mandato=mandato_2024, data_inicial=finaliza_apos_30_dias + timedelta(days=1), data_final=finaliza_apos_60_dias) + composicao_3 = composicao_factory.create( + mandato=mandato_2024, data_inicial=finaliza_apos_60_dias + timedelta(days=1), data_final=finaliza_apos_90_dias) + composicao_4 = composicao_factory.create( + mandato=mandato_2024, data_inicial=finaliza_apos_90_dias + timedelta(days=1), data_final=mandato_2024.data_final) + + presidente_executiva = ocupante_cargo_factory.create() + + cargo_1 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_1.data_inicial, + data_fim_no_cargo=composicao_1.data_final, + composicao=composicao_1, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + + cargo_2 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_2.data_inicial, + data_fim_no_cargo=composicao_2.data_final, + composicao=composicao_2, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + cargo_3 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_3.data_inicial, + data_fim_no_cargo=composicao_3.data_final, + composicao=composicao_3, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + + cargo_4_vigente = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_4.data_inicial, + data_fim_no_cargo=composicao_4.data_final, + composicao=composicao_4, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + response = jwt_authenticated_client_sme.get( + f'/api/cargos-composicao/cargos-da-composicao/?composicao_uuid={composicao_2.uuid}', + content_type='application/json' + ) + + assert response.json()[ + 'diretoria_executiva'][0]['data_fim_no_cargo_composicao_mais_recente'] == composicao_4.data_final.strftime("%Y-%m-%d") diff --git a/sme_ptrf_apps/mandatos/tests/tests_api_cargo_composicao/test_cargo_composicao_update.py b/sme_ptrf_apps/mandatos/tests/tests_api_cargo_composicao/test_cargo_composicao_update.py index d760e92d0..134879d2c 100644 --- a/sme_ptrf_apps/mandatos/tests/tests_api_cargo_composicao/test_cargo_composicao_update.py +++ b/sme_ptrf_apps/mandatos/tests/tests_api_cargo_composicao/test_cargo_composicao_update.py @@ -1,8 +1,11 @@ import json import pytest +from datetime import datetime, timedelta from rest_framework import status from waffle.testutils import override_flag from freezegun import freeze_time +from sme_ptrf_apps.mandatos.models.composicao import Composicao +from sme_ptrf_apps.mandatos.models.cargo_composicao import CargoComposicao pytestmark = pytest.mark.django_db @@ -26,6 +29,153 @@ def teste_cargos_composicao_update( assert response.status_code == status.HTTP_200_OK +@override_flag('historico-de-membros', active=True) +def teste_cargos_composicao_update_encerrar_primeiro_cargo_antes_do_fim_do_mandato( + jwt_authenticated_client_sme, + mandato_factory, + composicao_factory, + cargo_composicao_factory, + ocupante_cargo_factory +): + mandato_2024 = mandato_factory.create(data_inicial=datetime(2024, 1, 1)) + composicao = composicao_factory.create(mandato=mandato_2024, + data_inicial=mandato_2024.data_inicial, + data_final=mandato_2024.data_final) + presidente_executiva = ocupante_cargo_factory.create() + presidente_fiscal = ocupante_cargo_factory.create() + + cargo_composicao = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao.data_inicial, + data_fim_no_cargo=composicao.data_final, + composicao=composicao, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + + cargo_composicao_factory.create( + data_inicio_no_cargo=composicao.data_inicial, + data_fim_no_cargo=composicao.data_final, + composicao=composicao, + ocupante_do_cargo=presidente_fiscal, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_CONSELHO_FISCAL + ) + + uuid_cargo_composicao = f'{cargo_composicao.uuid}' + data_fim_no_cargo = cargo_composicao.data_inicio_no_cargo + timedelta(days=30) + + payload = { + "composicao": f"{composicao.uuid}", + "ocupante_do_cargo": { + "nome": f"{presidente_executiva.nome}", + "codigo_identificacao": f"{presidente_executiva.codigo_identificacao}", + "cargo_educacao": f"{presidente_executiva.cargo_educacao}", + "representacao": "SERVIDOR", + "representacao_label": "Servidor", + "email": f"{presidente_executiva.email}", + "cpf_responsavel": f"{presidente_executiva.cpf_responsavel}", + "telefone": f"{presidente_executiva.telefone}", + "cep": f"{presidente_executiva.cep}", + "bairro": f"{presidente_executiva.bairro}", + "endereco": f"{presidente_executiva.endereco}", + }, + + "cargo_associacao": cargo_composicao.cargo_associacao, + "substituto": False, + "substituido": False, + "data_inicio_no_cargo": cargo_composicao.data_inicio_no_cargo.strftime("%Y-%m-%d"), + "data_fim_no_cargo": cargo_composicao.data_fim_no_cargo.strftime("%Y-%m-%d"), + "data_saida_do_cargo": data_fim_no_cargo.strftime("%Y-%m-%d"), + } + + response = jwt_authenticated_client_sme.put( + f'/api/cargos-composicao/{uuid_cargo_composicao}/', + data=json.dumps(payload), + content_type='application/json' + ) + assert response.status_code == status.HTTP_200_OK + + composicao_finalizada = Composicao.objects.filter(associacao=composicao.associacao, + data_final=data_fim_no_cargo).first() + assert composicao_finalizada is not None + + for item in composicao_finalizada.cargos_da_composicao_da_composicao.all(): + assert item.data_fim_no_cargo.strftime("%Y-%m-%d") == composicao_finalizada.data_final.strftime("%Y-%m-%d") + + +@override_flag('historico-de-membros', active=True) +def teste_cargos_composicao_update_encerrar_segundo_cargo_antes_do_fim_do_mandato( + jwt_authenticated_client_sme, + mandato_factory, + composicao_factory, + cargo_composicao_factory, + ocupante_cargo_factory +): + mandato_2024 = mandato_factory.create(data_inicial=datetime(2024, 1, 1)) + composicao_encerrada = composicao_factory.create( + mandato=mandato_2024, data_inicial=mandato_2024.data_inicial, data_final=mandato_2024.data_inicial + timedelta(days=30)) + + composicao = composicao_factory.create(mandato=mandato_2024, + data_inicial=composicao_encerrada.data_final + timedelta(days=1), + data_final=mandato_2024.data_final) + + presidente_executiva = ocupante_cargo_factory.create() + presidente_fiscal = ocupante_cargo_factory.create() + + cargo_composicao_encerrado = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_encerrada.data_inicial, + data_fim_no_cargo=composicao_encerrada.data_final, + composicao=composicao_encerrada, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + + cargo_composicao = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao.data_inicial, + data_fim_no_cargo=composicao.data_final, + composicao=composicao, + ocupante_do_cargo=presidente_fiscal, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_CONSELHO_FISCAL + ) + + uuid_cargo_composicao = f'{cargo_composicao.uuid}' + data_fim_no_cargo = cargo_composicao_encerrado.data_fim_no_cargo + + payload = { + "composicao": f"{composicao.uuid}", + "ocupante_do_cargo": { + "nome": f"{presidente_executiva.nome}", + "codigo_identificacao": f"{presidente_executiva.codigo_identificacao}", + "cargo_educacao": f"{presidente_executiva.cargo_educacao}", + "representacao": "SERVIDOR", + "representacao_label": "Servidor", + "email": f"{presidente_executiva.email}", + "cpf_responsavel": f"{presidente_executiva.cpf_responsavel}", + "telefone": f"{presidente_executiva.telefone}", + "cep": f"{presidente_executiva.cep}", + "bairro": f"{presidente_executiva.bairro}", + "endereco": f"{presidente_executiva.endereco}", + }, + + "cargo_associacao": cargo_composicao.cargo_associacao, + "substituto": False, + "substituido": False, + "data_inicio_no_cargo": cargo_composicao.data_inicio_no_cargo.strftime("%Y-%m-%d"), + "data_fim_no_cargo": cargo_composicao.data_fim_no_cargo.strftime("%Y-%m-%d"), + "data_saida_do_cargo": data_fim_no_cargo.strftime("%Y-%m-%d"), + } + + response = jwt_authenticated_client_sme.put( + f'/api/cargos-composicao/{uuid_cargo_composicao}/', + data=json.dumps(payload), + content_type='application/json' + ) + assert response.status_code == status.HTTP_200_OK + + assert Composicao.objects.all().count() == 2 + + assert not CargoComposicao.objects.filter(uuid=uuid_cargo_composicao).exists() + + @override_flag('historico-de-membros', active=True) def teste_cargos_composicao_update_deve_retornar_erro_data_inicio_no_cargo_menor_que_data_inicio_composicao( composicao_01_2023_a_2025, @@ -182,7 +332,8 @@ def teste_cargos_composicao_update_deve_retornar_erro_data_saida_do_cargo_maior_ payload_update_cargo_composicao_data_saida_do_cargo_maior_que_data_atual, jwt_authenticated_client_sme, ): - payload_update_cargo_composicao_data_saida_do_cargo_maior_que_data_atual['ocupante_do_cargo']['codigo_identificacao'] = "7777777" + payload_update_cargo_composicao_data_saida_do_cargo_maior_que_data_atual[ + 'ocupante_do_cargo']['codigo_identificacao'] = "7777777" uuid_cargo_composicao = f'{cargo_composicao_01.uuid}' @@ -213,7 +364,8 @@ def teste_cargos_composicao_update_deve_retornar_erro_data_saida_do_cargo_maior_ payload_update_cargo_composicao_data_saida_do_cargo_maior_ou_igual_que_data_final_mandato, jwt_authenticated_client_sme, ): - payload_update_cargo_composicao_data_saida_do_cargo_maior_ou_igual_que_data_final_mandato['ocupante_do_cargo']['codigo_identificacao'] = "7777777" + payload_update_cargo_composicao_data_saida_do_cargo_maior_ou_igual_que_data_final_mandato[ + 'ocupante_do_cargo']['codigo_identificacao'] = "7777777" uuid_cargo_composicao = f'{cargo_composicao_01.uuid}' @@ -243,7 +395,8 @@ def teste_cargos_composicao_update_deve_retornar_erro_data_saida_do_cargo_anteri payload_update_cargo_composicao_data_saida_do_cargo_anterior_data_final_da_composicao_anterior, jwt_authenticated_client_sme, ): - payload_update_cargo_composicao_data_saida_do_cargo_anterior_data_final_da_composicao_anterior['ocupante_do_cargo']['codigo_identificacao'] = "7777777" + payload_update_cargo_composicao_data_saida_do_cargo_anterior_data_final_da_composicao_anterior[ + 'ocupante_do_cargo']['codigo_identificacao'] = "7777777" uuid_cargo_composicao = f'{cargo_composicao_01_testes_data_saida_do_cargo.uuid}' diff --git a/sme_ptrf_apps/mandatos/tests/tests_services/test_cargo_composicao_service.py b/sme_ptrf_apps/mandatos/tests/tests_services/test_cargo_composicao_service.py index 65a37630a..2dc6ce0d6 100644 --- a/sme_ptrf_apps/mandatos/tests/tests_services/test_cargo_composicao_service.py +++ b/sme_ptrf_apps/mandatos/tests/tests_services/test_cargo_composicao_service.py @@ -1,7 +1,8 @@ import pytest - +from datetime import datetime, timedelta from sme_ptrf_apps.mandatos.services import ServicoCargosDaComposicao, ServicoCargosDaDiretoriaExecutiva, \ ServicoPendenciaCargosDaComposicaoVigenteDaAssociacao +from sme_ptrf_apps.mandatos.models.cargo_composicao import CargoComposicao pytestmark = pytest.mark.django_db @@ -101,3 +102,129 @@ def test_servico_cargos_da_composicao_retorna_tags_substituto_substituido__subst assert not cargos_da_composicao['diretoria_executiva'][0]['substituido'] assert cargos_da_composicao['diretoria_executiva'][0]['substituto'] assert cargos_da_composicao['diretoria_executiva'][0]['tag_substituto'] == "Novo membro em 01/01/2024" + + +def test_servico_cargos_da_composicao_get_cargos_por_ocupante_e_mandato( + mandato_factory, + composicao_factory, + cargo_composicao_factory, + ocupante_cargo_factory +): + mandato_2024 = mandato_factory.create(data_inicial=datetime(2024, 1, 1), data_final=datetime(2024, 12, 31)) + composicao_1 = composicao_factory.create( + mandato=mandato_2024, data_inicial=mandato_2024.data_inicial, data_final=mandato_2024.data_inicial + timedelta(days=30)) + composicao_2 = composicao_factory.create( + mandato=mandato_2024, data_inicial=mandato_2024.data_inicial + timedelta(days=31), data_final=mandato_2024.data_inicial + timedelta(days=60)) + composicao_3 = composicao_factory.create( + mandato=mandato_2024, data_inicial=mandato_2024.data_inicial + timedelta(days=61), data_final=mandato_2024.data_inicial + timedelta(days=90)) + composicao_4 = composicao_factory.create( + mandato=mandato_2024, data_inicial=mandato_2024.data_inicial + timedelta(days=91), data_final=mandato_2024.data_inicial + timedelta(days=120)) + + presidente_executiva = ocupante_cargo_factory.create() + + cargo_1 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_1.data_inicial, + data_fim_no_cargo=composicao_1.data_final, + composicao=composicao_1, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + cargo_2 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_2.data_inicial, + data_fim_no_cargo=composicao_2.data_final, + composicao=composicao_2, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + cargo_3 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_3.data_inicial, + data_fim_no_cargo=composicao_3.data_final, + composicao=composicao_3, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + cargo_4 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_4.data_inicial, + data_fim_no_cargo=composicao_4.data_final, + composicao=composicao_4, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + + servico_cargos_da_composicao = ServicoCargosDaComposicao(composicao=composicao_1) + cargos = servico_cargos_da_composicao.get_cargos_por_ocupante_e_mandato(cargo_1) + + assert list(cargos.values_list('id', flat=True)) == [cargo_1.id, cargo_2.id, cargo_3.id, cargo_4.id] + assert cargos.count() == 4 + + +def test_servico_cargos_da_composicao_get_data_fim_no_cargo_composicao_mais_recente( + mandato_factory, + composicao_factory, + cargo_composicao_factory, + ocupante_cargo_factory +): + mandato_2024 = mandato_factory.create(data_inicial=datetime.now( + ) - timedelta(days=91), data_final=datetime.now() + timedelta(days=365)) + + finaliza_apos_30_dias = mandato_2024.data_inicial + timedelta(days=30) + finaliza_apos_60_dias = mandato_2024.data_inicial + timedelta(days=60) + finaliza_apos_90_dias = mandato_2024.data_inicial + timedelta(days=90) + + composicao_1 = composicao_factory.create( + mandato=mandato_2024, data_inicial=mandato_2024.data_inicial, data_final=finaliza_apos_30_dias) + composicao_2 = composicao_factory.create( + mandato=mandato_2024, data_inicial=finaliza_apos_30_dias + timedelta(days=1), data_final=finaliza_apos_60_dias) + composicao_3 = composicao_factory.create( + mandato=mandato_2024, data_inicial=finaliza_apos_60_dias + timedelta(days=1), data_final=finaliza_apos_90_dias) + composicao_4 = composicao_factory.create( + mandato=mandato_2024, data_inicial=finaliza_apos_90_dias + timedelta(days=1), data_final=mandato_2024.data_final) + + presidente_executiva = ocupante_cargo_factory.create() + + cargo_1 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_1.data_inicial, + data_fim_no_cargo=composicao_1.data_final, + composicao=composicao_1, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + + cargo_2 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_2.data_inicial, + data_fim_no_cargo=composicao_2.data_final, + composicao=composicao_2, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + cargo_3 = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_3.data_inicial, + data_fim_no_cargo=composicao_3.data_final, + composicao=composicao_3, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + + servico_cargos_da_composicao = ServicoCargosDaComposicao(composicao=composicao_1) + data_fim = servico_cargos_da_composicao.get_data_fim_no_cargo_composicao_mais_recente(cargo_1) + assert data_fim == composicao_3.data_final.strftime("%Y-%m-%d") + + servico_cargos_da_composicao = ServicoCargosDaComposicao(composicao=composicao_2) + data_fim = servico_cargos_da_composicao.get_data_fim_no_cargo_composicao_mais_recente(cargo_2) + assert data_fim == composicao_3.data_final.strftime("%Y-%m-%d") + + servico_cargos_da_composicao = ServicoCargosDaComposicao(composicao=composicao_3) + data_fim = servico_cargos_da_composicao.get_data_fim_no_cargo_composicao_mais_recente(cargo_3) + assert data_fim == composicao_3.data_final.strftime("%Y-%m-%d") + + cargo_4_vigente = cargo_composicao_factory.create( + data_inicio_no_cargo=composicao_4.data_inicial, + data_fim_no_cargo=composicao_4.data_final, + composicao=composicao_4, + ocupante_do_cargo=presidente_executiva, + cargo_associacao=CargoComposicao.CARGO_ASSOCIACAO_PRESIDENTE_DIRETORIA_EXECUTIVA + ) + + servico_cargos_da_composicao = ServicoCargosDaComposicao(composicao=composicao_4) + data_fim = servico_cargos_da_composicao.get_data_fim_no_cargo_composicao_mais_recente(cargo_4_vigente) + assert data_fim == None diff --git a/sme_ptrf_apps/receitas/admin.py b/sme_ptrf_apps/receitas/admin.py index 98b5c1aaa..01faabf17 100644 --- a/sme_ptrf_apps/receitas/admin.py +++ b/sme_ptrf_apps/receitas/admin.py @@ -101,8 +101,8 @@ class RepasseAdmin(admin.ModelAdmin): list_display = ('associacao', 'periodo', 'valor_capital', 'valor_custeio', 'valor_livre', 'tipo_conta', 'acao', 'status') list_filter = ('periodo', 'status', 'carga_origem') - # Campos tipo autocomplete substituem o componente padrão de seleção de chaves extrangeiras e são bem mais rápidos. - autocomplete_fields = ['associacao', 'periodo', 'conta_associacao', 'acao_associacao', 'carga_origem'] + raw_id_fields = ('associacao', 'periodo', 'conta_associacao', 'acao_associacao', 'carga_origem') + readonly_fields = ('uuid', 'id', 'criado_em', 'alterado_em') def tipo_conta(self, obj): return obj.conta_associacao.tipo_conta if obj.conta_associacao else '' diff --git a/sme_ptrf_apps/receitas/services/carga_repasses_previstos.py b/sme_ptrf_apps/receitas/services/carga_repasses_previstos.py index 635a2bf2f..8d5c510a3 100644 --- a/sme_ptrf_apps/receitas/services/carga_repasses_previstos.py +++ b/sme_ptrf_apps/receitas/services/carga_repasses_previstos.py @@ -4,7 +4,8 @@ import logging import os -from sme_ptrf_apps.core.models import Acao, AcaoAssociacao, Associacao, ContaAssociacao, Periodo, TipoConta +from sme_ptrf_apps.core.models import Acao, AcaoAssociacao, Associacao, ContaAssociacao, Periodo, TipoConta, \ + PrestacaoConta from sme_ptrf_apps.core.models.arquivo import ( DELIMITADOR_PONTO_VIRGULA, DELIMITADOR_VIRGULA, @@ -148,6 +149,9 @@ def processa_repasse(reader, tipo_conta, arquivo): periodo = get_periodo(nome_arquivo) + if PrestacaoConta.objects.filter(periodo=periodo).exists(): + raise CargaRepassePrevistoException(f"Já existe prestações de conta para o período {periodo.referencia}.") + logs = [] importados = 0 diff --git a/sme_ptrf_apps/receitas/services/carga_repasses_realizados.py b/sme_ptrf_apps/receitas/services/carga_repasses_realizados.py index dfd8b1f5c..c6336cf14 100644 --- a/sme_ptrf_apps/receitas/services/carga_repasses_realizados.py +++ b/sme_ptrf_apps/receitas/services/carga_repasses_realizados.py @@ -4,7 +4,15 @@ import logging from datetime import datetime -from sme_ptrf_apps.core.models import Acao, AcaoAssociacao, Associacao, ContaAssociacao, Periodo, TipoConta +from sme_ptrf_apps.core.models import ( + Acao, + AcaoAssociacao, + Associacao, + ContaAssociacao, + Periodo, + TipoConta, + PrestacaoConta +) from sme_ptrf_apps.core.models.arquivo import ( DELIMITADOR_PONTO_VIRGULA, DELIMITADOR_VIRGULA, @@ -320,6 +328,17 @@ def processa_repasse(reader, tipo_conta, arquivo): arquivo.save() +def arquivo_tem_periodos_com_pcs(reader): + logger.info("Verificando se arquivo tem períodos com PCs") + for index, row in enumerate(reader): + if index == 0: # Ignorar cabeçalho + continue + periodo = get_periodo(str(row[PERIODO]).strip()) + if PrestacaoConta.objects.filter(periodo=periodo).exists(): + return True + return False + + def carrega_repasses_realizados(arquivo): logger.info("Processando arquivo %s", arquivo.identificador) tipo_conta_nome = TipoContaEnum.CARTAO.value if 'cartao' in arquivo.identificador else TipoContaEnum.CHEQUE.value @@ -340,6 +359,12 @@ def carrega_repasses_realizados(arquivo): return reader = csv.reader(f, delimiter=sniffer.delimiter) + + if arquivo_tem_periodos_com_pcs(reader): + raise CargaRepasseRealizadoException(f"Não foi possível realizar a carga. Já existem PCs geradas no período.") + + f.seek(0) # Retorna ao início do arquivo para a segunda leitura + processa_repasse(reader, tipo_conta, arquivo) except Exception as err: diff --git a/sme_ptrf_apps/receitas/tests/test_repasse/test_carga_repasse_previstos.py b/sme_ptrf_apps/receitas/tests/test_repasse/test_carga_repasse_previstos.py index e887fa8c7..775ef753d 100644 --- a/sme_ptrf_apps/receitas/tests/test_repasse/test_carga_repasse_previstos.py +++ b/sme_ptrf_apps/receitas/tests/test_repasse/test_carga_repasse_previstos.py @@ -272,6 +272,6 @@ def test_carga_processado_com_erro_associacao_periodo_com_pc( acao_ptrf_basico ): carrega_repasses_previstos(arquivo_carga_associacao_periodo_com_pc) - msg = """Erro na linha 1: A associação 123456 já possui PC gerada no período 2019.1.\nForam criados 0 repasses. Erro na importação de 1 repasse(s).""" + msg = "Erro ao processar repasses previstos: Já existe prestações de conta para o período 2019.1." assert arquivo_carga_associacao_periodo_com_pc.log == msg assert arquivo_carga_associacao_periodo_com_pc.status == ERRO diff --git a/sme_ptrf_apps/receitas/tests/test_repasse/test_carga_repasse_realizados.py b/sme_ptrf_apps/receitas/tests/test_repasse/test_carga_repasse_realizados.py index 84d8f5127..0bfefef7d 100644 --- a/sme_ptrf_apps/receitas/tests/test_repasse/test_carga_repasse_realizados.py +++ b/sme_ptrf_apps/receitas/tests/test_repasse/test_carga_repasse_realizados.py @@ -124,7 +124,18 @@ def test_carga_com_erro_formatacao(arquivo_carga, tipo_conta_cheque): assert arquivo_carga.status == ERRO -def test_carga_com_erro(arquivo_carga_virgula, tipo_conta_cheque): +@pytest.fixture +def periodo_2020_u(): + return baker.make( + 'Periodo', + referencia='2020.u', + data_inicio_realizacao_despesas=datetime.date(2020, 1, 1), + data_fim_realizacao_despesas=datetime.date(2020, 12, 31), + periodo_anterior=None, + ) + + +def test_carga_com_erro(arquivo_carga_virgula, tipo_conta_cheque, periodo_2020_u): carrega_repasses_realizados(arquivo_carga_virgula) msg = """\nErro na linha 1: Associação com código eol: 93238 não encontrado. Linha ID:10 Foram criados 0 repasses. Erro na importação de 1 repasse(s).""" @@ -138,7 +149,7 @@ def acao_role_cultural_teste(): def test_carga_processado_com_erro(arquivo_carga_virgula_processado, periodo, associacao, tipo_receita_repasse, - tipo_conta_cheque, acao_role_cultural, acao_role_cultural_teste): + tipo_conta_cheque, acao_role_cultural, acao_role_cultural_teste, periodo_2020_u): carrega_repasses_realizados(arquivo_carga_virgula_processado) msg = """\nErro na linha 1: Ação Rolê Cultural não permite capital.\nErro na linha 2: Associação com código eol: 93238 não encontrado. Linha ID:20 Foram criados 0 repasses. Erro na importação de 2 repasse(s).""" @@ -254,6 +265,6 @@ def test_carga_processado_com_erro_associacao_periodo_com_pc( acao_ptrf_basico ): carrega_repasses_realizados(arquivo_carga_associacao_periodo_com_pc) - msg = """\nErro na linha 1: A associação 123456 já possui PC gerada no período 2024.1.\nForam criados 0 repasses. Erro na importação de 1 repasse(s).""" + msg = "Erro ao processar repasses realizados: Não foi possível realizar a carga. Já existem PCs geradas no período." assert arquivo_carga_associacao_periodo_com_pc.log == msg assert arquivo_carga_associacao_periodo_com_pc.status == ERRO diff --git a/sme_ptrf_apps/sme/api/views/exportacoes_dados.py b/sme_ptrf_apps/sme/api/views/exportacoes_dados.py index 4bec2e83d..5604a4996 100644 --- a/sme_ptrf_apps/sme/api/views/exportacoes_dados.py +++ b/sme_ptrf_apps/sme/api/views/exportacoes_dados.py @@ -22,6 +22,9 @@ exportar_rateios_async, exportar_demonstativos_financeiros_async, exportar_dados_conta_async, + exportar_repasses_async, + exportar_dados_membros_apm_async, + exportar_processos_sei_regularidade_async ) from sme_ptrf_apps.users.permissoes import ( @@ -116,6 +119,7 @@ def status_prestacoes_contas(self, request): data_inicio=request.query_params.get("data_inicio"), data_final=request.query_params.get("data_final"), username=request.user.username, + dre_uuid=request.query_params.get("dre_uuid"), ) return Response( @@ -292,3 +296,93 @@ def contas_associacao(self, request): }, status=HTTP_201_CREATED, ) + + @extend_schema( + parameters=[ + OpenApiParameter( + name="data_inicio", + type=OpenApiTypes.DATE, + description="Data de início", + required=False, + ), + OpenApiParameter( + name="data_final", + type=OpenApiTypes.DATE, + description="Data final", + required=False, + ), + ] + ) + @action( + detail=False, + methods=["get"], + url_path="repasses", + permission_classes=permission_classes, + ) + def repasses(self, request): + exportar_repasses_async.delay( + data_inicio=request.query_params.get("data_inicio"), + data_final=request.query_params.get("data_final"), + username=request.user.username, + ) + + return Response( + { + "response": "O arquivo está sendo gerado e será enviado para a central de download após conclusão." + }, + status=HTTP_201_CREATED, + ) + + @extend_schema( + parameters=[ + OpenApiParameter( + name="data_inicio", + type=OpenApiTypes.DATE, + description="Data de início", + required=False, + ), + OpenApiParameter( + name="data_final", + type=OpenApiTypes.DATE, + description="Data final", + required=False, + ), + ] + ) + @action( + detail=False, + methods=["get"], + url_path="dados_membros_apm", + permission_classes=permission_classes, + ) + def dados_membros_apm(self, request): + exportar_dados_membros_apm_async.delay( + data_inicio=request.query_params.get("data_inicio"), + data_final=request.query_params.get("data_final"), + username=request.user.username, + ) + + return Response( + { + "response": "O arquivo está sendo gerado e será enviado para a central de download após conclusão." + }, + status=HTTP_201_CREATED, + ) + + @action( + detail=False, + methods=["get"], + url_path="processos-sei-regularidade", + permission_classes=permission_classes, + ) + def processos_sei_regularidade(self, request): + exportar_processos_sei_regularidade_async.delay( + username=request.user.username, + ) + + return Response( + { + "response": "O arquivo está sendo gerado e será enviado para a central de download após conclusão." + }, + status=HTTP_201_CREATED, + ) diff --git a/sme_ptrf_apps/sme/services/exporta_atas_service.py b/sme_ptrf_apps/sme/services/exporta_atas_service.py index 8ed997c74..5361d5cf5 100644 --- a/sme_ptrf_apps/sme/services/exporta_atas_service.py +++ b/sme_ptrf_apps/sme/services/exporta_atas_service.py @@ -1,8 +1,9 @@ import csv -from datetime import datetime +from datetime import datetime, time import logging from django.core.files import File +from django.utils.timezone import make_aware from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload from sme_ptrf_apps.core.models.ambiente import Ambiente from sme_ptrf_apps.core.services.arquivo_download_service import ( @@ -14,10 +15,32 @@ logger = logging.getLogger(__name__) + +def get_informacoes_download(data_inicio, data_final): + """ + Retorna uma string com as informações do download conforme a data de início e final de extração. + """ + + data_inicio = datetime.strptime(data_inicio, "%Y-%m-%d").strftime("%d/%m/%Y") if data_inicio else None + data_final = datetime.strptime(data_final, "%Y-%m-%d").strftime("%d/%m/%Y") if data_final else None + + if data_inicio and data_final: + return f"Filtro aplicado: {data_inicio} a {data_final} (data de criação do registro)" + + if data_inicio and not data_final: + return f"Filtro aplicado: A partir de {data_inicio} (data de criação do registro)" + + if data_final and not data_inicio: + return f"Filtro aplicado: Até {data_final} (data de criação do registro)" + + return "" + + CABECALHO_ATAS = [ ('Código EOL', 'associacao__unidade__codigo_eol'), ('Nome unidade', 'associacao__unidade__nome'), ('Nome associação', 'associacao__nome'), + ('DRE', 'associacao__unidade__dre__nome'), ('Referência do período da PC', 'periodo__referencia'), ('Tipo de ata', 'tipo_ata'), ('Tipo de reunião', 'tipo_reuniao'), @@ -145,25 +168,25 @@ def monta_dados(self): return linhas_vertical def filtra_range_data(self, field): - if self.data_inicio and self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') + # Converte as datas inicial e final de texto para date + inicio = datetime.strptime(self.data_inicio, "%Y-%m-%d").date() if self.data_inicio else None + final = datetime.strptime(self.data_final, "%Y-%m-%d").date() if self.data_final else None + + # Define o horário da data_final para o último momento do dia + # Sem isso o filtro pode não incluir todos os registros do dia + final = make_aware(datetime.combine(final, time.max)) if final else None + if inicio and final: self.queryset = self.queryset.filter( - **{f'{field}__range': [self.data_inicio, self.data_final]} + **{f'{field}__gte': inicio, f'{field}__lte': final} ) - elif self.data_inicio and not self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') - + elif inicio and not final: self.queryset = self.queryset.filter( - **{f'{field}__gt': self.data_inicio} + **{f'{field}__gte': inicio} ) - - elif self.data_final and not self.data_inicio: - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') - + elif final and not inicio: self.queryset = self.queryset.filter( - **{f'{field}__lt': self.data_final} + **{f'{field}__lte': final} ) return self.queryset @@ -171,7 +194,8 @@ def cria_registro_central_download(self): logger.info(f"Criando registro na central de download") obj = gerar_arquivo_download( self.user, - self.nome_arquivo + self.nome_arquivo, + informacoes=get_informacoes_download(self.data_inicio, self.data_final) ) self.objeto_arquivo_download = obj @@ -193,21 +217,25 @@ def envia_arquivo_central_download(self, tmp): self.objeto_arquivo_download.save() logger.error("Erro arquivo download...") - - def texto_rodape(self): + def texto_info_arquivo_gerado(self): data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - texto = f"Arquivo gerado pelo {self.ambiente} em {data_hora_geracao}" + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" return texto def cria_rodape(self, write): rodape = [] - texto = self.texto_rodape() + texto_info_arquivo_gerado = self.texto_info_arquivo_gerado() - rodape.append("\n") + rodape.append(" ") write.writerow(rodape) rodape.clear() - rodape.append(texto) + rodape.append(texto_info_arquivo_gerado) write.writerow(rodape) rodape.clear() + + rodape.append(get_informacoes_download(self.data_inicio, self.data_final)) + write.writerow(rodape) + rodape.clear() + diff --git a/sme_ptrf_apps/sme/services/exporta_dados_contas_service.py b/sme_ptrf_apps/sme/services/exporta_dados_contas_service.py index a635b84bd..b0bdaf417 100644 --- a/sme_ptrf_apps/sme/services/exporta_dados_contas_service.py +++ b/sme_ptrf_apps/sme/services/exporta_dados_contas_service.py @@ -5,8 +5,11 @@ from django.core.files import File from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload +from sme_ptrf_apps.core.models.conta_associacao import ContaAssociacao from sme_ptrf_apps.core.models.ambiente import Ambiente from sme_ptrf_apps.core.services.arquivo_download_service import gerar_arquivo_download +from django.utils.timezone import make_aware +from django.db.models import QuerySet from sme_ptrf_apps.utils.built_in_custom import get_recursive_attr from tempfile import NamedTemporaryFile @@ -37,6 +40,7 @@ class ExportacaoDadosContasService: def __init__(self, **kwargs) -> None: + self.cabecalho = CABECALHO_CONTA[0] self.queryset = kwargs.get("queryset", None) self.data_inicio = kwargs.get("data_inicio", None) self.data_final = kwargs.get("data_final", None) @@ -44,15 +48,37 @@ def __init__(self, **kwargs) -> None: self.user = kwargs.get("user", None) self.ambiente = self.get_ambiente self.objeto_arquivo_download = None + self.texto_filtro_aplicado = self.get_texto_filtro_aplicado() @property def get_ambiente(self): ambiente = Ambiente.objects.first() return ambiente.prefixo if ambiente else "" + def get_texto_filtro_aplicado(self): + if self.data_inicio and self.data_final: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + + return f"Filtro aplicado: {data_inicio_formatada} a {data_final_formatada} (data de criação do registro)" + + if self.data_inicio: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: A partir de {data_inicio_formatada} (data de criação do registro)" + + if self.data_final: + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: Até {data_final_formatada} (data de criação do registro)" + + return "" + def exporta_contas_principal(self): self.cria_registro_central_download() - self.cabecalho = CABECALHO_CONTA[0] self.filtra_range_data("criado_em") self.exporta_contas_csv() @@ -69,6 +95,11 @@ def exporta_contas_csv(self) -> BinaryIO: write.writerow([cabecalho[0] for cabecalho in self.cabecalho]) for instance in self.queryset: + + if not ContaAssociacao.objects.filter(id=instance.id).exists(): + logger.info(f"Este registro não existe mais na base de dados, portanto será pulado") + continue + for _, campo in self.cabecalho: if campo in ["data_inicio", "criado_em", "data_encerramento"]: campo = getattr(instance, campo) @@ -77,7 +108,12 @@ def exporta_contas_csv(self) -> BinaryIO: ) elif campo == "saldo_atual": - campo = str(getattr(instance, campo)).replace(".", ",") + + if instance.associacao: + campo = str(getattr(instance, campo)).replace(".", ",") + else: + campo = "" + linha.append(campo) else: @@ -93,45 +129,44 @@ def exporta_contas_csv(self) -> BinaryIO: self.envia_arquivo_central_download(tmp) - def filtra_range_data(self, field): - if self.data_inicio and self.data_final: - self.data_inicio = datetime.strptime( - f"{self.data_inicio} 00:00:00", "%Y-%m-%d %H:%M:%S" - ) - self.data_final = datetime.strptime( - f"{self.data_final} 23:59:59", "%Y-%m-%d %H:%M:%S" - ) + def filtra_range_data(self, field) -> QuerySet: + import datetime + + # Converte as datas inicial e final de texto para date + inicio = datetime.datetime.strptime(self.data_inicio, "%Y-%m-%d").date() if self.data_inicio else None + final = datetime.datetime.strptime(self.data_final, "%Y-%m-%d").date() if self.data_final else None + # Define o horário da data_final para o último momento do dia + # Sem isso o filtro pode não incluir todos os registros do dia + final = make_aware(datetime.datetime.combine(final, datetime.time.max)) if final else None + + if inicio and final: self.queryset = self.queryset.filter( - **{f"{field}__range": [self.data_inicio, self.data_final]} + **{f'{field}__gte': inicio, f'{field}__lte': final} ) - elif self.data_inicio and not self.data_final: - self.data_inicio = datetime.strptime( - f"{self.data_inicio} 00:00:00", "%Y-%m-%d %H:%M:%S" + elif inicio and not final: + self.queryset = self.queryset.filter( + **{f'{field}__gte': inicio} ) - - self.queryset = self.queryset.filter(**{f"{field}__gt": self.data_inicio}) - - elif self.data_final and not self.data_inicio: - self.data_final = datetime.strptime( - f"{self.data_final} 23:59:59", "%Y-%m-%d %H:%M:%S" + elif final and not inicio: + self.queryset = self.queryset.filter( + **{f'{field}__lte': final} ) - - self.queryset = self.queryset.filter(**{f"{field}__lt": self.data_final}) return self.queryset def cria_registro_central_download(self): logger.info(f"Criando registro na central de download") obj = gerar_arquivo_download( self.user, - self.nome_arquivo + self.nome_arquivo, + self.texto_filtro_aplicado ) self.objeto_arquivo_download = obj def texto_rodape(self): data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - texto = f"Arquivo gerado pelo {self.ambiente} em {data_hora_geracao}" + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" return texto @@ -139,7 +174,6 @@ def cria_rodape(self, write): rodape = [] texto = self.texto_rodape() - rodape.append("\n") write.writerow(rodape) rodape.clear() @@ -147,6 +181,10 @@ def cria_rodape(self, write): write.writerow(rodape) rodape.clear() + rodape.append(self.texto_filtro_aplicado) + write.writerow(rodape) + rodape.clear() + def envia_arquivo_central_download(self, tmp) -> None: try: logger.info("Salvando arquivo download...") diff --git a/sme_ptrf_apps/sme/services/exporta_dados_creditos_service.py b/sme_ptrf_apps/sme/services/exporta_dados_creditos_service.py index 4494de222..696f6bd42 100644 --- a/sme_ptrf_apps/sme/services/exporta_dados_creditos_service.py +++ b/sme_ptrf_apps/sme/services/exporta_dados_creditos_service.py @@ -1,9 +1,14 @@ import csv -import datetime +import datetime, time import logging +from tempfile import NamedTemporaryFile +from typing import BinaryIO + +from django.utils.timezone import make_aware from django.core.files import File from django.db.models import QuerySet + from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload from sme_ptrf_apps.core.services.arquivo_download_service import ( gerar_arquivo_download @@ -12,8 +17,7 @@ APLICACAO_NOMES ) from sme_ptrf_apps.utils.built_in_custom import get_recursive_attr -from tempfile import NamedTemporaryFile -from typing import BinaryIO +from sme_ptrf_apps.core.models.ambiente import Ambiente CABECALHO_RECEITA = [ @@ -24,7 +28,7 @@ ('ID do crédito', 'id'), ('Data do crédito', 'data'), ('Valor do crédito', 'valor'), - ('ID da Conta Associação', 'associacao__id'), + ('ID da Conta Associação', 'conta_associacao__id'), ('ID do tipo de Conta', 'conta_associacao__tipo_conta__id'), ('Nome do tipo de Conta', 'conta_associacao__tipo_conta__nome'), ('ID da Ação Associação', 'acao_associacao__id'), @@ -32,12 +36,12 @@ ('Nome da Ação', 'acao_associacao__acao__nome'), ('ID do tipo de receita', 'tipo_receita__id'), ('Nome do tipo de receita', 'tipo_receita__nome'), - ('ID da categoria de receita', 'tipo_receita__id'), - ('Nome da categoria de receita', 'tipo_receita__nome'), - ('ID do detalhe de tipo de receita', 'categoria_receita'), - ('Nome do detalhe de tipo de receita', (APLICACAO_NOMES, 'categoria_receita')), - ('Detalhe (outros)', 'tipo_receita__id'), - ('ID do período de devolução ao tesouro', 'tipo_receita__nome'), + ('ID da categoria de receita', 'categoria_receita'), + ('Nome da categoria de receita', (APLICACAO_NOMES, 'categoria_receita')), + ('ID do detalhe de tipo de receita', 'detalhe_tipo_receita__id'), + ('Nome do detalhe de tipo de receita', 'detalhe_tipo_receita__nome'), + ('Detalhe (outros)', 'detalhe_outros'), + ('ID do período de devolução ao tesouro', 'referencia_devolucao__id'), ('Referência do Período da Prestação de contas da devolução ao tesouro', 'referencia_devolucao__referencia'), ('ID da despesa referente a saída de recurso externo', 'saida_do_recurso__id'), ('ID da despesa estornada (no caso de estorno)', 'rateio_estornado__id'), @@ -58,6 +62,26 @@ logger = logging.getLogger(__name__) +def get_informacoes_download(data_inicio, data_final): + """ + Retorna uma string com as informações do download conforme a data de início e final de extração. + """ + + data_inicio = datetime.datetime.strptime(data_inicio, "%Y-%m-%d").strftime("%d/%m/%Y") if data_inicio else None + data_final = datetime.datetime.strptime(data_final, "%Y-%m-%d").strftime("%d/%m/%Y") if data_final else None + + if data_inicio and data_final: + return f"Filtro aplicado: {data_inicio} a {data_final} (data de criação do registro)" + + if data_inicio and not data_final: + return f"Filtro aplicado: A partir de {data_inicio} (data de criação do registro)" + + if data_final and not data_inicio: + return f"Filtro aplicado: Até {data_final} (data de criação do registro)" + + return "" + + class ExportacoesDadosCreditosService: def __init__(self, **kwargs) -> None: @@ -67,13 +91,41 @@ def __init__(self, **kwargs) -> None: self.nome_arquivo = kwargs.get('nome_arquivo', None) self.user = kwargs.get('user', None) self.objeto_arquivo_download = None + self.ambiente = self.get_ambiente + + @property + def get_ambiente(self): + ambiente = Ambiente.objects.first() + return ambiente.prefixo if ambiente else "" def exporta_creditos_principal(self): self.cria_registro_central_download() self.cabecalho = CABECALHO_RECEITA[0] - self.filtra_range_data('data') + self.filtra_range_data('criado_em') self.exporta_credito_csv() + def cria_rodape(self, write): + rodape = [] + texto_info_arquivo_gerado = self.texto_info_arquivo_gerado() + + rodape.append(" ") + write.writerow(rodape) + rodape.clear() + + rodape.append(texto_info_arquivo_gerado) + write.writerow(rodape) + rodape.clear() + + rodape.append(get_informacoes_download(self.data_inicio, self.data_final)) + write.writerow(rodape) + rodape.clear() + + def texto_info_arquivo_gerado(self): + data_hora_geracao = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" + + return texto + def exporta_creditos_motivos_estorno(self): self.cria_registro_central_download() self.cabecalho = CABECALHO_MOTIVOS_ESTORNO[0] @@ -91,11 +143,11 @@ def exporta_credito_csv(self) -> BinaryIO: ) as tmp: write = csv.writer(tmp.file, delimiter=";") write.writerow([cabecalho[0] for cabecalho in self.cabecalho]) - + for instance in self.queryset: - + motivos = list(instance.motivos_estorno.all()) - + for _, campo in self.cabecalho: if campo == 'data': @@ -105,7 +157,7 @@ def exporta_credito_csv(self) -> BinaryIO: elif campo == 'valor': campo = str(getattr(instance, campo)).replace(".", ",") linha.append(campo) - + elif campo == 'outros_motivos_estorno': motivo_string = '; '.join(str(motivo) for motivo in motivos) if(len(motivo_string)): @@ -131,29 +183,42 @@ def exporta_credito_csv(self) -> BinaryIO: logger.info(f"Escrevendo linha {linha} do crédito {instance.id}.") write.writerow(linha) if linha else None linha.clear() + + self.cria_rodape(write) + self.envia_arquivo_central_download(tmp) def filtra_range_data(self, field) -> QuerySet: - if self.data_inicio and self.data_final: + # Converte as datas inicial e final de texto para date + inicio = datetime.datetime.strptime(self.data_inicio, "%Y-%m-%d").date() if self.data_inicio else None + final = datetime.datetime.strptime(self.data_final, "%Y-%m-%d").date() if self.data_final else None + + # Define o horário da data_final para o último momento do dia + # Sem isso o filtro pode não incluir todos os registros do dia + final = make_aware(datetime.datetime.combine(final, datetime.time.max)) if final else None + + if inicio and final: self.queryset = self.queryset.filter( - **{f'{field}__range': [self.data_inicio, self.data_final]} + **{f'{field}__gte': inicio, f'{field}__lte': final} ) - elif self.data_inicio and not self.data_final: + elif inicio and not final: self.queryset = self.queryset.filter( - **{f'{field}__gt': self.data_inicio} + **{f'{field}__gte': inicio} ) - elif self.data_final and not self.data_inicio: + elif final and not inicio: self.queryset = self.queryset.filter( - **{f'{field}__lt': self.data_final} + **{f'{field}__lte': final} ) return self.queryset - - def cria_registro_central_download(self): + + def cria_registro_central_download(self): logger.info(f"Criando registro na central de download") - - obj = gerar_arquivo_download( - self.user, - self.nome_arquivo ) + + obj = gerar_arquivo_download( + self.user, + self.nome_arquivo, + informacoes=get_informacoes_download(self.data_inicio, self.data_final) + ) self.objeto_arquivo_download = obj def envia_arquivo_central_download(self, tmp) -> None: diff --git a/sme_ptrf_apps/sme/services/exporta_dados_membros_apm_service.py b/sme_ptrf_apps/sme/services/exporta_dados_membros_apm_service.py new file mode 100644 index 000000000..dc2b9a4e5 --- /dev/null +++ b/sme_ptrf_apps/sme/services/exporta_dados_membros_apm_service.py @@ -0,0 +1,246 @@ +import csv +import logging + +from datetime import datetime + +from django.core.files import File +from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload +from sme_ptrf_apps.mandatos.models.cargo_composicao import CargoComposicao +from sme_ptrf_apps.mandatos.models.ocupante_cargo import OcupanteCargo +from sme_ptrf_apps.core.models.ambiente import Ambiente +from sme_ptrf_apps.core.services.arquivo_download_service import gerar_arquivo_download + +from sme_ptrf_apps.utils.built_in_custom import get_recursive_attr +from tempfile import NamedTemporaryFile + +CABECALHO_MEMBROS_APM = ( + [ + ("Código EOL", "composicao__associacao__unidade__codigo_eol"), + ("Tipo da unidade", "composicao__associacao__unidade__tipo_unidade"), + ("Nome da Unidade", "composicao__associacao__unidade__nome"), + ("Nome da Associação", "composicao__associacao__nome"), + ("CNPJ", "composicao__associacao__cnpj"), + ("DRE", "composicao__associacao__unidade__dre__nome"), + ("Mandato", "composicao__mandato__referencia_mandato"), + ("Data inicial do mandato", "composicao__mandato__data_inicial"), + ("Data final do mandato", "composicao__mandato__data_final"), + ("Composição data inicial", "composicao__data_inicial"), + ("Composição data final", "composicao__data_final"), + ("Cargo", "cargo_associacao"), + ("Nome", "ocupante_do_cargo__nome"), + ("Número de identificação", "ocupante_do_cargo__cpf_responsavel"), + ("Representação", "ocupante_do_cargo__representacao"), + ("Período inicial de ocupação", "data_inicio_no_cargo"), + ("Período final de ocupação", "data_fim_no_cargo"), + ], +) + +logger = logging.getLogger(__name__) + + +class ExportacaoDadosMembrosApmService: + def __init__(self, **kwargs) -> None: + self.cabecalho = CABECALHO_MEMBROS_APM[0] + self.queryset = kwargs.get("queryset", None) + self.data_inicio = kwargs.get("data_inicio", None) + self.data_final = kwargs.get("data_final", None) + self.nome_arquivo = kwargs.get("nome_arquivo", None) + self.user = kwargs.get("user", None) + self.ambiente = self.get_ambiente + self.objeto_arquivo_download = None + self.texto_filtro_aplicado = self.get_texto_filtro_aplicado() + + @property + def get_ambiente(self): + ambiente = Ambiente.objects.first() + return ambiente.prefixo if ambiente else "" + + def get_texto_filtro_aplicado(self): + if self.data_inicio and self.data_final: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + + return f"Filtro aplicado: {data_inicio_formatada} a {data_final_formatada} (data de criação do registro)" + + if self.data_inicio: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: A partir de {data_inicio_formatada} (data de criação do registro)" + + if self.data_final: + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: Até {data_final_formatada} (data de criação do registro)" + + return "" + + def exporta_membros_apm(self): + self.cria_registro_central_download() + self.filtra_range_data("criado_em") + self.exporta_membros_apm_csv() + + + def exporta_membros_apm_csv(self): + dados = self.monta_dados() + + with NamedTemporaryFile( + mode="r+", + newline='', + encoding='utf-8', + prefix=self.nome_arquivo, + suffix='.csv' + ) as tmp: + write = csv.writer(tmp.file, delimiter=";") + write.writerow([cabecalho[0] for cabecalho in self.cabecalho]) + + for linha in dados: + write.writerow(linha) if linha else None + + self.cria_rodape(write) + self.envia_arquivo_central_download(tmp) + + def monta_dados(self): + linhas_vertical = [] + + for instance in self.queryset: + logger.info(f"Iniciando extração de dados de membros apm, id: {instance.id}.") + + if not CargoComposicao.objects.filter(id=instance.id).exists(): + logger.info(f"Este registro não existe mais na base de dados, portanto será pulado") + continue + + linha_horizontal = [] + + for _, campo in self.cabecalho: + if campo == "composicao__mandato__data_inicial": + campo = get_recursive_attr(instance, campo) + data_inicial_formatada = campo.strftime("%d/%m/%Y") + linha_horizontal.append(data_inicial_formatada) + continue + if campo == "composicao__mandato__data_final": + campo = get_recursive_attr(instance, campo) + data_final_formatada = campo.strftime("%d/%m/%Y") + linha_horizontal.append(data_final_formatada) + continue + if campo == "composicao__data_inicial": + campo = get_recursive_attr(instance, campo) + data_inicial_formatada = campo.strftime("%d/%m/%Y") + linha_horizontal.append(data_inicial_formatada) + continue + if campo == "composicao__data_final": + campo = get_recursive_attr(instance, campo) + data_final_formatada = campo.strftime("%d/%m/%Y") + linha_horizontal.append(data_final_formatada) + continue + if campo == "ocupante_do_cargo__cpf_responsavel": + representacao = get_recursive_attr(instance, 'ocupante_do_cargo__representacao') + if representacao == OcupanteCargo.REPRESENTACAO_CARGO_SERVIDOR: + campo = get_recursive_attr(instance, 'ocupante_do_cargo__codigo_identificacao') + linha_horizontal.append(campo) + continue + campo = get_recursive_attr(instance, 'ocupante_do_cargo__cpf_responsavel').replace(".", "").replace("-", "").replace(" ", "") + if len(campo) < 10: + masked_cpf = "" + else: + start = campo[:3] + end = campo[-2:] + middle = "X" * (len(campo) - len(start) - len(end)) + masked_cpf = start + middle + end + linha_horizontal.append(masked_cpf) + continue + if campo == "data_inicio_no_cargo": + campo = get_recursive_attr(instance, campo) + data_inicial_formatada = campo.strftime("%d/%m/%Y") + linha_horizontal.append(data_inicial_formatada) + continue + if campo == "data_fim_no_cargo": + campo = get_recursive_attr(instance, campo) + if campo: + data_final_formatada = campo.strftime("%d/%m/%Y") + linha_horizontal.append(data_final_formatada) + continue + linha_horizontal.append('') + + campo = get_recursive_attr(instance, campo) + linha_horizontal.append(campo) + + logger.info(f"Escrevendo linha {linha_horizontal} de membros apm, id: {instance.id}.") + linhas_vertical.append(linha_horizontal) + logger.info(f"Finalizando extração de dados de membros apm, id: {instance.id}.") + + return linhas_vertical + + def filtra_range_data(self, field): + if self.data_inicio and self.data_final: + self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') + self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') + + self.queryset = self.queryset.filter( + **{f'{field}__range': [self.data_inicio, self.data_final]} + ) + elif self.data_inicio and not self.data_final: + self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') + + self.queryset = self.queryset.filter( + **{f'{field}__gt': self.data_inicio} + ) + + elif self.data_final and not self.data_inicio: + self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') + + self.queryset = self.queryset.filter( + **{f'{field}__lt': self.data_final} + ) + return self.queryset + + def cria_registro_central_download(self): + logger.info(f"Criando registro na central de download") + + obj = gerar_arquivo_download( + self.user, + self.nome_arquivo, + self.texto_filtro_aplicado + ) + + self.objeto_arquivo_download = obj + + def envia_arquivo_central_download(self, tmp): + try: + logger.info("Salvando arquivo download...") + self.objeto_arquivo_download.arquivo.save( + name=self.objeto_arquivo_download.identificador, + content=File(tmp) + ) + self.objeto_arquivo_download.status = ArquivoDownload.STATUS_CONCLUIDO + self.objeto_arquivo_download.save() + logger.info("Arquivo salvo com sucesso...") + + except Exception as e: + self.objeto_arquivo_download.status = ArquivoDownload.STATUS_ERRO + self.objeto_arquivo_download.msg_erro = str(e) + self.objeto_arquivo_download.save() + logger.error("Erro arquivo download...") + + def cria_rodape(self, write): + rodape = [] + texto_info_arquivo_gerado = self.texto_info_arquivo_gerado() + + write.writerow(rodape) + rodape.clear() + + rodape.append(texto_info_arquivo_gerado) + write.writerow(rodape) + rodape.clear() + + rodape.append(self.texto_filtro_aplicado) + write.writerow(rodape) + rodape.clear() + + def texto_info_arquivo_gerado(self): + data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" + + return texto diff --git a/sme_ptrf_apps/sme/services/exporta_dados_processos_sei_regularidade_service.py b/sme_ptrf_apps/sme/services/exporta_dados_processos_sei_regularidade_service.py new file mode 100644 index 000000000..2f1fc4e28 --- /dev/null +++ b/sme_ptrf_apps/sme/services/exporta_dados_processos_sei_regularidade_service.py @@ -0,0 +1,124 @@ +import csv +import logging + +from datetime import datetime + +from django.core.files import File +from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload +from sme_ptrf_apps.core.models.ambiente import Ambiente +from sme_ptrf_apps.core.services.arquivo_download_service import gerar_arquivo_download + +from sme_ptrf_apps.utils.built_in_custom import get_recursive_attr +from tempfile import NamedTemporaryFile + +CABECALHO_PROCESSOS_SEI_REGULARIDADE = ( + [ + ("Código EOL", "unidade__codigo_eol"), + ("Nome Unidade", "unidade__nome"), + ("Nome Associação", "nome"), + ("CNPj da Associação", "cnpj"), + ("DRE", "unidade__dre__nome"), + ("Número do processo SEI de regularidade", "processo_regularidade"), + ], +) + +logger = logging.getLogger(__name__) + + +class ExportaDadosProcessosSeiRegularidadeService: + def __init__(self, **kwargs) -> None: + self.cabecalho = CABECALHO_PROCESSOS_SEI_REGULARIDADE[0] + self.queryset = kwargs.get("queryset", None) + self.nome_arquivo = kwargs.get("nome_arquivo", None) + self.user = kwargs.get("user", None) + self.ambiente = self.get_ambiente + self.objeto_arquivo_download = None + + @property + def get_ambiente(self): + ambiente = Ambiente.objects.first() + return ambiente.prefixo if ambiente else "" + + def exportar(self): + self.cria_registro_central_download() + self.exportar_csv() + + def exportar_csv(self): + dados = self.monta_dados() + + with NamedTemporaryFile( + mode="r+", + newline='', + encoding='utf-8', + prefix=self.nome_arquivo, + suffix='.csv' + ) as tmp: + write = csv.writer(tmp.file, delimiter=";") + write.writerow([cabecalho[0] for cabecalho in self.cabecalho]) + + for linha in dados: + write.writerow(linha) if linha else None + + self.cria_rodape(write) + self.envia_arquivo_central_download(tmp) + + def monta_dados(self): + linhas_vertical = [] + + for instance in self.queryset: + logger.info(f"Iniciando extração de dados processos SEI regularidade, id: {instance.id}.") + linha_horizontal = [] + + for _, campo in self.cabecalho: + campo = get_recursive_attr(instance, campo) + linha_horizontal.append(campo) + + logger.info(f"Escrevendo linha {linha_horizontal}, id: {instance.id}.") + linhas_vertical.append(linha_horizontal) + logger.info(f"Finalizando extração de dados, id: {instance.id}.") + + return linhas_vertical + + def cria_registro_central_download(self): + logger.info(f"Criando registro na central de download") + + obj = gerar_arquivo_download( + self.user, + self.nome_arquivo, + ) + + self.objeto_arquivo_download = obj + + def envia_arquivo_central_download(self, tmp): + try: + logger.info("Salvando arquivo download...") + self.objeto_arquivo_download.arquivo.save( + name=self.objeto_arquivo_download.identificador, + content=File(tmp) + ) + self.objeto_arquivo_download.status = ArquivoDownload.STATUS_CONCLUIDO + self.objeto_arquivo_download.save() + logger.info("Arquivo salvo com sucesso...") + + except Exception as e: + self.objeto_arquivo_download.status = ArquivoDownload.STATUS_ERRO + self.objeto_arquivo_download.msg_erro = str(e) + self.objeto_arquivo_download.save() + logger.error("Erro arquivo download...") + + def cria_rodape(self, write): + rodape = [] + texto_info_arquivo_gerado = self.texto_info_arquivo_gerado() + + write.writerow(rodape) + rodape.clear() + + rodape.append(texto_info_arquivo_gerado) + write.writerow(rodape) + rodape.clear() + + def texto_info_arquivo_gerado(self): + data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" + + return texto diff --git a/sme_ptrf_apps/sme/services/exporta_demonstrativos_financeiros_service.py b/sme_ptrf_apps/sme/services/exporta_demonstrativos_financeiros_service.py index b7c0776e5..0d9a8a362 100644 --- a/sme_ptrf_apps/sme/services/exporta_demonstrativos_financeiros_service.py +++ b/sme_ptrf_apps/sme/services/exporta_demonstrativos_financeiros_service.py @@ -3,10 +3,12 @@ import logging from django.core.files import File - +from django.utils.timezone import make_aware +from django.db.models import QuerySet from sme_ptrf_apps.core.models import ObservacaoConciliacao from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload from sme_ptrf_apps.core.models.ambiente import Ambiente +from sme_ptrf_apps.core.models.demonstrativo_financeiro import DemonstrativoFinanceiro from sme_ptrf_apps.core.services.arquivo_download_service import ( gerar_arquivo_download ) @@ -20,6 +22,7 @@ ('Código EOL', 'conta_associacao__associacao__unidade__codigo_eol'), ('Nome Unidade', 'conta_associacao__associacao__unidade__nome'), ('Nome Associação', 'conta_associacao__associacao__nome'), + ('DRE', 'conta_associacao__associacao__unidade__dre__nome'), ('Referência do Período da PC', 'prestacao_conta__periodo__referencia'), ('Nome do tipo de Conta', 'conta_associacao__tipo_conta__nome'), ('Data (Saldo bancário)', 'DATA_SALDO_BANCARIO'), @@ -32,6 +35,7 @@ ('Data e hora da última atualização', 'alterado_em'), ] + class ExportaDemonstrativosFinanceirosService: def __init__(self, **kwargs): @@ -43,52 +47,75 @@ def __init__(self, **kwargs): self.cabecalho = CABECALHO_DEMONSTATIVOS_FINANCEIROS self.ambiente = self.get_ambiente self.objeto_arquivo_download = None + self.texto_filtro_aplicado = self.get_texto_filtro_aplicado() @property def get_ambiente(self): ambiente = Ambiente.objects.first() return ambiente.prefixo if ambiente else "" + def get_texto_filtro_aplicado(self): + if self.data_inicio and self.data_final: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + + return f"Filtro aplicado: {data_inicio_formatada} a {data_final_formatada} (data de criação do registro)" + + if self.data_inicio: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: A partir de {data_inicio_formatada} (data de criação do registro)" + + if self.data_final: + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: Até {data_final_formatada} (data de criação do registro)" + + return "" + def exporta_demonstrativos_financeiros(self): self.cria_registro_central_download() self.filtra_range_data('criado_em') self.exporta_demonstrativos_financeiros_csv() - def cria_registro_central_download(self): logger.info(f"Criando registro na central de download") obj = gerar_arquivo_download( self.user, - self.nome_arquivo + self.nome_arquivo, + self.texto_filtro_aplicado ) self.objeto_arquivo_download = obj + def filtra_range_data(self, field) -> QuerySet: + import datetime - def filtra_range_data(self, field): - if self.data_inicio and self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') + # Converte as datas inicial e final de texto para date + inicio = datetime.datetime.strptime(self.data_inicio, "%Y-%m-%d").date() if self.data_inicio else None + final = datetime.datetime.strptime(self.data_final, "%Y-%m-%d").date() if self.data_final else None + + # Define o horário da data_final para o último momento do dia + # Sem isso o filtro pode não incluir todos os registros do dia + final = make_aware(datetime.datetime.combine(final, datetime.time.max)) if final else None + if inicio and final: self.queryset = self.queryset.filter( - **{f'{field}__range': [self.data_inicio, self.data_final]} + **{f'{field}__gte': inicio, f'{field}__lte': final} ) - elif self.data_inicio and not self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') - + elif inicio and not final: self.queryset = self.queryset.filter( - **{f'{field}__gt': self.data_inicio} + **{f'{field}__gte': inicio} ) - - elif self.data_final and not self.data_inicio: - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') - + elif final and not inicio: self.queryset = self.queryset.filter( - **{f'{field}__lt': self.data_final} + **{f'{field}__lte': final} ) return self.queryset - def exporta_demonstrativos_financeiros_csv(self): dados = self.monta_dados() @@ -130,6 +157,11 @@ def monta_dados(self): for instance in self.queryset: logger.info(f"Iniciando extração de dados do demonstrativo financeiro : {instance.id}.") + + if not DemonstrativoFinanceiro.objects.filter(id=instance.id).exists(): + logger.info(f"Este registro não existe mais na base de dados, portanto será pulado") + continue + linha_horizontal = [] for _, campo in self.cabecalho: @@ -209,7 +241,6 @@ def monta_dados(self): linha_horizontal.append(texto) continue - campo = get_recursive_attr(instance, campo) linha_horizontal.append(campo) @@ -219,10 +250,9 @@ def monta_dados(self): return linhas_vertical - def texto_rodape(self): data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - texto = f"Arquivo gerado pelo {self.ambiente} em {data_hora_geracao}" + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" return texto @@ -230,7 +260,6 @@ def cria_rodape(self, write): rodape = [] texto = self.texto_rodape() - rodape.append("\n") write.writerow(rodape) rodape.clear() @@ -238,6 +267,10 @@ def cria_rodape(self, write): write.writerow(rodape) rodape.clear() + rodape.append(self.texto_filtro_aplicado) + write.writerow(rodape) + rodape.clear() + def envia_arquivo_central_download(self, tmp): try: logger.info("Salvando arquivo download...") diff --git a/sme_ptrf_apps/sme/services/exporta_documentos_despesas.py b/sme_ptrf_apps/sme/services/exporta_documentos_despesas.py index 32b8f2088..28979bc7d 100644 --- a/sme_ptrf_apps/sme/services/exporta_documentos_despesas.py +++ b/sme_ptrf_apps/sme/services/exporta_documentos_despesas.py @@ -15,34 +15,35 @@ logger = logging.getLogger(__name__) CABECALHO_DOCS = [ - ('Código EOL', 'associacao__unidade__codigo_eol'), - ('Nome unidade', 'associacao__unidade__nome'), - ('Nome associação', 'associacao__nome'), - ('DRE', 'associacao__unidade__dre__nome'), - ('ID do gasto', 'id'), - ('É despesa sem comprovação fiscal?', 'eh_despesa_sem_comprovacao_fiscal'), - ('É despesa reconhecida pela Associação?', 'eh_despesa_reconhecida_pela_associacao'), - ('Número do documento', 'numero_documento'), - ('Tipo de documento', 'tipo_documento__nome'), - ('Data do documento', 'data_documento'), - ('CPF_CNPJ do fornecedor', 'cpf_cnpj_fornecedor'), - ('Nome do fornecedor', 'nome_fornecedor'), - ('Tipo de transação', 'tipo_transacao__nome'), - ('Número do documento de transação', 'documento_transacao'), - ('Data da transação', 'data_transacao'), - ('Valor total do documento', 'valor_total'), - ('Valor realizado', 'valor_original'), - ('Valor pago com recursos próprios', 'valor_recursos_proprios'), - ('Número do Boletim de Ocorrência', 'numero_boletim_de_ocorrencia'), - ('Retem impostos?', 'retem_imposto'), - ('Descrição do motivo de pagamento antecipado', 'motivos'), - ('É saída de recurso externo?', 'saida_recurso_externo'), - ('Status do gasto', 'status'), - ('Data e hora de criação', 'criado_em'), - ('Data e hora da última atualização', 'alterado_em'), - ('UUID do gasto', 'uuid') + ('Código EOL', 'associacao__unidade__codigo_eol'), + ('Nome unidade', 'associacao__unidade__nome'), + ('Nome associação', 'associacao__nome'), + ('DRE', 'associacao__unidade__dre__nome'), + ('ID do gasto', 'id'), + ('É despesa sem comprovação fiscal?', 'eh_despesa_sem_comprovacao_fiscal'), + ('É despesa reconhecida pela Associação?', 'eh_despesa_reconhecida_pela_associacao'), + ('Número do documento', 'numero_documento'), + ('Tipo de documento', 'tipo_documento__nome'), + ('Data do documento', 'data_documento'), + ('CPF_CNPJ do fornecedor', 'cpf_cnpj_fornecedor'), + ('Nome do fornecedor', 'nome_fornecedor'), + ('Tipo de transação', 'tipo_transacao__nome'), + ('Número do documento de transação', 'documento_transacao'), + ('Data da transação', 'data_transacao'), + ('Valor total do documento', 'valor_total'), + ('Valor realizado', 'valor_original'), + ('Valor pago com recursos próprios', 'valor_recursos_proprios'), + ('Número do Boletim de Ocorrência', 'numero_boletim_de_ocorrencia'), + ('Retem impostos?', 'retem_imposto'), + ('Descrição do motivo de pagamento antecipado', 'motivos'), + ('É saída de recurso externo?', 'saida_recurso_externo'), + ('Status do gasto', 'status'), + ('Data e hora de criação', 'criado_em'), + ('Data e hora da última atualização', 'alterado_em'), + ('UUID do gasto', 'uuid') ] + class ExportacoesDocumentosDespesasService: def __init__(self, **kwargs): @@ -54,12 +55,32 @@ def __init__(self, **kwargs): self.cabecalho = CABECALHO_DOCS self.ambiente = self.get_ambiente self.objeto_arquivo_download = None + self.informacoes_download = self.get_informacoes_download() @property def get_ambiente(self): ambiente = Ambiente.objects.first() return ambiente.prefixo if ambiente else "" + def get_informacoes_download(self): + """ + Retorna uma string com as informações do download conforme a data de início e final de extração. + """ + + data_inicio = datetime.strptime(self.data_inicio, "%Y-%m-%d").strftime("%d/%m/%Y") if self.data_inicio else None + data_final = datetime.strptime(self.data_final, "%Y-%m-%d").strftime("%d/%m/%Y") if self.data_final else None + + if data_inicio and data_final: + return f"Filtro aplicado: {data_inicio} a {data_final} (data de criação do registro)" + + if data_inicio and not data_final: + return f"Filtro aplicado: A partir de {data_inicio} (data de criação do registro)" + + if data_final and not data_inicio: + return f"Filtro aplicado: Até {data_final} (data de criação do registro)" + + return "" + def exporta_despesas(self): self.cria_registro_central_download() self.filtra_range_data('criado_em') @@ -116,13 +137,13 @@ def monta_dados(self): valor_total_formatado = str(campo).replace(".", ",") if campo is not None else '' linha_horizontal.append(valor_total_formatado) continue - + if campo == "valor_original": campo = get_recursive_attr(instance, campo) valor_original_formatado = str(campo).replace(".", ",") if campo is not None else '' linha_horizontal.append(valor_original_formatado) continue - + if campo == "valor_recursos_proprios": campo = get_recursive_attr(instance, campo) valor_recursos_proprios_formatado = str(campo).replace(".", ",") if campo is not None else '' @@ -164,9 +185,9 @@ def monta_dados(self): if campo == "motivos": motivo_string = '; '.join(str(motivo) for motivo in motivos) - if(len(motivo_string)): + if (len(motivo_string)): motivo_string = motivo_string + '; ' + instance.outros_motivos_pagamento_antecipado - elif(len(instance.outros_motivos_pagamento_antecipado)): + elif (len(instance.outros_motivos_pagamento_antecipado)): motivo_string = instance.outros_motivos_pagamento_antecipado linha_horizontal.append(motivo_string) continue @@ -178,7 +199,7 @@ def monta_dados(self): campo = get_recursive_attr(instance, campo) linha_horizontal.append(campo) - + logger.info(f"Escrevendo linha {linha_horizontal} de despesas, despesa id: {instance.id}.") linhas_vertical.append(linha_horizontal) @@ -213,7 +234,8 @@ def cria_registro_central_download(self): logger.info(f"Criando registro na central de download") obj = gerar_arquivo_download( self.user, - self.nome_arquivo + self.nome_arquivo, + self.informacoes_download ) self.objeto_arquivo_download = obj @@ -237,7 +259,7 @@ def envia_arquivo_central_download(self, tmp): def texto_rodape(self): data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - texto = f"Arquivo gerado pelo {self.ambiente} em {data_hora_geracao}" + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" return texto @@ -245,10 +267,13 @@ def cria_rodape(self, write): rodape = [] texto = self.texto_rodape() - rodape.append("\n") write.writerow(rodape) rodape.clear() rodape.append(texto) write.writerow(rodape) - rodape.clear() \ No newline at end of file + rodape.clear() + + rodape.append(self.informacoes_download) + write.writerow(rodape) + rodape.clear() diff --git a/sme_ptrf_apps/sme/services/exporta_rateios_service.py b/sme_ptrf_apps/sme/services/exporta_rateios_service.py index 3e30491f7..abb332b05 100644 --- a/sme_ptrf_apps/sme/services/exporta_rateios_service.py +++ b/sme_ptrf_apps/sme/services/exporta_rateios_service.py @@ -2,9 +2,12 @@ from datetime import datetime import logging +from django.utils.timezone import make_aware +from django.db.models import QuerySet from django.core.files import File from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload from sme_ptrf_apps.core.models.ambiente import Ambiente +from sme_ptrf_apps.despesas.models.rateio_despesa import RateioDespesa from sme_ptrf_apps.core.services.arquivo_download_service import ( gerar_arquivo_download ) @@ -18,6 +21,7 @@ ('Código EOL', 'associacao__unidade__codigo_eol'), ('Nome Unidade', 'associacao__unidade__nome'), ('Nome Associação', 'associacao__nome'), + ('DRE', 'associacao__unidade__dre__nome'), ('ID do Gasto', 'despesa__id'), ('Número do documento', 'despesa__numero_documento'), ('Tipo de documento', 'despesa__tipo_documento__nome'), @@ -62,12 +66,35 @@ def __init__(self, **kwargs): self.cabecalho = CABECALHO_RATEIOS self.ambiente = self.get_ambiente self.objeto_arquivo_download = None + self.texto_filtro_aplicado = self.get_texto_filtro_aplicado() @property def get_ambiente(self): ambiente = Ambiente.objects.first() return ambiente.prefixo if ambiente else "" + def get_texto_filtro_aplicado(self): + if self.data_inicio and self.data_final: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + + return f"Filtro aplicado: {data_inicio_formatada} a {data_final_formatada} (data de criação do registro)" + + if self.data_inicio: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: A partir de {data_inicio_formatada} (data de criação do registro)" + + if self.data_final: + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: Até {data_final_formatada} (data de criação do registro)" + + return "" + def exporta_rateios(self): self.cria_registro_central_download() self.filtra_range_data('despesa__criado_em') @@ -97,6 +124,11 @@ def monta_dados(self): for instance in self.queryset: logger.info(f"Iniciando extração de dados de rateios, rateio id: {instance.id}.") + + if not RateioDespesa.objects.filter(id=instance.id).exists(): + logger.info(f"Este registro não existe mais na base de dados, portanto será pulado") + continue + linha_horizontal = [] for _, campo in self.cabecalho: @@ -182,26 +214,28 @@ def monta_dados(self): return linhas_vertical - def filtra_range_data(self, field): - if self.data_inicio and self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') + def filtra_range_data(self, field) -> QuerySet: + import datetime + # Converte as datas inicial e final de texto para date + inicio = datetime.datetime.strptime(self.data_inicio, "%Y-%m-%d").date() if self.data_inicio else None + final = datetime.datetime.strptime(self.data_final, "%Y-%m-%d").date() if self.data_final else None + + # Define o horário da data_final para o último momento do dia + # Sem isso o filtro pode não incluir todos os registros do dia + final = make_aware(datetime.datetime.combine(final, datetime.time.max)) if final else None + + if inicio and final: self.queryset = self.queryset.filter( - **{f'{field}__range': [self.data_inicio, self.data_final]} + **{f'{field}__gte': inicio, f'{field}__lte': final} ) - elif self.data_inicio and not self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') - + elif inicio and not final: self.queryset = self.queryset.filter( - **{f'{field}__gt': self.data_inicio} + **{f'{field}__gte': inicio} ) - - elif self.data_final and not self.data_inicio: - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') - + elif final and not inicio: self.queryset = self.queryset.filter( - **{f'{field}__lt': self.data_final} + **{f'{field}__lte': final} ) return self.queryset @@ -209,7 +243,8 @@ def cria_registro_central_download(self): logger.info(f"Criando registro na central de download") obj = gerar_arquivo_download( self.user, - self.nome_arquivo + self.nome_arquivo, + self.texto_filtro_aplicado ) self.objeto_arquivo_download = obj @@ -231,10 +266,9 @@ def envia_arquivo_central_download(self, tmp): self.objeto_arquivo_download.save() logger.error("Erro arquivo download...") - def texto_rodape(self): data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - texto = f"Arquivo gerado pelo {self.ambiente} em {data_hora_geracao}" + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" return texto @@ -242,10 +276,14 @@ def cria_rodape(self, write): rodape = [] texto = self.texto_rodape() - rodape.append("\n") write.writerow(rodape) rodape.clear() rodape.append(texto) write.writerow(rodape) rodape.clear() + + rodape.append(self.texto_filtro_aplicado) + write.writerow(rodape) + rodape.clear() + diff --git a/sme_ptrf_apps/sme/services/exporta_relacao_bens_pc.py b/sme_ptrf_apps/sme/services/exporta_relacao_bens_pc.py index 2b218eae5..b5240fa6b 100644 --- a/sme_ptrf_apps/sme/services/exporta_relacao_bens_pc.py +++ b/sme_ptrf_apps/sme/services/exporta_relacao_bens_pc.py @@ -2,10 +2,13 @@ from datetime import datetime import logging +from django.utils.timezone import make_aware +from django.db.models import QuerySet from django.core.files import File from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload from sme_ptrf_apps.core.models.prestacao_conta import PrestacaoConta from sme_ptrf_apps.core.models.ambiente import Ambiente +from sme_ptrf_apps.core.models.relacao_bens import RelacaoBens from sme_ptrf_apps.core.services.arquivo_download_service import ( gerar_arquivo_download ) @@ -19,6 +22,7 @@ ('Código EOL', 'conta_associacao__associacao__unidade__codigo_eol'), ('Nome Unidade', 'conta_associacao__associacao__unidade__nome'), ('Nome Associação', 'conta_associacao__associacao__nome'), + ('DRE', 'conta_associacao__associacao__unidade__dre__nome'), ('Referência do Período da PC', 'prestacao_conta__periodo__referencia'), ('Status da PC', 'prestacao_conta__status'), ('Nome do tipo de Conta', 'conta_associacao__tipo_conta__nome'), @@ -29,6 +33,7 @@ ('Data e hora da última atualização', 'alterado_em'), ] + class ExportacoesDadosRelacaoBensService: def __init__(self, **kwargs): @@ -40,12 +45,35 @@ def __init__(self, **kwargs): self.cabecalho = CABECALHO_RELACAO_BENS self.ambiente = self.get_ambiente self.objeto_arquivo_download = None + self.texto_filtro_aplicado = self.get_texto_filtro_aplicado() @property def get_ambiente(self): ambiente = Ambiente.objects.first() return ambiente.prefixo if ambiente else "" + def get_texto_filtro_aplicado(self): + if self.data_inicio and self.data_final: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + + return f"Filtro aplicado: {data_inicio_formatada} a {data_final_formatada} (data de criação do registro)" + + if self.data_inicio: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: A partir de {data_inicio_formatada} (data de criação do registro)" + + if self.data_final: + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: Até {data_final_formatada} (data de criação do registro)" + + return "" + def exporta_relacao_bens(self): self.cria_registro_central_download() self.filtra_range_data('criado_em') @@ -75,6 +103,11 @@ def monta_dados(self): for instance in self.queryset: logger.info(f"Iniciando extração de dados de relação de bens : {instance.id}.") + + if not RelacaoBens.objects.filter(id=instance.id).exists(): + logger.info(f"Este registro não existe mais na base de dados, portanto será pulado") + continue + linha_horizontal = [] for _, campo in self.cabecalho: @@ -119,26 +152,28 @@ def monta_dados(self): return linhas_vertical - def filtra_range_data(self, field): - if self.data_inicio and self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') + def filtra_range_data(self, field) -> QuerySet: + import datetime + + # Converte as datas inicial e final de texto para date + inicio = datetime.datetime.strptime(self.data_inicio, "%Y-%m-%d").date() if self.data_inicio else None + final = datetime.datetime.strptime(self.data_final, "%Y-%m-%d").date() if self.data_final else None + + # Define o horário da data_final para o último momento do dia + # Sem isso o filtro pode não incluir todos os registros do dia + final = make_aware(datetime.datetime.combine(final, datetime.time.max)) if final else None + if inicio and final: self.queryset = self.queryset.filter( - **{f'{field}__range': [self.data_inicio, self.data_final]} + **{f'{field}__gte': inicio, f'{field}__lte': final} ) - elif self.data_inicio and not self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') - + elif inicio and not final: self.queryset = self.queryset.filter( - **{f'{field}__gt': self.data_inicio} + **{f'{field}__gte': inicio} ) - - elif self.data_final and not self.data_inicio: - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') - + elif final and not inicio: self.queryset = self.queryset.filter( - **{f'{field}__lt': self.data_final} + **{f'{field}__lte': final} ) return self.queryset @@ -146,7 +181,8 @@ def cria_registro_central_download(self): logger.info(f"Criando registro na central de download") obj = gerar_arquivo_download( self.user, - self.nome_arquivo + self.nome_arquivo, + self.texto_filtro_aplicado ) self.objeto_arquivo_download = obj @@ -168,10 +204,9 @@ def envia_arquivo_central_download(self, tmp): self.objeto_arquivo_download.save() logger.error("Erro arquivo download...") - def texto_rodape(self): data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - texto = f"Arquivo gerado pelo {self.ambiente} em {data_hora_geracao}" + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" return texto @@ -179,10 +214,13 @@ def cria_rodape(self, write): rodape = [] texto = self.texto_rodape() - rodape.append("\n") write.writerow(rodape) rodape.clear() rodape.append(texto) write.writerow(rodape) rodape.clear() + + rodape.append(self.texto_filtro_aplicado) + write.writerow(rodape) + rodape.clear() diff --git a/sme_ptrf_apps/sme/services/exporta_repasses_service.py b/sme_ptrf_apps/sme/services/exporta_repasses_service.py new file mode 100644 index 000000000..f8bcac0c6 --- /dev/null +++ b/sme_ptrf_apps/sme/services/exporta_repasses_service.py @@ -0,0 +1,238 @@ +import csv +import logging + +from datetime import datetime + +from django.core.files import File +from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload +from sme_ptrf_apps.receitas.models.repasse import Repasse +from sme_ptrf_apps.core.models.ambiente import Ambiente +from sme_ptrf_apps.core.services.arquivo_download_service import gerar_arquivo_download +from django.utils.timezone import make_aware +from django.db.models import QuerySet + +from sme_ptrf_apps.utils.built_in_custom import get_recursive_attr +from tempfile import NamedTemporaryFile + +CABECALHO_REPASSES = ( + [ + ("Código EOL", "associacao__unidade__codigo_eol"), + ("Nome Unidade", "associacao__unidade__nome"), + ("Nome Associação", "associacao__nome"), + ("DRE", "associacao__unidade__dre__nome"), + ("Período", "periodo__referencia"), + ("Nome do tipo de conta", "conta_associacao__tipo_conta__nome"), + ("Nome da Ação", "acao_associacao__acao__nome"), + ("Valor custeio", "valor_custeio"), + ("Valor capital", "valor_capital"), + ("Valor livre aplicação", "valor_livre"), + ("Realizado custeio?", "realizado_custeio"), + ("Realizado capital?", "realizado_capital"), + ("Realizado livre aplicação?", "realizado_livre"), + ("Carga origem", "carga_origem__identificador"), + ("ID da linha da carga origem", "carga_origem_linha_id"), + ('Data e hora de criação', 'criado_em'), + ], +) + +logger = logging.getLogger(__name__) + + +class ExportacaoDadosRepassesService: + def __init__(self, **kwargs) -> None: + self.cabecalho = CABECALHO_REPASSES[0] + self.queryset = kwargs.get("queryset", None) + self.data_inicio = kwargs.get("data_inicio", None) + self.data_final = kwargs.get("data_final", None) + self.nome_arquivo = kwargs.get("nome_arquivo", None) + self.user = kwargs.get("user", None) + self.ambiente = self.get_ambiente + self.objeto_arquivo_download = None + self.texto_filtro_aplicado = self.get_texto_filtro_aplicado() + + @property + def get_ambiente(self): + ambiente = Ambiente.objects.first() + return ambiente.prefixo if ambiente else "" + + def get_texto_filtro_aplicado(self): + if self.data_inicio and self.data_final: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + + return f"Filtro aplicado: {data_inicio_formatada} a {data_final_formatada} (data de criação do registro)" + + if self.data_inicio: + data_inicio_formatada = datetime.strptime(f"{self.data_inicio}", '%Y-%m-%d') + data_inicio_formatada = data_inicio_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: A partir de {data_inicio_formatada} (data de criação do registro)" + + if self.data_final: + data_final_formatada = datetime.strptime(f"{self.data_final}", '%Y-%m-%d') + data_final_formatada = data_final_formatada.strftime("%d/%m/%Y") + return f"Filtro aplicado: Até {data_final_formatada} (data de criação do registro)" + + return "" + + def exporta_repasses(self): + self.cria_registro_central_download() + self.filtra_range_data("criado_em") + self.exporta_repasses_csv() + + def exporta_repasses_csv(self): + dados = self.monta_dados() + + with NamedTemporaryFile( + mode="r+", + newline='', + encoding='utf-8', + prefix=self.nome_arquivo, + suffix='.csv' + ) as tmp: + write = csv.writer(tmp.file, delimiter=";") + write.writerow([cabecalho[0] for cabecalho in self.cabecalho]) + + for linha in dados: + write.writerow(linha) if linha else None + + self.cria_rodape(write) + self.envia_arquivo_central_download(tmp) + + def monta_dados(self): + linhas_vertical = [] + + for instance in self.queryset: + logger.info(f"Iniciando extração de dados de repasses, id: {instance.id}.") + + if not Repasse.objects.filter(id=instance.id).exists(): + logger.info(f"Este registro não existe mais na base de dados, portanto será pulado") + continue + + linha_horizontal = [] + + for _, campo in self.cabecalho: + if campo == "valor_custeio": + valor_custeio = str(getattr(instance, campo)).replace(".", ",") + linha_horizontal.append(valor_custeio) + continue + + if campo == "valor_capital": + valor_capital = str(getattr(instance, campo)).replace(".", ",") + linha_horizontal.append(valor_capital) + continue + + if campo == "valor_livre": + valor_livre = str(getattr(instance, campo)).replace(".", ",") + linha_horizontal.append(valor_livre) + continue + + if campo == "realizado_custeio": + campo = get_recursive_attr(instance, campo) + realizado_custeio = "Sim" if campo else "Não" + linha_horizontal.append(realizado_custeio) + continue + + if campo == "realizado_capital": + campo = get_recursive_attr(instance, campo) + realizado_capital = "Sim" if campo else "Não" + linha_horizontal.append(realizado_capital) + continue + + if campo == "realizado_livre": + campo = get_recursive_attr(instance, campo) + realizado_livre = "Sim" if campo else "Não" + linha_horizontal.append(realizado_livre) + continue + + if campo == "criado_em": + campo = get_recursive_attr(instance, campo) + criado_em_formatado = campo.strftime("%d/%m/%Y às %H:%M:%S") + linha_horizontal.append(criado_em_formatado) + continue + + campo = get_recursive_attr(instance, campo) + linha_horizontal.append(campo) + + logger.info(f"Escrevendo linha {linha_horizontal} de repasses, repasse id: {instance.id}.") + linhas_vertical.append(linha_horizontal) + logger.info(f"Finalizando extração de dados de repasses, repasse id: {instance.id}.") + + return linhas_vertical + + def filtra_range_data(self, field) -> QuerySet: + import datetime + + # Converte as datas inicial e final de texto para date + inicio = datetime.datetime.strptime(self.data_inicio, "%Y-%m-%d").date() if self.data_inicio else None + final = datetime.datetime.strptime(self.data_final, "%Y-%m-%d").date() if self.data_final else None + + # Define o horário da data_final para o último momento do dia + # Sem isso o filtro pode não incluir todos os registros do dia + final = make_aware(datetime.datetime.combine(final, datetime.time.max)) if final else None + + if inicio and final: + self.queryset = self.queryset.filter( + **{f'{field}__gte': inicio, f'{field}__lte': final} + ) + elif inicio and not final: + self.queryset = self.queryset.filter( + **{f'{field}__gte': inicio} + ) + elif final and not inicio: + self.queryset = self.queryset.filter( + **{f'{field}__lte': final} + ) + return self.queryset + + def cria_registro_central_download(self): + logger.info(f"Criando registro na central de download") + + obj = gerar_arquivo_download( + self.user, + self.nome_arquivo, + self.texto_filtro_aplicado + ) + + self.objeto_arquivo_download = obj + + def envia_arquivo_central_download(self, tmp): + try: + logger.info("Salvando arquivo download...") + self.objeto_arquivo_download.arquivo.save( + name=self.objeto_arquivo_download.identificador, + content=File(tmp) + ) + self.objeto_arquivo_download.status = ArquivoDownload.STATUS_CONCLUIDO + self.objeto_arquivo_download.save() + logger.info("Arquivo salvo com sucesso...") + + except Exception as e: + self.objeto_arquivo_download.status = ArquivoDownload.STATUS_ERRO + self.objeto_arquivo_download.msg_erro = str(e) + self.objeto_arquivo_download.save() + logger.error("Erro arquivo download...") + + def cria_rodape(self, write): + rodape = [] + texto_info_arquivo_gerado = self.texto_info_arquivo_gerado() + + write.writerow(rodape) + rodape.clear() + + rodape.append(texto_info_arquivo_gerado) + write.writerow(rodape) + rodape.clear() + + rodape.append(self.texto_filtro_aplicado) + write.writerow(rodape) + rodape.clear() + + def texto_info_arquivo_gerado(self): + data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" + + return texto + diff --git a/sme_ptrf_apps/sme/services/exporta_saldo_final_periodo_pc_service.py b/sme_ptrf_apps/sme/services/exporta_saldo_final_periodo_pc_service.py index 999e623d5..35a847aff 100644 --- a/sme_ptrf_apps/sme/services/exporta_saldo_final_periodo_pc_service.py +++ b/sme_ptrf_apps/sme/services/exporta_saldo_final_periodo_pc_service.py @@ -17,16 +17,16 @@ logger = logging.getLogger(__name__) CABECALHO_SALDO_FINAL_PERIODO = [ - ('Código EOL', 'associacao__unidade__codigo_eol'), - ('Nome Unidade', 'associacao__unidade__nome'), - ('Nome Associação', 'associacao__nome'), - ('DRE', 'associacao__unidade__dre__nome'), - ('Referência do Período da PC', 'periodo__referencia'), - ('Status da PC', 'prestacao_conta__status'), - ('Nome do tipo de Conta', 'conta_associacao__tipo_conta__nome'), - ('Nome da Ação', 'acao_associacao__acao__nome'), - ('Tipo de aplicação do recurso', 'TIPO_APLICACAO'), - ('Valor', 'VALOR_TIPO_APLICACAO'), + ('Código EOL', 'associacao__unidade__codigo_eol'), + ('Nome Unidade', 'associacao__unidade__nome'), + ('Nome Associação', 'associacao__nome'), + ('DRE', 'associacao__unidade__dre__nome'), + ('Referência do Período da PC', 'periodo__referencia'), + ('Status da PC', 'prestacao_conta__status'), + ('Nome do tipo de Conta', 'conta_associacao__tipo_conta__nome'), + ('Nome da Ação', 'acao_associacao__acao__nome'), + ('Tipo de aplicação do recurso', 'TIPO_APLICACAO'), + ('Valor', 'VALOR_TIPO_APLICACAO'), ] TIPOS_APLICACAO = [ @@ -35,6 +35,7 @@ ("Livre aplicação", "saldo_reprogramado_livre") ] + class ExportacoesDadosSaldosFinaisPeriodoService: def __init__(self, **kwargs): @@ -46,12 +47,32 @@ def __init__(self, **kwargs): self.cabecalho = CABECALHO_SALDO_FINAL_PERIODO self.ambiente = self.get_ambiente self.objeto_arquivo_download = None + self.informacoes_download = self.get_informacoes_download() @property def get_ambiente(self): ambiente = Ambiente.objects.first() return ambiente.prefixo if ambiente else "" + def get_informacoes_download(self): + """ + Retorna uma string com as informações do download conforme a data de início e final de extração. + """ + + data_inicio = datetime.strptime(self.data_inicio, "%Y-%m-%d").strftime("%d/%m/%Y") if self.data_inicio else None + data_final = datetime.strptime(self.data_final, "%Y-%m-%d").strftime("%d/%m/%Y") if self.data_final else None + + if data_inicio and data_final: + return f"Filtro aplicado: {data_inicio} a {data_final} (data de criação do registro)" + + if data_inicio and not data_final: + return f"Filtro aplicado: A partir de {data_inicio} (data de criação do registro)" + + if data_final and not data_inicio: + return f"Filtro aplicado: Até {data_final} (data de criação do registro)" + + return "" + def exporta_saldos_finais_periodos(self): self.cria_registro_central_download() self.filtra_range_data('criado_em') @@ -108,7 +129,8 @@ def monta_dados(self): campo = get_recursive_attr(instance, campo) linha_horizontal.append(campo) - logger.info(f"Escrevendo linha {linha_horizontal} de saldos finais do periodo, fechamento id: {instance.id}.") + logger.info( + f"Escrevendo linha {linha_horizontal} de saldos finais do periodo, fechamento id: {instance.id}.") linhas_vertical.append(linha_horizontal) logger.info(f"Finalizado extração de dados de saldos finais do periodo, fechamento id: {instance.id}.") @@ -141,7 +163,8 @@ def cria_registro_central_download(self): logger.info(f"Criando registro na central de download") obj = gerar_arquivo_download( self.user, - self.nome_arquivo + self.nome_arquivo, + self.informacoes_download ) self.objeto_arquivo_download = obj @@ -163,10 +186,9 @@ def envia_arquivo_central_download(self, tmp): self.objeto_arquivo_download.save() logger.error("Erro arquivo download...") - def texto_rodape(self): data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - texto = f"Arquivo gerado pelo {self.ambiente} em {data_hora_geracao}" + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" return texto @@ -174,10 +196,13 @@ def cria_rodape(self, write): rodape = [] texto = self.texto_rodape() - rodape.append("\n") write.writerow(rodape) rodape.clear() rodape.append(texto) write.writerow(rodape) rodape.clear() + + rodape.append(self.informacoes_download) + write.writerow(rodape) + rodape.clear() diff --git a/sme_ptrf_apps/sme/services/exporta_status_prestacoes_conta_service.py b/sme_ptrf_apps/sme/services/exporta_status_prestacoes_conta_service.py index e7a271bc1..e8a0276b9 100644 --- a/sme_ptrf_apps/sme/services/exporta_status_prestacoes_conta_service.py +++ b/sme_ptrf_apps/sme/services/exporta_status_prestacoes_conta_service.py @@ -4,10 +4,14 @@ from django.core.files import File from sme_ptrf_apps.core.models.ambiente import Ambiente from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload +from sme_ptrf_apps.core.models.associacao import Associacao +from sme_ptrf_apps.core.models.periodo import Periodo +from sme_ptrf_apps.core.models.prestacao_conta import PrestacaoConta from sme_ptrf_apps.core.services.arquivo_download_service import ( gerar_arquivo_download ) from sme_ptrf_apps.utils.built_in_custom import get_recursive_attr +from django.db.models import Q from tempfile import NamedTemporaryFile @@ -17,6 +21,7 @@ ('Código EOL', 'associacao__unidade__codigo_eol'), ('Nome Unidade', 'associacao__unidade__nome'), ('Nome Associação', 'associacao__nome'), + ('DRE', 'associacao__unidade__dre__nome'), ('Referência do Período da PC', 'periodo__referencia'), ('Status da PC', 'status'), ('Descrição do motivo aprovação com ressalvas', 'motivos_aprovacao_ressalva'), @@ -24,6 +29,24 @@ ('Descrição do motivo de reprovação', 'motivos_reprovacao'), ], +def get_informacoes_download(data_inicio, data_final): + """ + Retorna uma string com as informações do download conforme a data de início e final de extração. + """ + + data_inicio = datetime.strptime(data_inicio, "%Y-%m-%d").strftime("%d/%m/%Y") if data_inicio else None + data_final = datetime.strptime(data_final, "%Y-%m-%d").strftime("%d/%m/%Y") if data_final else None + + if data_inicio and data_final: + return f"Filtro aplicado: {data_inicio} a {data_final} (data de criação do registro)" + + if data_inicio and not data_final: + return f"Filtro aplicado: A partir de {data_inicio} (data de criação do registro)" + + if data_final and not data_inicio: + return f"Filtro aplicado: Até {data_final} (data de criação do registro)" + + return "" class ExportacoesStatusPrestacoesContaService: @@ -34,7 +57,10 @@ def __init__(self, **kwargs): self.nome_arquivo = kwargs.get('nome_arquivo', None) self.user = kwargs.get('user', None) self.cabecalho = CABECALHO[0] + self.objeto_arquivo_download = None self.ambiente = self.get_ambiente + self.periodos = None + self.dre_uuid = kwargs.get('dre_uuid', None) @property def get_ambiente(self): @@ -42,11 +68,15 @@ def get_ambiente(self): return ambiente.prefixo if ambiente else "" def exporta_status_prestacoes_conta(self): + self.filtra_range_data('criado_em') + self.cria_registro_central_download() + self.define_periodos_selecionados_no_range_do_filtro_de_data() self.exporta_status_prestacoes_conta_csv() def exporta_status_prestacoes_conta_csv(self): dados = self.monta_dados() + dados_pcs_nao_apresentadas = self.monta_dados_pcs_nao_apresentadas() with NamedTemporaryFile( mode="r+", @@ -61,6 +91,9 @@ def exporta_status_prestacoes_conta_csv(self): for linha in dados: write.writerow(linha) if linha else None + for linha in dados_pcs_nao_apresentadas: + write.writerow(linha) if linha else None + self.cria_rodape(write) self.envia_arquivo_central_download(tmp) @@ -71,6 +104,10 @@ def monta_dados(self): linha_horizontal = [] + if not PrestacaoConta.objects.filter(id=instance.id).exists(): + logger.info(f"Este fechamento não existe mais na base de dados, portanto será pulado") + continue + for _, campo in self.cabecalho: motivos_concatenados = "" @@ -90,7 +127,7 @@ def monta_dados(self): if outros_motivos.strip(): motivos_concatenados += "; " + outros_motivos - linha_horizontal[5] = motivos_concatenados + linha_horizontal[6] = motivos_concatenados if campo == 'motivos_reprovacao' and getattr(instance, 'status') == 'REPROVADA': motivosReprovacao = instance.motivos_reprovacao.values_list('motivo', flat=True) @@ -102,7 +139,7 @@ def monta_dados(self): if outros_motivos.strip(): motivos_concatenados += "; " + outros_motivos - linha_horizontal[7] = motivos_concatenados + linha_horizontal[8] = motivos_concatenados logger.info( f"Escrevendo linha {linha_horizontal} de status de prestação de conta de custeio {instance.id}.") @@ -110,50 +147,124 @@ def monta_dados(self): return linhas_vertical + def monta_dados_pcs_nao_apresentadas(self): + + if self.dre_uuid: + associacoes_com_periodo_inicial = Associacao.objects.filter( + unidade__dre__uuid=self.dre_uuid + ).exclude(periodo_inicial=None) + else: + associacoes_com_periodo_inicial = Associacao.objects.exclude(periodo_inicial=None) + + dados_pcs_nao_apresentadas = [] + + for associacao in associacoes_com_periodo_inicial: + for periodo in self.periodos: + + # Verifica se associação já foi iniciada nesse periodo + if periodo.data_inicio_realizacao_despesas > associacao.periodo_inicial.data_inicio_realizacao_despesas: + + periodo_encerramento = None + if associacao.data_de_encerramento: + periodo_encerramento = Periodo.objects.get( + data_inicio_realizacao_despesas__lte=associacao.data_de_encerramento, + data_fim_realizacao_despesas__gte=associacao.data_de_encerramento + ).proximo_periodo + + if periodo_encerramento and periodo.data_fim_realizacao_despesas > periodo_encerramento.data_inicio_realizacao_despesas: + # associacao encerrada nesse periodo + continue + + + # Verifica se não tem PC nesse periodo, se não existir é uma "PC não entregue" + if not PrestacaoConta.objects.filter(associacao=associacao, periodo=periodo).exists(): + linha_horizontal = [] + + for _, campo in self.cabecalho: + if campo == 'associacao__unidade__codigo_eol': + linha_horizontal.append(associacao.unidade.codigo_eol) + elif campo == 'associacao__unidade__nome': + linha_horizontal.append(associacao.unidade.nome) + elif campo == 'associacao__nome': + linha_horizontal.append(associacao.nome) + elif campo == 'associacao__unidade__dre__nome': + linha_horizontal.append(associacao.unidade.dre.nome if associacao.unidade.dre else '') + elif campo == 'periodo__referencia': + linha_horizontal.append(periodo.referencia) + elif campo == 'status': + linha_horizontal.append('NAO_APRESENTADA') + + logger.info(f"Escrevendo linha {linha_horizontal} de status de prestação de conta não apresentada da associacao {associacao.id} do periodo {periodo}.") + dados_pcs_nao_apresentadas.append(linha_horizontal) + + return dados_pcs_nao_apresentadas + def filtra_range_data(self, field): if self.data_inicio and self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') + data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') + data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') self.queryset = self.queryset.filter( - **{f'{field}__range': [self.data_inicio, self.data_final]} + **{f'{field}__range': [data_inicio, data_final]} ) elif self.data_inicio and not self.data_final: - self.data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') + data_inicio = datetime.strptime(f"{self.data_inicio} 00:00:00", '%Y-%m-%d %H:%M:%S') self.queryset = self.queryset.filter( - **{f'{field}__gt': self.data_inicio} + **{f'{field}__gt': data_inicio} ) elif self.data_final and not self.data_inicio: - self.data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') + data_final = datetime.strptime(f"{self.data_final} 23:59:59", '%Y-%m-%d %H:%M:%S') self.queryset = self.queryset.filter( - **{f'{field}__lt': self.data_final} + **{f'{field}__lt': data_final} ) return self.queryset - def envia_arquivo_central_download(self, tmp): - logger.info("Gerando arquivo download...") - obj_arquivo_download = gerar_arquivo_download( + def define_periodos_selecionados_no_range_do_filtro_de_data(self): + if self.data_inicio and self.data_final: + periodos = Periodo.objects.filter( + Q(data_inicio_realizacao_despesas__lte=self.data_inicio, data_fim_realizacao_despesas__gte=self.data_inicio) | + Q(data_inicio_realizacao_despesas__lte=self.data_final, data_fim_realizacao_despesas__gte=self.data_final) | + Q(data_inicio_realizacao_despesas__gte=self.data_inicio, data_fim_realizacao_despesas__lte=self.data_final) + ).order_by('data_inicio_realizacao_despesas') + elif self.data_inicio and not self.data_final: + periodos = Periodo.objects.filter(data_fim_realizacao_despesas__gte=self.data_inicio).order_by('data_inicio_realizacao_despesas') + elif self.data_final and not self.data_inicio: + periodos = Periodo.objects.filter(data_inicio_realizacao_despesas__lte=self.data_final).order_by('data_inicio_realizacao_despesas') + else: + periodos = Periodo.objects.all().order_by('data_inicio_realizacao_despesas') + + self.periodos = periodos + + return periodos + + def cria_registro_central_download(self): + logger.info(f"Criando registro na central de download") + + obj = gerar_arquivo_download( self.user, - self.nome_arquivo + self.nome_arquivo, + informacoes=get_informacoes_download(self.data_inicio, self.data_final) ) + self.objeto_arquivo_download = obj + def envia_arquivo_central_download(self, tmp): try: logger.info("Salvando arquivo download...") - obj_arquivo_download.arquivo.save( - name=obj_arquivo_download.identificador, + self.objeto_arquivo_download.arquivo.save( + name=self.objeto_arquivo_download.identificador, content=File(tmp) ) - obj_arquivo_download.status = ArquivoDownload.STATUS_CONCLUIDO - obj_arquivo_download.save() + self.objeto_arquivo_download.status = ArquivoDownload.STATUS_CONCLUIDO + self.objeto_arquivo_download.save() logger.info("Arquivo salvo com sucesso...") except Exception as e: - obj_arquivo_download.status = ArquivoDownload.STATUS_ERRO - obj_arquivo_download.msg_erro = str(e) - obj_arquivo_download.save() + self.objeto_arquivo_download.status = ArquivoDownload.STATUS_ERRO + self.objeto_arquivo_download.msg_erro = str(e) + self.objeto_arquivo_download.save() logger.error("Erro arquivo download...") def texto_rodape(self): @@ -162,11 +273,24 @@ def texto_rodape(self): return texto + def texto_info_arquivo_gerado(self): + data_hora_geracao = datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + texto = f"Arquivo gerado via {self.ambiente} pelo usuário {self.user} em {data_hora_geracao}" + + return texto + def cria_rodape(self, write): rodape = [] - texto = self.texto_rodape() + texto_info_arquivo_gerado = self.texto_info_arquivo_gerado() + + rodape.append(" ") + write.writerow(rodape) + rodape.clear() + + rodape.append(texto_info_arquivo_gerado) + write.writerow(rodape) + rodape.clear() - write.writerow([]) - rodape.append(texto) + rodape.append(get_informacoes_download(self.data_inicio, self.data_final)) write.writerow(rodape) rodape.clear() diff --git a/sme_ptrf_apps/sme/tasks/__init__.py b/sme_ptrf_apps/sme/tasks/__init__.py index 0e18f58cf..2d970c183 100644 --- a/sme_ptrf_apps/sme/tasks/__init__.py +++ b/sme_ptrf_apps/sme/tasks/__init__.py @@ -10,3 +10,6 @@ from .exportar_documentos_despesas import exportar_documentos_despesas_async from .gerar_relatorio_devolucao_acertos import gerar_relatorio_devolucao_acertos_async from .exportar_dados_contas import exportar_dados_conta_async +from .exportar_repasses import exportar_repasses_async +from .exportar_membros_apm import exportar_dados_membros_apm_async +from .exportar_processos_sei_regularidade import exportar_processos_sei_regularidade_async diff --git a/sme_ptrf_apps/sme/tasks/exportar_membros_apm.py b/sme_ptrf_apps/sme/tasks/exportar_membros_apm.py new file mode 100644 index 000000000..f504431b3 --- /dev/null +++ b/sme_ptrf_apps/sme/tasks/exportar_membros_apm.py @@ -0,0 +1,36 @@ +import logging + +from celery import shared_task +from sme_ptrf_apps.mandatos.models.cargo_composicao import CargoComposicao +from sme_ptrf_apps.sme.services.exporta_dados_membros_apm_service import ExportacaoDadosMembrosApmService + +logger = logging.getLogger(__name__) + + +@shared_task( + retry_backoff=2, + retry_kwargs={'max_retries': 8}, + time_limet=600, + soft_time_limit=30000 +) +def exportar_dados_membros_apm_async(data_inicio, data_final, username): + logger.info("Exportando csv em processamento...") + queryset = CargoComposicao.objects.all() + try: + logger.info("Criando arquivo %s membros_apm.csv") + params = { + 'queryset': queryset, + 'data_inicio': data_inicio, + 'data_final': data_final, + 'user': username + } + ExportacaoDadosMembrosApmService( + **params, + nome_arquivo='membros_apm.csv' + ).exporta_membros_apm() + + except Exception as e: + logger.error(f"Erro ao exportar csv: {e}") + raise e + + logger.info("Exportação csv finalizada com sucesso.") \ No newline at end of file diff --git a/sme_ptrf_apps/sme/tasks/exportar_processos_sei_regularidade.py b/sme_ptrf_apps/sme/tasks/exportar_processos_sei_regularidade.py new file mode 100644 index 000000000..83bf4ed12 --- /dev/null +++ b/sme_ptrf_apps/sme/tasks/exportar_processos_sei_regularidade.py @@ -0,0 +1,34 @@ +import logging + +from celery import shared_task +from sme_ptrf_apps.core.models.associacao import Associacao +from sme_ptrf_apps.sme.services.exporta_dados_processos_sei_regularidade_service import ExportaDadosProcessosSeiRegularidadeService + +logger = logging.getLogger(__name__) + + +@shared_task( + retry_backoff=2, + retry_kwargs={'max_retries': 8}, + time_limet=600, + soft_time_limit=30000 +) +def exportar_processos_sei_regularidade_async(username): + logger.info("Exportando csv em processamento...") + queryset = Associacao.objects.all() + try: + logger.info("Criando arquivo %s processo_sei_regularidade.csv") + params = { + 'queryset': queryset, + 'user': username + } + ExportaDadosProcessosSeiRegularidadeService( + **params, + nome_arquivo='processo_sei_regularidade.csv' + ).exportar() + + except Exception as e: + logger.error(f"Erro ao exportar csv: {e}") + raise e + + logger.info("Exportação csv finalizada com sucesso.") diff --git a/sme_ptrf_apps/sme/tasks/exportar_repasses.py b/sme_ptrf_apps/sme/tasks/exportar_repasses.py new file mode 100644 index 000000000..890c7c4d6 --- /dev/null +++ b/sme_ptrf_apps/sme/tasks/exportar_repasses.py @@ -0,0 +1,36 @@ +import logging + +from celery import shared_task +from sme_ptrf_apps.receitas.models import Repasse +from sme_ptrf_apps.sme.services.exporta_repasses_service import ExportacaoDadosRepassesService + +logger = logging.getLogger(__name__) + + +@shared_task( + retry_backoff=2, + retry_kwargs={'max_retries': 8}, + time_limet=600, + soft_time_limit=30000 +) +def exportar_repasses_async(data_inicio, data_final, username): + logger.info("Exportando csv em processamento...") + queryset = Repasse.objects.all() + try: + logger.info("Criando arquivo %s repasses.csv") + params = { + 'queryset': queryset, + 'data_inicio': data_inicio, + 'data_final': data_final, + 'user': username + } + ExportacaoDadosRepassesService( + **params, + nome_arquivo='repasses.csv' + ).exporta_repasses() + + except Exception as e: + logger.error(f"Erro ao exportar csv: {e}") + raise e + + logger.info("Exportação csv finalizada com sucesso.") diff --git a/sme_ptrf_apps/sme/tasks/exportar_status_prestacoes_contas.py b/sme_ptrf_apps/sme/tasks/exportar_status_prestacoes_contas.py index 7b8f01888..6c4c3bea6 100644 --- a/sme_ptrf_apps/sme/tasks/exportar_status_prestacoes_contas.py +++ b/sme_ptrf_apps/sme/tasks/exportar_status_prestacoes_contas.py @@ -13,10 +13,15 @@ time_limet=600, soft_time_limit=30000 ) -def exportar_status_prestacoes_contas_async(data_inicio, data_final, username): +def exportar_status_prestacoes_contas_async(data_inicio, data_final, username, dre_uuid): logger.info("Exportando csv em processamento...") - queryset = PrestacaoConta.objects.all().order_by('criado_em') + if dre_uuid: + queryset = PrestacaoConta.objects.filter( + associacao__unidade__dre__uuid=dre_uuid, + ).order_by('criado_em') + else: + queryset = PrestacaoConta.objects.all().order_by('criado_em') try: logger.info("Criando arquivo %s status_prestacoes_de_contas.csv") @@ -25,6 +30,7 @@ def exportar_status_prestacoes_contas_async(data_inicio, data_final, username): 'data_inicio': data_inicio, 'data_final': data_final, 'user': username, + 'dre_uuid': dre_uuid, } ExportacoesStatusPrestacoesContaService( @@ -36,4 +42,4 @@ def exportar_status_prestacoes_contas_async(data_inicio, data_final, username): logger.error(f"Erro ao exportar csv: {e}") raise e - logger.info("Exportação csv finalizada com sucesso.") \ No newline at end of file + logger.info("Exportação csv finalizada com sucesso.") diff --git a/sme_ptrf_apps/sme/tests/test_api_exportacoes_dados_membros_apm/__init__.py b/sme_ptrf_apps/sme/tests/test_api_exportacoes_dados_membros_apm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/sme/tests/test_api_exportacoes_dados_membros_apm/test_exportacoes_dados_membros_apm.py b/sme_ptrf_apps/sme/tests/test_api_exportacoes_dados_membros_apm/test_exportacoes_dados_membros_apm.py new file mode 100644 index 000000000..201e45e06 --- /dev/null +++ b/sme_ptrf_apps/sme/tests/test_api_exportacoes_dados_membros_apm/test_exportacoes_dados_membros_apm.py @@ -0,0 +1,23 @@ +import json +import pytest + +from rest_framework.status import HTTP_201_CREATED + + +pytestmark = pytest.mark.django_db + + +def test_exportacoes_dados_membros_apm(jwt_authenticated_client_sme): + url = f'/api/exportacoes-dados/dados_membros_apm/' + resultado_esperado = { + 'response': 'O arquivo está sendo gerado e será enviado para a central de download após conclusão.' + } + + response = jwt_authenticated_client_sme.get( + url, + content_type='multipart/form-data') + + result = json.loads(response.content) + + assert response.status_code == HTTP_201_CREATED + assert result == resultado_esperado \ No newline at end of file diff --git a/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_processos_sei_regularidade/__init__.py b/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_processos_sei_regularidade/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_processos_sei_regularidade/test_exportacoes_dados_processos_sei_regularidade.py b/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_processos_sei_regularidade/test_exportacoes_dados_processos_sei_regularidade.py new file mode 100644 index 000000000..6db4de711 --- /dev/null +++ b/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_processos_sei_regularidade/test_exportacoes_dados_processos_sei_regularidade.py @@ -0,0 +1,36 @@ +import json +import pytest + +from unittest.mock import Mock + +from rest_framework.status import HTTP_201_CREATED + +from sme_ptrf_apps.sme.tasks import exportar_processos_sei_regularidade_async + +pytestmark = pytest.mark.django_db + + +def test_exportacoes_dados_processos_sei_regularidade(jwt_authenticated_client_sme, usuario_permissao_sme, monkeypatch): + url = f'/api/exportacoes-dados/processos-sei-regularidade/' + resultado_esperado = { + 'response': 'O arquivo está sendo gerado e será enviado para a central de download após conclusão.' + } + + mock_exportar_processos_sei_regularidade_async = Mock() + monkeypatch.setattr(exportar_processos_sei_regularidade_async, 'delay', + mock_exportar_processos_sei_regularidade_async) + + response = jwt_authenticated_client_sme.get( + url, + content_type='multipart/form-data') + + result = json.loads(response.content) + + # Testa o resultado da requisição + assert response.status_code == HTTP_201_CREATED + assert result == resultado_esperado + + # Testa se a função mockada foi chamada com os parâmetros corretos + mock_exportar_processos_sei_regularidade_async.assert_called_once_with( + username=usuario_permissao_sme.username + ) diff --git a/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_repasses/__init__.py b/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_repasses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_repasses/test_exportacoes_dados_repasses.py b/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_repasses/test_exportacoes_dados_repasses.py new file mode 100644 index 000000000..f6391f27a --- /dev/null +++ b/sme_ptrf_apps/sme/tests/tests_api_exportacoes_dados_repasses/test_exportacoes_dados_repasses.py @@ -0,0 +1,40 @@ +import datetime +import json +import pytest + +from unittest.mock import Mock + +from rest_framework.status import HTTP_201_CREATED + +from sme_ptrf_apps.sme.tasks import exportar_repasses_async + +pytestmark = pytest.mark.django_db + +DATAS = (datetime.date(2020, 3, 26), datetime.date(2020, 4, 26)) + + +def test_exportacoes_dados_repasses(jwt_authenticated_client_sme, usuario_permissao_sme, monkeypatch): + url = f'/api/exportacoes-dados/repasses/?data_inicio={DATAS[0]}&data_final={DATAS[1]}' + resultado_esperado = { + 'response': 'O arquivo está sendo gerado e será enviado para a central de download após conclusão.' + } + + mock_exportar_repasse_async = Mock() + monkeypatch.setattr(exportar_repasses_async, 'delay', mock_exportar_repasse_async) + + response = jwt_authenticated_client_sme.get( + url, + content_type='multipart/form-data') + + result = json.loads(response.content) + + # Testa o resultado da requisição + assert response.status_code == HTTP_201_CREATED + assert result == resultado_esperado + + # Testa se a função mockada foi chamada com os parâmetros corretos + mock_exportar_repasse_async.assert_called_once_with( + data_inicio='2020-03-26', + data_final='2020-04-26', + username=usuario_permissao_sme.username + ) diff --git a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_creditos.py b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_creditos.py index 19170a564..1fad2cee1 100644 --- a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_creditos.py +++ b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_creditos.py @@ -7,9 +7,9 @@ DATA_FILTRADAS = [ - (datetime.date(2020, 2, 25), datetime.date(2020, 4, 26), 2), - (None, datetime.date(2020, 4, 26), 2), - (datetime.date(2050, 10, 28), None, 0) + ('2020-02-25', '2020-04-26', 2), + (None, '2020-04-26', 2), + ('2050-10-28', None, 0) ] diff --git a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_membros_apm/__init__.py b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_membros_apm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_membros_apm/conftest.py b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_membros_apm/conftest.py new file mode 100644 index 000000000..5875c7111 --- /dev/null +++ b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_membros_apm/conftest.py @@ -0,0 +1,81 @@ +from datetime import date +import pytest +from model_bakery import baker + + +@pytest.fixture +def ambiente(): + return baker.make( + 'Ambiente', + prefixo='dev-sig-escola', + nome='Ambiente de desenvolvimento', + ) + + +@pytest.fixture +def membros_apm_fixture_mock(cargo_composicao_factory, ocupante_cargo_factory, periodo_factory, associacao_factory, mandato_factory, composicao_factory): + + periodo_inicial = periodo_factory.create(referencia="2022.1", data_inicio_realizacao_despesas=date(2022,1,1), data_fim_realizacao_despesas=date(2022,4,20)) + + associacao = associacao_factory.create(periodo_inicial=periodo_inicial) + + mandato_2023_a_2025 = mandato_factory.create( + referencia_mandato='2023 a 2025', + data_inicial=date(2023, 7, 19), + data_final=date(2023, 7, 20) + ) + + composicao_2023_a_2025 = composicao_factory.create( + associacao=associacao, + mandato=mandato_2023_a_2025, + data_inicial=date(2023, 1, 1), + data_final=date(2025, 12, 31), + ) + + ocupante_1 = ocupante_cargo_factory.create( + nome="Matheus Diori", + codigo_identificacao='1234567', + cargo_educacao="Diretor de Escola", + representacao='Servidor', + email='mdiori@hotmail.com', + cpf_responsavel='907.536.560-86', + telefone='11976643126', + cep='02847070', + bairro='Jd. Shangrila', + endereco='Rua longa, 222', + ) + + ocupante_2 = ocupante_cargo_factory.create( + nome="Bert Macklin FBI", + codigo_identificacao='1234567', + cargo_educacao="Vogal 1", + representacao='Servidor', + email='mdiori@hotmail.com', + cpf_responsavel='907.536.560-86', + telefone='11976643126', + cep='02847070', + bairro='Jd. Space', + endereco='Rua curta, 222', + ) + + cargo_composicao_1 = cargo_composicao_factory.create( + composicao=composicao_2023_a_2025, + ocupante_do_cargo=ocupante_1, + cargo_associacao='Presidente da diretoria executiva', + substituto=False, + substituido=False, + data_inicio_no_cargo=date(2023, 1, 10), + data_fim_no_cargo=date(2023, 4, 10), + ) + + cargo_composicao_2 = cargo_composicao_factory.create( + composicao=composicao_2023_a_2025, + ocupante_do_cargo=ocupante_2, + cargo_associacao='Vogal 1', + substituto=False, + substituido=False, + data_inicio_no_cargo=date(2023, 3, 10), + data_fim_no_cargo=date(2023, 5, 10), + ) + + return diff --git a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_membros_apm/test_exportacoes_membros_apm.py b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_membros_apm/test_exportacoes_membros_apm.py new file mode 100644 index 000000000..710218535 --- /dev/null +++ b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_membros_apm/test_exportacoes_membros_apm.py @@ -0,0 +1,244 @@ +import datetime +from tempfile import NamedTemporaryFile +import pytest +from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload + +from sme_ptrf_apps.mandatos.models.cargo_composicao import CargoComposicao +from sme_ptrf_apps.mandatos.models.ocupante_cargo import OcupanteCargo +from sme_ptrf_apps.sme.services.exporta_dados_membros_apm_service import ExportacaoDadosMembrosApmService +from sme_ptrf_apps.receitas.models.repasse import Repasse + +pytestmark = pytest.mark.django_db + + +def test_cabecalho(): + dados = ExportacaoDadosMembrosApmService() + + cabecalho = [cabecalho[0] for cabecalho in dados.cabecalho] + + resultado_esperado = [ + "Código EOL", + "Tipo da unidade", + "Nome da Unidade", + "Nome da Associação", + "CNPJ", + "DRE", + "Mandato", + "Data inicial do mandato", + "Data final do mandato", + "Composição data inicial", + "Composição data final", + "Cargo", + "Nome", + "Número de identificação", + "Representação", + "Período inicial de ocupação", + "Período final de ocupação", + ] + + assert cabecalho == resultado_esperado + + +def test_dados_repasses_esperados_csv(membros_apm_fixture_mock, ambiente): + queryset = CargoComposicao.objects.all() + + dados = ExportacaoDadosMembrosApmService(queryset=queryset).monta_dados() + linha_individual = dados[0] + + resultado_esperado = [ + queryset[0].composicao.associacao.unidade.codigo_eol, + queryset[0].composicao.associacao.unidade.tipo_unidade, + queryset[0].composicao.associacao.unidade.nome, + queryset[0].composicao.associacao.nome, + queryset[0].composicao.associacao.cnpj, + queryset[0].composicao.associacao.unidade.dre.nome, + queryset[0].composicao.mandato.referencia_mandato, + '19/07/2023', + '20/07/2023', + '01/01/2023', + '31/12/2025', + queryset[0].cargo_associacao, + queryset[0].ocupante_do_cargo.nome, + '907XXXXXX86', + queryset[0].ocupante_do_cargo.representacao, + '10/01/2023', + '10/04/2023', + ] + + assert resultado_esperado == linha_individual + + +def test_rodape(membros_apm_fixture_mock, ambiente): + queryset = Repasse.objects.all() + + dados = ExportacaoDadosMembrosApmService( + queryset=queryset, + user="12345" + ).texto_info_arquivo_gerado() + + data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário 12345 em {data_atual}" + + assert dados == resultado_esperado + + +def test_filtra_range_data_fora_do_range(membros_apm_fixture_mock): + queryset = Repasse.objects.all() + + data_inicio = datetime.date(2020, 2, 10) + data_final = datetime.date(2020, 5, 10) + + queryset_filtrado = ExportacaoDadosMembrosApmService( + queryset=queryset, + data_inicio=data_inicio, + data_final=data_final + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == 0 + + +def test_filtra_range_data_dentro_do_range(membros_apm_fixture_mock): + queryset = Repasse.objects.all() + + data_inicio = datetime.date.today() + data_final = datetime.date.today() + + queryset_filtrado = ExportacaoDadosMembrosApmService( + queryset=queryset, + data_inicio=data_inicio, + data_final=data_final + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtra_range_data_com_data_inicio_e_sem_data_final(membros_apm_fixture_mock): + queryset = Repasse.objects.all() + + data_inicio = datetime.date.today() + + queryset_filtrado = ExportacaoDadosMembrosApmService( + queryset=queryset, + data_inicio=data_inicio + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtra_range_data_sem_data_inicio_e_com_data_final(membros_apm_fixture_mock): + queryset = Repasse.objects.all() + + data_final = datetime.date.today() + + queryset_filtrado = ExportacaoDadosMembrosApmService( + queryset=queryset, + data_final=data_final + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtra_range_data_sem_data_inicio_e_sem_data_final(membros_apm_fixture_mock): + queryset = Repasse.objects.all() + + queryset_filtrado = ExportacaoDadosMembrosApmService( + queryset=queryset + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtros_aplicados_sem_data_inicio_e_sem_data_final(membros_apm_fixture_mock): + queryset = Repasse.objects.all() + + dados = ExportacaoDadosMembrosApmService( + queryset=queryset + ).get_texto_filtro_aplicado() + + resultado_esperado = "" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_com_data_final(membros_apm_fixture_mock): + queryset = Repasse.objects.all() + + data_inicio = datetime.date.today() + data_final = datetime.date.today() + + dados = ExportacaoDadosMembrosApmService( + queryset=queryset, + data_inicio=data_inicio, + data_final=data_final + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: {data_inicio.strftime('%d/%m/%Y')} a {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_sem_data_final(membros_apm_fixture_mock): + queryset = Repasse.objects.all() + + data_inicio = datetime.date.today() + + dados = ExportacaoDadosMembrosApmService( + queryset=queryset, + data_inicio=data_inicio, + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: A partir de {data_inicio.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_com_data_final(membros_apm_fixture_mock): + queryset = Repasse.objects.all() + + data_final = datetime.date.today() + + dados = ExportacaoDadosMembrosApmService( + queryset=queryset, + data_final=data_final, + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: Até {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_cria_registro_central_download(usuario_para_teste): + exportacao_repasses = ExportacaoDadosMembrosApmService( + nome_arquivo='repasses.csv', + user=usuario_para_teste.username + ) + + exportacao_repasses.cria_registro_central_download() + objeto_arquivo_download = exportacao_repasses.objeto_arquivo_download + + assert objeto_arquivo_download.status == ArquivoDownload.STATUS_EM_PROCESSAMENTO + assert objeto_arquivo_download.identificador == 'repasses.csv' + assert ArquivoDownload.objects.count() == 1 + + +def test_envia_arquivo_central_download(usuario_para_teste): + with NamedTemporaryFile( + mode="r+", + newline='', + encoding='utf-8', + prefix='repasses', + suffix='.csv' + ) as file: + file.write("testando central de download") + + exportacao_repasses = ExportacaoDadosMembrosApmService( + nome_arquivo='repasses.csv', + user=usuario_para_teste.username + ) + exportacao_repasses.cria_registro_central_download() + exportacao_repasses.envia_arquivo_central_download(file) + objeto_arquivo_download = exportacao_repasses.objeto_arquivo_download + + assert objeto_arquivo_download.status == ArquivoDownload.STATUS_CONCLUIDO + assert objeto_arquivo_download.identificador == 'repasses.csv' + assert ArquivoDownload.objects.count() == 1 diff --git a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_processos_sei_regularidade/__init__.py b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_processos_sei_regularidade/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_processos_sei_regularidade/test_exportacoes_dados_processos_sei_regularidade.py b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_processos_sei_regularidade/test_exportacoes_dados_processos_sei_regularidade.py new file mode 100644 index 000000000..ce577ac3c --- /dev/null +++ b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_processos_sei_regularidade/test_exportacoes_dados_processos_sei_regularidade.py @@ -0,0 +1,107 @@ +import datetime +from tempfile import NamedTemporaryFile +import pytest +from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload + +from sme_ptrf_apps.sme.services.exporta_dados_processos_sei_regularidade_service import ExportaDadosProcessosSeiRegularidadeService +from sme_ptrf_apps.core.models.associacao import Associacao + +pytestmark = pytest.mark.django_db + + +def test_cabecalho(): + dados = ExportaDadosProcessosSeiRegularidadeService() + + cabecalho = [cabecalho[0] for cabecalho in dados.cabecalho] + + resultado_esperado = [ + "Código EOL", + "Nome Unidade", + "Nome Associação", + "CNPj da Associação", + "DRE", + "Número do processo SEI de regularidade", + ] + + assert cabecalho == resultado_esperado + + +def test_dados_processos_sei_regularidade_esperados_csv(associacao_factory): + associacao_factory.create() + associacao_factory.create() + + queryset = Associacao.objects.all() + + dados = ExportaDadosProcessosSeiRegularidadeService(queryset=queryset).monta_dados() + + resultado_esperado = [ + [ + queryset[0].unidade.codigo_eol, + queryset[0].unidade.nome, + queryset[0].nome, + queryset[0].cnpj, + queryset[0].unidade.dre.nome, + queryset[0].processo_regularidade, + ], + [ + queryset[1].unidade.codigo_eol, + queryset[1].unidade.nome, + queryset[1].nome, + queryset[1].cnpj, + queryset[1].unidade.dre.nome, + queryset[1].processo_regularidade, + ] + ] + + assert resultado_esperado == dados + + +def test_rodape(ambiente, usuario_para_teste): + queryset = Associacao.objects.all() + + dados = ExportaDadosProcessosSeiRegularidadeService( + queryset=queryset, + user=usuario_para_teste + ).texto_info_arquivo_gerado() + + data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário {usuario_para_teste} em {data_atual}" + + assert dados == resultado_esperado + + +def test_cria_registro_central_download(usuario_para_teste): + exportacao = ExportaDadosProcessosSeiRegularidadeService( + nome_arquivo='processo_sei_regularidade.csv', + user=usuario_para_teste.username + ) + + exportacao.cria_registro_central_download() + objeto_arquivo_download = exportacao.objeto_arquivo_download + + assert objeto_arquivo_download.status == ArquivoDownload.STATUS_EM_PROCESSAMENTO + assert objeto_arquivo_download.identificador == 'processo_sei_regularidade.csv' + assert ArquivoDownload.objects.count() == 1 + + +def test_envia_arquivo_central_download(usuario_para_teste): + with NamedTemporaryFile( + mode="r+", + newline='', + encoding='utf-8', + prefix='processo_sei_regularidade', + suffix='.csv' + ) as file: + file.write("testando central de download") + + exportacao = ExportaDadosProcessosSeiRegularidadeService( + nome_arquivo='processo_sei_regularidade.csv', + user=usuario_para_teste.username + ) + exportacao.cria_registro_central_download() + exportacao.envia_arquivo_central_download(file) + objeto_arquivo_download = exportacao.objeto_arquivo_download + + assert objeto_arquivo_download.status == ArquivoDownload.STATUS_CONCLUIDO + assert objeto_arquivo_download.identificador == 'processo_sei_regularidade.csv' + assert ArquivoDownload.objects.count() == 1 diff --git a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_saldos_finais_periodo.py b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_saldos_finais_periodo.py index d6003676d..ea11e209c 100644 --- a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_saldos_finais_periodo.py +++ b/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_saldos_finais_periodo.py @@ -7,6 +7,7 @@ pytestmark = pytest.mark.django_db + def test_cria_registro_central_download(usuario_para_teste): exportacao_saldo_final = ExportacoesDadosSaldosFinaisPeriodoService( nome_arquivo='pcs_saldo_final_periodo.csv', @@ -20,6 +21,7 @@ def test_cria_registro_central_download(usuario_para_teste): assert objeto_arquivo_download.identificador == 'pcs_saldo_final_periodo.csv' assert ArquivoDownload.objects.count() == 1 + def test_envia_arquivo_central_download(usuario_para_teste): with NamedTemporaryFile( mode="r+", @@ -42,9 +44,12 @@ def test_envia_arquivo_central_download(usuario_para_teste): assert objeto_arquivo_download.identificador == 'pcs_saldo_final_periodo.csv' assert ArquivoDownload.objects.count() == 1 + def test_filtra_range_data_fora_do_range(fechamento_periodo_queryset): data_inicio = datetime.date(2020, 2, 25) + data_inicio = data_inicio.strftime('%Y-%m-%d') data_final = datetime.date(2020, 4, 26) + data_final = data_final.strftime('%Y-%m-%d') queryset_filtrado = ExportacoesDadosSaldosFinaisPeriodoService( queryset=fechamento_periodo_queryset, @@ -54,9 +59,12 @@ def test_filtra_range_data_fora_do_range(fechamento_periodo_queryset): assert queryset_filtrado.count() == 0 + def test_filtra_range_data_dentro_do_range(fechamento_periodo_queryset): data_inicio = datetime.date.today() + data_inicio = data_inicio.strftime('%Y-%m-%d') data_final = datetime.date.today() + data_final = data_final.strftime('%Y-%m-%d') queryset_filtrado = ExportacoesDadosSaldosFinaisPeriodoService( queryset=fechamento_periodo_queryset, @@ -66,8 +74,10 @@ def test_filtra_range_data_dentro_do_range(fechamento_periodo_queryset): assert queryset_filtrado.count() == len(fechamento_periodo_queryset) + def test_filtra_range_data_com_data_inicio_e_sem_data_final(fechamento_periodo_queryset): data_inicio = datetime.date.today() + data_inicio = data_inicio.strftime('%Y-%m-%d') queryset_filtrado = ExportacoesDadosSaldosFinaisPeriodoService( queryset=fechamento_periodo_queryset, @@ -76,8 +86,10 @@ def test_filtra_range_data_com_data_inicio_e_sem_data_final(fechamento_periodo_q assert queryset_filtrado.count() == len(fechamento_periodo_queryset) + def test_filtra_range_data_sem_data_inicio_e_com_data_final(fechamento_periodo_queryset): data_final = datetime.date.today() + data_final = data_final.strftime('%Y-%m-%d') queryset_filtrado = ExportacoesDadosSaldosFinaisPeriodoService( queryset=fechamento_periodo_queryset, @@ -86,6 +98,7 @@ def test_filtra_range_data_sem_data_inicio_e_com_data_final(fechamento_periodo_q assert queryset_filtrado.count() == len(fechamento_periodo_queryset) + def test_filtra_range_data_sem_data_inicio_e_sem_data_final(fechamento_periodo_queryset): queryset_filtrado = ExportacoesDadosSaldosFinaisPeriodoService( queryset=fechamento_periodo_queryset @@ -104,6 +117,7 @@ def test_quantidade_dados_extracao(fechamento_periodo_queryset): assert len(dados) == 6 + def test_quantidade_linha_individual_dados_extracao(fechamento_periodo_queryset): dados = ExportacoesDadosSaldosFinaisPeriodoService( queryset=fechamento_periodo_queryset, @@ -125,6 +139,7 @@ def test_quantidade_linha_individual_dados_extracao(fechamento_periodo_queryset) assert len(linha_individual) == 10 + def test_resultado_esperado_dados_extracao(fechamento_periodo_queryset): dados = ExportacoesDadosSaldosFinaisPeriodoService( queryset=fechamento_periodo_queryset, @@ -148,6 +163,7 @@ def test_resultado_esperado_dados_extracao(fechamento_periodo_queryset): assert linha_individual == resultado_esperado + def test_cabecalho(fechamento_periodo_queryset): dados = ExportacoesDadosSaldosFinaisPeriodoService( queryset=fechamento_periodo_queryset, @@ -170,13 +186,68 @@ def test_cabecalho(fechamento_periodo_queryset): assert cabecalho == resultado_esperado + def test_rodape(fechamento_periodo_queryset, ambiente): dados = ExportacoesDadosSaldosFinaisPeriodoService( queryset=fechamento_periodo_queryset, + user="12345" ).texto_rodape() data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - resultado_esperado = f"Arquivo gerado pelo {ambiente.prefixo} em {data_atual}" + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário 12345 em {data_atual}" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_sem_data_final(fechamento_periodo_queryset, ambiente): + dados = ExportacoesDadosSaldosFinaisPeriodoService( + queryset=fechamento_periodo_queryset, + ).get_informacoes_download() + + resultado_esperado = "" assert dados == resultado_esperado + +def test_filtros_aplicados_com_data_inicio_e_com_data_final(fechamento_periodo_queryset, ambiente): + + data_inicio = '2024-03-01' + data_final = '2024-03-26' + + dados = ExportacoesDadosSaldosFinaisPeriodoService( + queryset=fechamento_periodo_queryset, + data_inicio=data_inicio, + data_final=data_final + ).get_informacoes_download() + + resultado_esperado = f"Filtro aplicado: 01/03/2024 a 26/03/2024 (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_sem_data_final(fechamento_periodo_queryset, ambiente): + + data_inicio = '2024-03-01' + + dados = ExportacoesDadosSaldosFinaisPeriodoService( + queryset=fechamento_periodo_queryset, + data_inicio=data_inicio, + ).get_informacoes_download() + + resultado_esperado = f"Filtro aplicado: A partir de 01/03/2024 (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_com_data_final(fechamento_periodo_queryset, ambiente): + + data_final = '2024-03-26' + + dados = ExportacoesDadosSaldosFinaisPeriodoService( + queryset=fechamento_periodo_queryset, + data_final=data_final, + ).get_informacoes_download() + + resultado_esperado = f"Filtro aplicado: Até 26/03/2024 (data de criação do registro)" + + assert dados == resultado_esperado diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_atas/test_exportacoes_atas.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_atas/test_exportacoes_atas.py index 007c24db8..44361cbce 100644 --- a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_atas/test_exportacoes_atas.py +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_atas/test_exportacoes_atas.py @@ -8,6 +8,7 @@ pytestmark = pytest.mark.django_db + def test_cabecalho(): dados = ExportacoesAtasService() @@ -17,6 +18,7 @@ def test_cabecalho(): 'Código EOL', 'Nome unidade', 'Nome associação', + 'DRE', 'Referência do período da PC', 'Tipo de ata', 'Tipo de reunião', @@ -41,16 +43,18 @@ def test_cabecalho(): assert cabecalho == resultado_esperado + def test_dados_esperados_csv(queryset_ordered, ambiente): dados = ExportacoesAtasService(queryset=queryset_ordered).monta_dados() linha_individual = dados[0] ata = queryset_ordered.first() - + resultado_esperado = [ ata.associacao.unidade.codigo_eol, ata.associacao.unidade.nome, ata.associacao.nome, + ata.associacao.unidade.dre.nome, ata.periodo.referencia, ata.tipo_ata, ata.tipo_reuniao, @@ -75,19 +79,21 @@ def test_dados_esperados_csv(queryset_ordered, ambiente): assert linha_individual == resultado_esperado + def test_rodape(queryset_ordered, ambiente): dados = ExportacoesAtasService( queryset=queryset_ordered, - ).texto_rodape() + ).texto_info_arquivo_gerado() data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - resultado_esperado = f"Arquivo gerado pelo {ambiente.prefixo} em {data_atual}" + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário None em {data_atual}" assert dados == resultado_esperado + def test_filtra_range_data_fora_do_range(queryset_ordered): - data_inicio = datetime.date(2020, 2, 10) - data_final = datetime.date(2020, 5, 10) + data_inicio = "2020-02-10" + data_final = "2020-05-10" queryset_filtrado = ExportacoesAtasService( queryset=queryset_ordered, @@ -97,9 +103,10 @@ def test_filtra_range_data_fora_do_range(queryset_ordered): assert queryset_filtrado.count() == 0 + def test_filtra_range_data_dentro_do_range(queryset_ordered): - data_inicio = datetime.date.today() - data_final = datetime.date.today() + data_inicio = datetime.date.today().strftime("%Y-%m-%d") + data_final = datetime.date.today().strftime("%Y-%m-%d") queryset_filtrado = ExportacoesAtasService( queryset=queryset_ordered, @@ -109,8 +116,9 @@ def test_filtra_range_data_dentro_do_range(queryset_ordered): assert queryset_filtrado.count() == len(queryset_ordered) + def test_filtra_range_data_com_data_inicio_e_sem_data_final(queryset_ordered): - data_inicio = datetime.date.today() + data_inicio = datetime.date.today().strftime("%Y-%m-%d") queryset_filtrado = ExportacoesAtasService( queryset=queryset_ordered, @@ -119,8 +127,9 @@ def test_filtra_range_data_com_data_inicio_e_sem_data_final(queryset_ordered): assert queryset_filtrado.count() == len(queryset_ordered) + def test_filtra_range_data_sem_data_inicio_e_com_data_final(queryset_ordered): - data_final = datetime.date.today() + data_final = datetime.date.today().strftime("%Y-%m-%d") queryset_filtrado = ExportacoesAtasService( queryset=queryset_ordered, @@ -129,13 +138,15 @@ def test_filtra_range_data_sem_data_inicio_e_com_data_final(queryset_ordered): assert queryset_filtrado.count() == len(queryset_ordered) + def test_filtra_range_data_sem_data_inicio_e_sem_data_final(queryset_ordered): queryset_filtrado = ExportacoesAtasService( queryset=queryset_ordered ).filtra_range_data('criado_em') - + assert queryset_filtrado.count() == len(queryset_ordered) + def test_cria_registro_central_download(usuario_para_teste): exportacao_saldo_final = ExportacoesAtasService( nome_arquivo='pcs_atas.csv', @@ -149,6 +160,7 @@ def test_cria_registro_central_download(usuario_para_teste): assert objeto_arquivo_download.identificador == 'pcs_atas.csv' assert ArquivoDownload.objects.count() == 1 + def test_envia_arquivo_central_download(usuario_para_teste): with NamedTemporaryFile( mode="r+", @@ -169,4 +181,4 @@ def test_envia_arquivo_central_download(usuario_para_teste): assert objeto_arquivo_download.status == ArquivoDownload.STATUS_CONCLUIDO assert objeto_arquivo_download.identificador == 'pcs_atas.csv' - assert ArquivoDownload.objects.count() == 1 \ No newline at end of file + assert ArquivoDownload.objects.count() == 1 diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_contas/__init__.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_contas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_contas/conftest.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_contas/conftest.py new file mode 100644 index 000000000..a6c8f515a --- /dev/null +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_contas/conftest.py @@ -0,0 +1,26 @@ +import pytest +from model_bakery import baker +from datetime import date + + +@pytest.fixture +def ambiente(): + return baker.make( + 'Ambiente', + prefixo='dev-sig-escola', + nome='Ambiente de desenvolvimento', + ) + + +@pytest.fixture +def conta_associacao_exportacao_csv(associacao, tipo_conta): + return baker.make( + 'ContaAssociacao', + associacao=associacao, + tipo_conta=tipo_conta, + banco_nome='Banco do Brasil', + agencia='12345', + numero_conta='123456-x', + numero_cartao='534653264523', + data_inicio=date(2019, 1, 1) + ) diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_contas/test_exportacoes_dados_contas.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_contas/test_exportacoes_dados_contas.py new file mode 100644 index 000000000..85b3ae25c --- /dev/null +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_contas/test_exportacoes_dados_contas.py @@ -0,0 +1,209 @@ +import datetime +from tempfile import NamedTemporaryFile +import pytest +from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload +from sme_ptrf_apps.sme.services.exporta_dados_contas_service import ExportacaoDadosContasService +from sme_ptrf_apps.core.models.conta_associacao import ContaAssociacao + +pytestmark = pytest.mark.django_db + + +def test_cabecalho(): + dados = ExportacaoDadosContasService() + cabecalho = [cabecalho[0] for cabecalho in dados.cabecalho] + + resultado_esperado = [ + 'Código EOL', + 'Nome Unidade', + 'Nome Associação', + 'DRE', + 'Nome do tipo de conta', + 'Data de criação da conta', + 'Data de início da conta', + 'Banco', + 'Agência', + 'Nº da conta com o dígito', + 'Saldo_atual', + 'Status', + 'Data do encerramento', + 'Status do encerramento', + 'Motivo de rejeição do encerramento', + ] + + assert cabecalho == resultado_esperado + + +def test_rodape(conta_associacao_exportacao_csv, ambiente): + queryset = ContaAssociacao.objects.all() + + dados = ExportacaoDadosContasService( + queryset=queryset, + user="12345" + ).texto_rodape() + + data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário 12345 em {data_atual}" + + assert dados == resultado_esperado + + +def test_filtra_range_data_fora_do_range(conta_associacao_exportacao_csv): + queryset = ContaAssociacao.objects.all() + + data_inicio = str(datetime.date(2020, 2, 10)) + data_final = str(datetime.date(2020, 5, 10)) + + queryset_filtrado = ExportacaoDadosContasService( + queryset=queryset, + data_inicio=data_inicio, + data_final=data_final + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == 0 + + +def test_filtra_range_data_dentro_do_range(conta_associacao_exportacao_csv): + queryset = ContaAssociacao.objects.all() + + data_inicio = str(datetime.date.today()) + data_final = str(datetime.date.today()) + + queryset_filtrado = ExportacaoDadosContasService( + queryset=queryset, + data_inicio=data_inicio, + data_final=data_final + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtra_range_data_com_data_inicio_e_sem_data_final(conta_associacao_exportacao_csv): + queryset = ContaAssociacao.objects.all() + + data_inicio = str(datetime.date.today()) + + queryset_filtrado = ExportacaoDadosContasService( + queryset=queryset, + data_inicio=data_inicio + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtra_range_data_sem_data_inicio_e_com_data_final(conta_associacao_exportacao_csv): + queryset = ContaAssociacao.objects.all() + + data_final = str(datetime.date.today()) + + queryset_filtrado = ExportacaoDadosContasService( + queryset=queryset, + data_final=data_final + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtra_range_data_sem_data_inicio_e_sem_data_final(conta_associacao_exportacao_csv): + queryset = ContaAssociacao.objects.all() + + queryset_filtrado = ExportacaoDadosContasService( + queryset=queryset + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtros_aplicados_sem_data_inicio_e_sem_data_final(conta_associacao_exportacao_csv): + queryset = ContaAssociacao.objects.all() + + dados = ExportacaoDadosContasService( + queryset=queryset + ).get_texto_filtro_aplicado() + + resultado_esperado = "" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_com_data_final(conta_associacao_exportacao_csv): + queryset = ContaAssociacao.objects.all() + + data_inicio = datetime.date.today() + data_final = datetime.date.today() + + dados = ExportacaoDadosContasService( + queryset=queryset, + data_inicio=str(data_inicio), + data_final=str(data_final) + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: {data_inicio.strftime('%d/%m/%Y')} a {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_sem_data_final(conta_associacao_exportacao_csv): + queryset = ContaAssociacao.objects.all() + + data_inicio = datetime.date.today() + + dados = ExportacaoDadosContasService( + queryset=queryset, + data_inicio=str(data_inicio), + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: A partir de {data_inicio.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_com_data_final(conta_associacao_exportacao_csv): + queryset = ContaAssociacao.objects.all() + + data_final = datetime.date.today() + + dados = ExportacaoDadosContasService( + queryset=queryset, + data_final=str(data_final), + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: Até {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_cria_registro_central_download(usuario_para_teste): + exportacao_dados_contas = ExportacaoDadosContasService( + nome_arquivo='dados_contas.csv', + user=usuario_para_teste.username + ) + + exportacao_dados_contas.cria_registro_central_download() + objeto_arquivo_download = exportacao_dados_contas.objeto_arquivo_download + + assert objeto_arquivo_download.status == ArquivoDownload.STATUS_EM_PROCESSAMENTO + assert objeto_arquivo_download.identificador == 'dados_contas.csv' + assert ArquivoDownload.objects.count() == 1 + + +def test_envia_arquivo_central_download(usuario_para_teste): + with NamedTemporaryFile( + mode="r+", + newline='', + encoding='utf-8', + prefix='dados_contas', + suffix='.csv' + ) as file: + file.write("testando central de download") + + exportacao_dados_contas = ExportacaoDadosContasService( + nome_arquivo='dados_contas.csv', + user=usuario_para_teste.username + ) + exportacao_dados_contas.cria_registro_central_download() + exportacao_dados_contas.envia_arquivo_central_download(file) + objeto_arquivo_download = exportacao_dados_contas.objeto_arquivo_download + + assert objeto_arquivo_download.status == ArquivoDownload.STATUS_CONCLUIDO + assert objeto_arquivo_download.identificador == 'dados_contas.csv' + assert ArquivoDownload.objects.count() == 1 diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_despesas/test_exportacoes_despesas.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_despesas/test_exportacoes_despesas.py index 63ba0a4b9..4b243a099 100644 --- a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_despesas/test_exportacoes_despesas.py +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_despesas/test_exportacoes_despesas.py @@ -7,6 +7,7 @@ pytestmark = pytest.mark.django_db + def test_dados_esperados_csv(queryset_ordered): dados = ExportacoesDocumentosDespesasService(queryset=queryset_ordered).monta_dados() @@ -44,6 +45,7 @@ def test_dados_esperados_csv(queryset_ordered): assert linha_individual == resultado_esperado + def test_cabecalho(): dados = ExportacoesDocumentosDespesasService() @@ -80,17 +82,24 @@ def test_cabecalho(): assert cabecalho == resultado_esperado + def test_rodape(ambiente): - dados = ExportacoesDocumentosDespesasService().texto_rodape() + dados = ExportacoesDocumentosDespesasService( + user="12345" + ).texto_rodape() data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - resultado_esperado = f"Arquivo gerado pelo {ambiente.prefixo} em {data_atual}" + # resultado_esperado = f"Arquivo gerado pelo {ambiente.prefixo} em {data_atual}" + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário 12345 em {data_atual}" assert dados == resultado_esperado + def test_filtra_range_data_fora_do_range(queryset_ordered): - data_inicio = datetime.date(2020, 2, 10) - data_final = datetime.date(2020, 5, 10) + data_inicio = datetime.date(2020, 2, 25) + data_inicio = data_inicio.strftime('%Y-%m-%d') + data_final = datetime.date(2020, 4, 26) + data_final = data_final.strftime('%Y-%m-%d') queryset_filtrado = ExportacoesDocumentosDespesasService( queryset=queryset_ordered, @@ -100,9 +109,12 @@ def test_filtra_range_data_fora_do_range(queryset_ordered): assert queryset_filtrado.count() == 0 + def test_filtra_range_data_dentro_do_range(queryset_ordered): data_inicio = datetime.date.today() + data_inicio = data_inicio.strftime('%Y-%m-%d') data_final = datetime.date.today() + data_final = data_final.strftime('%Y-%m-%d') queryset_filtrado = ExportacoesDocumentosDespesasService( queryset=queryset_ordered, @@ -112,8 +124,10 @@ def test_filtra_range_data_dentro_do_range(queryset_ordered): assert queryset_filtrado.count() == len(queryset_ordered) + def test_filtra_range_data_com_data_inicio_e_sem_data_final(queryset_ordered): data_inicio = datetime.date.today() + data_inicio = data_inicio.strftime('%Y-%m-%d') queryset_filtrado = ExportacoesDocumentosDespesasService( queryset=queryset_ordered, @@ -122,8 +136,10 @@ def test_filtra_range_data_com_data_inicio_e_sem_data_final(queryset_ordered): assert queryset_filtrado.count() == len(queryset_ordered) + def test_filtra_range_data_sem_data_inicio_e_com_data_final(queryset_ordered): data_final = datetime.date.today() + data_final = data_final.strftime('%Y-%m-%d') queryset_filtrado = ExportacoesDocumentosDespesasService( queryset=queryset_ordered, @@ -132,13 +148,15 @@ def test_filtra_range_data_sem_data_inicio_e_com_data_final(queryset_ordered): assert queryset_filtrado.count() == len(queryset_ordered) + def test_filtra_range_data_sem_data_inicio_e_sem_data_final(queryset_ordered): queryset_filtrado = ExportacoesDocumentosDespesasService( queryset=queryset_ordered ).filtra_range_data('criado_em') - + assert queryset_filtrado.count() == len(queryset_ordered) + def test_cria_registro_central_download(usuario_para_teste): exportacao_saldo_final = ExportacoesDocumentosDespesasService( nome_arquivo='pcs_despesas.csv', @@ -152,6 +170,7 @@ def test_cria_registro_central_download(usuario_para_teste): assert objeto_arquivo_download.identificador == 'pcs_despesas.csv' assert ArquivoDownload.objects.count() == 1 + def test_envia_arquivo_central_download(usuario_para_teste): with NamedTemporaryFile( mode="r+", @@ -172,4 +191,58 @@ def test_envia_arquivo_central_download(usuario_para_teste): assert objeto_arquivo_download.status == ArquivoDownload.STATUS_CONCLUIDO assert objeto_arquivo_download.identificador == 'pcs_despesas.csv' - assert ArquivoDownload.objects.count() == 1 \ No newline at end of file + assert ArquivoDownload.objects.count() == 1 + + +def test_filtros_aplicados_sem_data_inicio_e_sem_data_final(queryset_ordered, ambiente): + dados = ExportacoesDocumentosDespesasService( + queryset=queryset_ordered, + ).get_informacoes_download() + + resultado_esperado = "" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_com_data_final(queryset_ordered, ambiente): + + data_inicio = '2024-03-01' + data_final = '2024-03-26' + + dados = ExportacoesDocumentosDespesasService( + queryset=queryset_ordered, + data_inicio=data_inicio, + data_final=data_final + ).get_informacoes_download() + + resultado_esperado = f"Filtro aplicado: 01/03/2024 a 26/03/2024 (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_sem_data_final(queryset_ordered, ambiente): + + data_inicio = '2024-03-01' + + dados = ExportacoesDocumentosDespesasService( + queryset=queryset_ordered, + data_inicio=data_inicio, + ).get_informacoes_download() + + resultado_esperado = f"Filtro aplicado: A partir de 01/03/2024 (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_com_data_final(queryset_ordered, ambiente): + + data_final = '2024-03-26' + + dados = ExportacoesDocumentosDespesasService( + queryset=queryset_ordered, + data_final=data_final + ).get_informacoes_download() + + resultado_esperado = f"Filtro aplicado: Até 26/03/2024 (data de criação do registro)" + + assert dados == resultado_esperado diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_rateios/__init__.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_rateios/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_rateios.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_rateios/test_exportacoes_dados_rateios.py similarity index 80% rename from sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_rateios.py rename to sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_rateios/test_exportacoes_dados_rateios.py index 49934d96b..b324d68f2 100644 --- a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_dados_rateios.py +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_rateios/test_exportacoes_dados_rateios.py @@ -7,6 +7,7 @@ pytestmark = pytest.mark.django_db + def test_cria_registro_central_download(usuario_para_teste): exportacao_rateio = ExportacoesRateiosService( nome_arquivo='despesas_classificacao_item.csv', @@ -43,9 +44,10 @@ def test_envia_arquivo_central_download(usuario_para_teste): assert objeto_arquivo_download.identificador == 'despesas_classificacao_item.csv' assert ArquivoDownload.objects.count() == 1 + def test_filtra_range_data_fora_do_range(rateios_despesa_queryset): - data_inicio = datetime.date(2020, 2, 25) - data_final = datetime.date(2020, 4, 26) + data_inicio = str(datetime.date(2020, 2, 25)) + data_final = str(datetime.date(2020, 4, 26)) queryset_filtrado = ExportacoesRateiosService( queryset=rateios_despesa_queryset, @@ -55,9 +57,10 @@ def test_filtra_range_data_fora_do_range(rateios_despesa_queryset): assert queryset_filtrado.count() == 0 + def test_filtra_range_data_dentro_do_range(rateios_despesa_queryset): - data_inicio = datetime.date.today() - data_final = datetime.date.today() + data_inicio = str(datetime.date.today()) + data_final = str(datetime.date.today()) queryset_filtrado = ExportacoesRateiosService( queryset=rateios_despesa_queryset, @@ -67,8 +70,9 @@ def test_filtra_range_data_dentro_do_range(rateios_despesa_queryset): assert queryset_filtrado.count() == len(rateios_despesa_queryset) + def test_filtra_range_data_com_data_inicio_e_sem_data_final(rateios_despesa_queryset): - data_inicio = datetime.date.today() + data_inicio = str(datetime.date.today()) queryset_filtrado = ExportacoesRateiosService( queryset=rateios_despesa_queryset, @@ -77,8 +81,9 @@ def test_filtra_range_data_com_data_inicio_e_sem_data_final(rateios_despesa_quer assert queryset_filtrado.count() == len(rateios_despesa_queryset) + def test_filtra_range_data_sem_data_inicio_e_com_data_final(rateios_despesa_queryset): - data_final = datetime.date.today() + data_final = str(datetime.date.today()) queryset_filtrado = ExportacoesRateiosService( queryset=rateios_despesa_queryset, @@ -87,6 +92,7 @@ def test_filtra_range_data_sem_data_inicio_e_com_data_final(rateios_despesa_quer assert queryset_filtrado.count() == len(rateios_despesa_queryset) + def test_filtra_range_data_sem_data_inicio_e_sem_data_final(rateios_despesa_queryset): queryset_filtrado = ExportacoesRateiosService( queryset=rateios_despesa_queryset @@ -103,6 +109,7 @@ def test_quantidade_dados_extracao(rateios_despesa_queryset): # Existem tres registros de rateios assert len(dados) == 3 + def test_quantidade_linha_individual_dados_extracao(rateios_despesa_queryset): dados = ExportacoesRateiosService( queryset=rateios_despesa_queryset, @@ -115,6 +122,7 @@ def test_quantidade_linha_individual_dados_extracao(rateios_despesa_queryset): Codigo eol Nome unidade Nome associacao + DRE ID do Gasto Número do documento Tipo de documento @@ -147,7 +155,8 @@ def test_quantidade_linha_individual_dados_extracao(rateios_despesa_queryset): UUID do rateio """ - assert len(linha_individual) == 33 + assert len(linha_individual) == 34 + def test_resultado_esperado_dados_extracao(rateios_despesa_queryset): dados = ExportacoesRateiosService( @@ -161,6 +170,7 @@ def test_resultado_esperado_dados_extracao(rateios_despesa_queryset): primeiro_rateio.associacao.unidade.codigo_eol, primeiro_rateio.associacao.unidade.nome, primeiro_rateio.associacao.nome, + primeiro_rateio.associacao.unidade.nome_dre, primeiro_rateio.despesa.id, primeiro_rateio.despesa.numero_documento, primeiro_rateio.despesa.tipo_documento.nome, @@ -195,6 +205,7 @@ def test_resultado_esperado_dados_extracao(rateios_despesa_queryset): assert linha_individual == resultado_esperado + def test_cabecalho(rateios_despesa_queryset): dados = ExportacoesRateiosService( queryset=rateios_despesa_queryset, @@ -206,6 +217,7 @@ def test_cabecalho(rateios_despesa_queryset): 'Código EOL', 'Nome Unidade', 'Nome Associação', + 'DRE', 'ID do Gasto', 'Número do documento', 'Tipo de documento', @@ -240,13 +252,65 @@ def test_cabecalho(rateios_despesa_queryset): assert cabecalho == resultado_esperado + def test_rodape(rateios_despesa_queryset, ambiente): dados = ExportacoesRateiosService( queryset=rateios_despesa_queryset, + user="12345" ).texto_rodape() data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - resultado_esperado = f"Arquivo gerado pelo {ambiente.prefixo} em {data_atual}" + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário 12345 em {data_atual}" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_sem_data_final(rateios_despesa_queryset): + dados = ExportacoesRateiosService( + queryset=rateios_despesa_queryset, + ).get_texto_filtro_aplicado() + + resultado_esperado = "" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_com_data_final(rateios_despesa_queryset): + data_inicio = datetime.date.today() + data_final = datetime.date.today() + + dados = ExportacoesRateiosService( + queryset=rateios_despesa_queryset, + data_inicio=str(data_inicio), + data_final=str(data_final) + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: {data_inicio.strftime('%d/%m/%Y')} a {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_sem_data_final(rateios_despesa_queryset): + data_inicio = datetime.date.today() + + dados = ExportacoesRateiosService( + queryset=rateios_despesa_queryset, + data_inicio=str(data_inicio) + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: A partir de {data_inicio.strftime('%d/%m/%Y')} (data de criação do registro)" assert dados == resultado_esperado + +def test_filtros_aplicados_sem_data_inicio_e_com_data_final(rateios_despesa_queryset): + data_final = datetime.date.today() + + dados = ExportacoesRateiosService( + queryset=rateios_despesa_queryset, + data_final=str(data_final) + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: Até {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_repasses/__init__.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_repasses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_repasses/conftest.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_repasses/conftest.py new file mode 100644 index 000000000..676f1ecaa --- /dev/null +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_repasses/conftest.py @@ -0,0 +1,27 @@ +import pytest +from model_bakery import baker + + +@pytest.fixture +def ambiente(): + return baker.make( + 'Ambiente', + prefixo='dev-sig-escola', + nome='Ambiente de desenvolvimento', + ) + + +@pytest.fixture +def repasse_exportacao_csv(associacao, conta_associacao, acao_associacao, periodo_2020_1): + return baker.make( + 'Repasse', + associacao=associacao, + periodo=periodo_2020_1, + valor_custeio=1000, + valor_capital=1000, + conta_associacao=conta_associacao, + acao_associacao=acao_associacao, + status='PENDENTE', + realizado_capital=False, + realizado_custeio=True + ) diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_repasses/test_exportacoes_repasses.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_repasses/test_exportacoes_repasses.py new file mode 100644 index 000000000..22c769e5a --- /dev/null +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_dados_repasses/test_exportacoes_repasses.py @@ -0,0 +1,242 @@ +import datetime +from tempfile import NamedTemporaryFile +import pytest +from sme_ptrf_apps.core.models.arquivos_download import ArquivoDownload + +from sme_ptrf_apps.sme.services.exporta_repasses_service import ExportacaoDadosRepassesService +from sme_ptrf_apps.receitas.models.repasse import Repasse + +pytestmark = pytest.mark.django_db + + +def test_cabecalho(): + dados = ExportacaoDadosRepassesService() + + cabecalho = [cabecalho[0] for cabecalho in dados.cabecalho] + + resultado_esperado = [ + 'Código EOL', + 'Nome Unidade', + 'Nome Associação', + 'DRE', + 'Período', + 'Nome do tipo de conta', + 'Nome da Ação', + 'Valor custeio', + 'Valor capital', + 'Valor livre aplicação', + 'Realizado custeio?', + 'Realizado capital?', + 'Realizado livre aplicação?', + 'Carga origem', + 'ID da linha da carga origem', + 'Data e hora de criação', + ] + + assert cabecalho == resultado_esperado + + +def test_dados_repasses_esperados_csv(repasse_exportacao_csv, ambiente): + queryset = Repasse.objects.all() + + dados = ExportacaoDadosRepassesService(queryset=queryset).monta_dados() + + linha_individual = dados[0] + + resultado_esperado = [ + repasse_exportacao_csv.associacao.unidade.codigo_eol, + repasse_exportacao_csv.associacao.unidade.nome, + repasse_exportacao_csv.associacao.nome, + repasse_exportacao_csv.associacao.unidade.nome_dre, + repasse_exportacao_csv.periodo.referencia, + repasse_exportacao_csv.conta_associacao.tipo_conta.nome, + repasse_exportacao_csv.acao_associacao.acao.nome, + "1000,00", + "1000,00", + "0,00", + "Sim", + "Não", + "Não", + None, + 0, + repasse_exportacao_csv.criado_em.strftime("%d/%m/%Y às %H:%M:%S") + + ] + + assert linha_individual == resultado_esperado + + +def test_rodape(repasse_exportacao_csv, ambiente): + queryset = Repasse.objects.all() + + dados = ExportacaoDadosRepassesService( + queryset=queryset, + user="12345" + ).texto_info_arquivo_gerado() + + data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário 12345 em {data_atual}" + + assert dados == resultado_esperado + + +def test_filtra_range_data_fora_do_range(repasse_exportacao_csv): + queryset = Repasse.objects.all() + + data_inicio = str(datetime.date(2020, 2, 10)) + data_final = str(datetime.date(2020, 5, 10)) + + queryset_filtrado = ExportacaoDadosRepassesService( + queryset=queryset, + data_inicio=data_inicio, + data_final=data_final + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == 0 + + +def test_filtra_range_data_dentro_do_range(repasse_exportacao_csv): + queryset = Repasse.objects.all() + + data_inicio = str(datetime.date.today()) + data_final = str(datetime.date.today()) + + queryset_filtrado = ExportacaoDadosRepassesService( + queryset=queryset, + data_inicio=data_inicio, + data_final=data_final + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtra_range_data_com_data_inicio_e_sem_data_final(repasse_exportacao_csv): + queryset = Repasse.objects.all() + + data_inicio = str(datetime.date.today()) + + queryset_filtrado = ExportacaoDadosRepassesService( + queryset=queryset, + data_inicio=data_inicio + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtra_range_data_sem_data_inicio_e_com_data_final(repasse_exportacao_csv): + queryset = Repasse.objects.all() + + data_final = str(datetime.date.today()) + + queryset_filtrado = ExportacaoDadosRepassesService( + queryset=queryset, + data_final=data_final + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtra_range_data_sem_data_inicio_e_sem_data_final(repasse_exportacao_csv): + queryset = Repasse.objects.all() + + queryset_filtrado = ExportacaoDadosRepassesService( + queryset=queryset + ).filtra_range_data('criado_em') + + assert queryset_filtrado.count() == len(queryset) + + +def test_filtros_aplicados_sem_data_inicio_e_sem_data_final(repasse_exportacao_csv): + queryset = Repasse.objects.all() + + dados = ExportacaoDadosRepassesService( + queryset=queryset + ).get_texto_filtro_aplicado() + + resultado_esperado = "" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_com_data_final(repasse_exportacao_csv): + queryset = Repasse.objects.all() + + data_inicio = datetime.date.today() + data_final = datetime.date.today() + + dados = ExportacaoDadosRepassesService( + queryset=queryset, + data_inicio=str(data_inicio), + data_final=str(data_final) + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: {data_inicio.strftime('%d/%m/%Y')} a {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_sem_data_final(repasse_exportacao_csv): + queryset = Repasse.objects.all() + + data_inicio = datetime.date.today() + + dados = ExportacaoDadosRepassesService( + queryset=queryset, + data_inicio=str(data_inicio), + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: A partir de {data_inicio.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_com_data_final(repasse_exportacao_csv): + queryset = Repasse.objects.all() + + data_final = datetime.date.today() + + dados = ExportacaoDadosRepassesService( + queryset=queryset, + data_final=str(data_final), + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: Até {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_cria_registro_central_download(usuario_para_teste): + exportacao_repasses = ExportacaoDadosRepassesService( + nome_arquivo='repasses.csv', + user=usuario_para_teste.username + ) + + exportacao_repasses.cria_registro_central_download() + objeto_arquivo_download = exportacao_repasses.objeto_arquivo_download + + assert objeto_arquivo_download.status == ArquivoDownload.STATUS_EM_PROCESSAMENTO + assert objeto_arquivo_download.identificador == 'repasses.csv' + assert ArquivoDownload.objects.count() == 1 + + +def test_envia_arquivo_central_download(usuario_para_teste): + with NamedTemporaryFile( + mode="r+", + newline='', + encoding='utf-8', + prefix='repasses', + suffix='.csv' + ) as file: + file.write("testando central de download") + + exportacao_repasses = ExportacaoDadosRepassesService( + nome_arquivo='repasses.csv', + user=usuario_para_teste.username + ) + exportacao_repasses.cria_registro_central_download() + exportacao_repasses.envia_arquivo_central_download(file) + objeto_arquivo_download = exportacao_repasses.objeto_arquivo_download + + assert objeto_arquivo_download.status == ArquivoDownload.STATUS_CONCLUIDO + assert objeto_arquivo_download.identificador == 'repasses.csv' + assert ArquivoDownload.objects.count() == 1 diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_relacoes_bens/__init__.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_relacoes_bens/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_relacao_bens.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_relacoes_bens/test_exportacoes_relacao_bens.py similarity index 74% rename from sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_relacao_bens.py rename to sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_relacoes_bens/test_exportacoes_relacao_bens.py index 34d566e4e..284a6e80d 100644 --- a/sme_ptrf_apps/sme/tests/tests_services/test_exportacoes_relacao_bens.py +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_relacoes_bens/test_exportacoes_relacao_bens.py @@ -21,6 +21,7 @@ def test_cria_registro_central_download(usuario_para_teste): assert objeto_arquivo_download.identificador == 'pcs_relacoes_bens.csv' assert ArquivoDownload.objects.count() == 1 + def test_envia_arquivo_central_download(usuario_para_teste): with NamedTemporaryFile( mode="r+", @@ -43,9 +44,10 @@ def test_envia_arquivo_central_download(usuario_para_teste): assert objeto_arquivo_download.identificador == 'pcs_relacoes_bens.csv' assert ArquivoDownload.objects.count() == 1 + def test_filtra_range_data_fora_do_range(relacao_bens_queryset): - data_inicio = datetime.date(2020, 2, 25) - data_final = datetime.date(2020, 4, 26) + data_inicio = str(datetime.date(2020, 2, 25)) + data_final = str(datetime.date(2020, 4, 26)) queryset_filtrado = ExportacoesDadosRelacaoBensService( queryset=relacao_bens_queryset, @@ -55,9 +57,10 @@ def test_filtra_range_data_fora_do_range(relacao_bens_queryset): assert queryset_filtrado.count() == 0 + def test_filtra_range_data_dentro_do_range(relacao_bens_queryset): - data_inicio = datetime.date.today() - data_final = datetime.date.today() + data_inicio = str(datetime.date.today()) + data_final = str(datetime.date.today()) queryset_filtrado = ExportacoesDadosRelacaoBensService( queryset=relacao_bens_queryset, @@ -67,8 +70,9 @@ def test_filtra_range_data_dentro_do_range(relacao_bens_queryset): assert queryset_filtrado.count() == len(relacao_bens_queryset) + def test_filtra_range_data_com_data_inicio_e_sem_data_final(relacao_bens_queryset): - data_inicio = datetime.date.today() + data_inicio = str(datetime.date.today()) queryset_filtrado = ExportacoesDadosRelacaoBensService( queryset=relacao_bens_queryset, @@ -77,8 +81,9 @@ def test_filtra_range_data_com_data_inicio_e_sem_data_final(relacao_bens_queryse assert queryset_filtrado.count() == len(relacao_bens_queryset) + def test_filtra_range_data_sem_data_inicio_e_com_data_final(relacao_bens_queryset): - data_final = datetime.date.today() + data_final = str(datetime.date.today()) queryset_filtrado = ExportacoesDadosRelacaoBensService( queryset=relacao_bens_queryset, @@ -87,6 +92,7 @@ def test_filtra_range_data_sem_data_inicio_e_com_data_final(relacao_bens_queryse assert queryset_filtrado.count() == len(relacao_bens_queryset) + def test_filtra_range_data_sem_data_inicio_e_sem_data_final(relacao_bens_queryset): queryset_filtrado = ExportacoesDadosRelacaoBensService( queryset=relacao_bens_queryset @@ -103,6 +109,7 @@ def test_quantidade_dados_extracao(relacao_bens_queryset): # Existem dois registros de relação de bens assert len(dados) == 2 + def test_quantidade_linha_individual_dados_extracao(relacao_bens_queryset): dados = ExportacoesDadosRelacaoBensService( queryset=relacao_bens_queryset, @@ -115,6 +122,7 @@ def test_quantidade_linha_individual_dados_extracao(relacao_bens_queryset): Codigo eol Nome unidade Nome associacao + DRE Referencia do periodo Status da PC Nome do tipo de Conta @@ -125,7 +133,8 @@ def test_quantidade_linha_individual_dados_extracao(relacao_bens_queryset): Data e hora da última atualização """ - assert len(linha_individual) == 11 + assert len(linha_individual) == 12 + def test_resultado_esperado_dados_extracao(relacao_bens_queryset, ambiente): dados = ExportacoesDadosRelacaoBensService( @@ -139,6 +148,7 @@ def test_resultado_esperado_dados_extracao(relacao_bens_queryset, ambiente): primeira_relacao_bens.prestacao_conta.associacao.unidade.codigo_eol, primeira_relacao_bens.prestacao_conta.associacao.unidade.nome, primeira_relacao_bens.prestacao_conta.associacao.nome, + primeira_relacao_bens.prestacao_conta.associacao.unidade.nome_dre, primeira_relacao_bens.prestacao_conta.periodo.referencia, primeira_relacao_bens.prestacao_conta.status, primeira_relacao_bens.conta_associacao.tipo_conta.nome, @@ -151,6 +161,7 @@ def test_resultado_esperado_dados_extracao(relacao_bens_queryset, ambiente): assert linha_individual == resultado_esperado + def test_cabecalho(relacao_bens_queryset): dados = ExportacoesDadosRelacaoBensService( queryset=relacao_bens_queryset, @@ -162,6 +173,7 @@ def test_cabecalho(relacao_bens_queryset): 'Código EOL', 'Nome Unidade', 'Nome Associação', + 'DRE', 'Referência do Período da PC', 'Status da PC', 'Nome do tipo de Conta', @@ -174,13 +186,65 @@ def test_cabecalho(relacao_bens_queryset): assert cabecalho == resultado_esperado + def test_rodape(relacao_bens_queryset, ambiente): dados = ExportacoesDadosRelacaoBensService( queryset=relacao_bens_queryset, + user="12345" ).texto_rodape() data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - resultado_esperado = f"Arquivo gerado pelo {ambiente.prefixo} em {data_atual}" + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário 12345 em {data_atual}" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_sem_data_final(relacao_bens_queryset): + dados = ExportacoesDadosRelacaoBensService( + queryset=relacao_bens_queryset, + ).get_texto_filtro_aplicado() + + resultado_esperado = "" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_com_data_final(relacao_bens_queryset): + data_inicio = datetime.date.today() + data_final = datetime.date.today() + + dados = ExportacoesDadosRelacaoBensService( + queryset=relacao_bens_queryset, + data_inicio=str(data_inicio), + data_final=str(data_final) + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: {data_inicio.strftime('%d/%m/%Y')} a {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_sem_data_final(relacao_bens_queryset): + data_inicio = datetime.date.today() + + dados = ExportacoesDadosRelacaoBensService( + queryset=relacao_bens_queryset, + data_inicio=str(data_inicio), + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: A partir de {data_inicio.strftime('%d/%m/%Y')} (data de criação do registro)" assert dados == resultado_esperado + +def test_filtros_aplicados_sem_data_inicio_e_com_data_final(relacao_bens_queryset): + data_final = datetime.date.today() + + dados = ExportacoesDadosRelacaoBensService( + queryset=relacao_bens_queryset, + data_final=str(data_final) + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: Até {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/conftest.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/conftest.py index f4a8e812b..7d5bba547 100644 --- a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/conftest.py +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/conftest.py @@ -7,6 +7,7 @@ pytestmark = pytest.mark.django_db + @pytest.fixture def motivo_aprovacao_ressalva_1(): return baker.make( @@ -14,6 +15,7 @@ def motivo_aprovacao_ressalva_1(): motivo="Motivo aprovação 1" ) + @pytest.fixture def motivo_aprovacao_ressalva_2(): return baker.make( @@ -21,6 +23,7 @@ def motivo_aprovacao_ressalva_2(): motivo="Motivo aprovação 2" ) + @pytest.fixture def motivo_reprovacao_1(): return baker.make( @@ -28,6 +31,7 @@ def motivo_reprovacao_1(): motivo="Motivo reprovação 1" ) + @pytest.fixture def motivo_reprovacao_2(): return baker.make( @@ -35,12 +39,29 @@ def motivo_reprovacao_2(): motivo="Motivo reprovação 2" ) + @pytest.fixture -def prestacao_conta_aprovada_com_ressalva(periodo, associacao, motivo_aprovacao_ressalva_1, motivo_aprovacao_ressalva_2): +def associacao_1(unidade, periodo_anterior): + return baker.make( + 'Associacao', + nome='Escola Teste', + cnpj='52.302.275/0001-83', + unidade=unidade, + periodo_inicial=periodo_anterior, + ccm='0.000.00-0', + email="ollyverottoboni@gmail.com", + processo_regularidade='123456', + data_de_encerramento=datetime.date(2019, 12, 20) + ) + + +@pytest.fixture +def prestacao_conta_aprovada_com_ressalva(periodo, associacao_1, motivo_aprovacao_ressalva_1, + motivo_aprovacao_ressalva_2): return baker.make( 'PrestacaoConta', periodo=periodo, - associacao=associacao, + associacao=associacao_1, recomendacoes="Recomendação teste", status='APROVADA_RESSALVA', outros_motivos_aprovacao_ressalva='Teste outro motivo aprovação ressalva', @@ -48,6 +69,7 @@ def prestacao_conta_aprovada_com_ressalva(periodo, associacao, motivo_aprovacao_ criado_em=datetime.date(2020, 1, 1) ) + @pytest.fixture def prestacao_conta_reprovada(periodo, outra_associacao, motivo_reprovacao_1, motivo_reprovacao_2): return baker.make( @@ -60,6 +82,7 @@ def prestacao_conta_reprovada(periodo, outra_associacao, motivo_reprovacao_1, mo criado_em=datetime.date(2021, 1, 1) ) + @pytest.fixture def ambiente(): return baker.make( @@ -68,7 +91,14 @@ def ambiente(): nome='Ambiente de desenvolvimento', ) + @pytest.fixture def queryset_ordered(prestacao_conta_aprovada_com_ressalva, prestacao_conta_reprovada): return PrestacaoConta.objects.all().order_by('criado_em') + +@pytest.fixture +def queryset_ordered_dre_sem_pcs(prestacao_conta_aprovada_com_ressalva, prestacao_conta_reprovada, dre_ipiranga): + return PrestacaoConta.objects.filter( + associacao__unidade__dre__uuid=f"{dre_ipiranga.uuid}", + ).order_by('criado_em') diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/test_exportacoes_demonstrativos_financeiros.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/test_exportacoes_demonstrativos_financeiros.py index ab12837d6..a08c6408a 100644 --- a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/test_exportacoes_demonstrativos_financeiros.py +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/test_exportacoes_demonstrativos_financeiros.py @@ -47,8 +47,8 @@ def test_envia_arquivo_central_download(usuario_para_teste): def test_filtra_range_data_fora_do_range(demonstrativo_financeiro_queryset): - data_inicio = datetime.date(2020, 2, 25) - data_final = datetime.date(2020, 4, 26) + data_inicio = str(datetime.date(2020, 2, 25)) + data_final = str(datetime.date(2020, 4, 26)) queryset_filtrado = ExportaDemonstrativosFinanceirosService( queryset=demonstrativo_financeiro_queryset, @@ -60,8 +60,8 @@ def test_filtra_range_data_fora_do_range(demonstrativo_financeiro_queryset): def test_filtra_range_data_dentro_do_range(demonstrativo_financeiro_queryset): - data_inicio = datetime.date.today() - data_final = datetime.date.today() + data_inicio = str(datetime.date.today()) + data_final = str(datetime.date.today()) queryset_filtrado = ExportaDemonstrativosFinanceirosService( queryset=demonstrativo_financeiro_queryset, @@ -73,7 +73,7 @@ def test_filtra_range_data_dentro_do_range(demonstrativo_financeiro_queryset): def test_filtra_range_data_com_data_inicio_e_sem_data_final(demonstrativo_financeiro_queryset): - data_inicio = datetime.date.today() + data_inicio = str(datetime.date.today()) queryset_filtrado = ExportaDemonstrativosFinanceirosService( queryset=demonstrativo_financeiro_queryset, @@ -84,7 +84,7 @@ def test_filtra_range_data_com_data_inicio_e_sem_data_final(demonstrativo_financ def test_filtra_range_data_sem_data_inicio_e_com_data_final(demonstrativo_financeiro_queryset): - data_final = datetime.date.today() + data_final = str(datetime.date.today()) queryset_filtrado = ExportaDemonstrativosFinanceirosService( queryset=demonstrativo_financeiro_queryset, @@ -122,6 +122,7 @@ def test_quantidade_linha_individual_dados_extracao(demonstrativo_financeiro_que Código EOL Nome Unidade Nome Associação + DRE Referência do Período da PC Nome do tipo de Conta Data (Saldo bancário) @@ -134,7 +135,7 @@ def test_quantidade_linha_individual_dados_extracao(demonstrativo_financeiro_que Data e hora da última atualização """ - assert len(linha_individual) == 13 + assert len(linha_individual) == 14 def test_resultado_esperado_dados_extracao( @@ -153,6 +154,7 @@ def test_resultado_esperado_dados_extracao( primeiro_demonstrativo_financeiro.conta_associacao.associacao.unidade.codigo_eol, primeiro_demonstrativo_financeiro.conta_associacao.associacao.unidade.nome, primeiro_demonstrativo_financeiro.conta_associacao.associacao.nome, + primeiro_demonstrativo_financeiro.conta_associacao.associacao.unidade.nome_dre, primeiro_demonstrativo_financeiro.prestacao_conta.periodo.referencia, primeiro_demonstrativo_financeiro.conta_associacao.tipo_conta.nome, datetime.date(2020, 7, 1), @@ -179,6 +181,7 @@ def test_cabecalho(demonstrativo_financeiro_queryset): 'Código EOL', 'Nome Unidade', 'Nome Associação', + 'DRE', 'Referência do Período da PC', 'Nome do tipo de Conta', 'Data (Saldo bancário)', @@ -197,9 +200,61 @@ def test_cabecalho(demonstrativo_financeiro_queryset): def test_rodape(demonstrativo_financeiro_queryset, ambiente): dados = ExportaDemonstrativosFinanceirosService( queryset=demonstrativo_financeiro_queryset, + user="12345" ).texto_rodape() data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") - resultado_esperado = f"Arquivo gerado pelo {ambiente.prefixo} em {data_atual}" + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário 12345 em {data_atual}" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_sem_data_final(demonstrativo_financeiro_queryset): + dados = ExportaDemonstrativosFinanceirosService( + queryset=demonstrativo_financeiro_queryset, + ).get_texto_filtro_aplicado() + + resultado_esperado = "" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_com_data_final(demonstrativo_financeiro_queryset): + data_inicio = datetime.date.today() + data_final = datetime.date.today() + + dados = ExportaDemonstrativosFinanceirosService( + queryset=demonstrativo_financeiro_queryset, + data_inicio=str(data_inicio), + data_final=str(data_final) + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: {data_inicio.strftime('%d/%m/%Y')} a {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_com_data_inicio_e_sem_data_final(demonstrativo_financeiro_queryset): + data_inicio = datetime.date.today() + + dados = ExportaDemonstrativosFinanceirosService( + queryset=demonstrativo_financeiro_queryset, + data_inicio=str(data_inicio), + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: A partir de {data_inicio.strftime('%d/%m/%Y')} (data de criação do registro)" + + assert dados == resultado_esperado + + +def test_filtros_aplicados_sem_data_inicio_e_com_data_final(demonstrativo_financeiro_queryset): + data_final = datetime.date.today() + + dados = ExportaDemonstrativosFinanceirosService( + queryset=demonstrativo_financeiro_queryset, + data_final=str(data_final) + ).get_texto_filtro_aplicado() + + resultado_esperado = f"Filtro aplicado: Até {data_final.strftime('%d/%m/%Y')} (data de criação do registro)" assert dados == resultado_esperado diff --git a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/test_exportacoes_status_prestacoes_conta.py b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/test_exportacoes_status_prestacoes_conta.py index 06a8b7b0d..eddb34322 100644 --- a/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/test_exportacoes_status_prestacoes_conta.py +++ b/sme_ptrf_apps/sme/tests/tests_services/tests_exportacoes_status_prestacoes_conta/test_exportacoes_status_prestacoes_conta.py @@ -6,10 +6,18 @@ pytestmark = pytest.mark.django_db resultado_esperado = [ - ['123456', 'Escola Teste', 'Escola Teste', '2019.2', 'APROVADA_RESSALVA', 'Motivo aprovação 1; Motivo aprovação 2; Teste outro motivo aprovação ressalva', 'Recomendação teste', ''], - ['123456', 'Escola Teste', 'Outra', '2019.2', 'REPROVADA', '', '', 'Motivo reprovação 1; Motivo reprovação 2; Teste outro motivo reprovação'] + ['123456', 'Escola Teste', 'Escola Teste', 'DRE teste', '2019.2', 'APROVADA_RESSALVA', + 'Motivo aprovação 1; Motivo aprovação 2; Teste outro motivo aprovação ressalva', 'Recomendação teste', ''], + ['123456', 'Escola Teste', 'Outra', 'DRE teste', '2019.2', 'REPROVADA', '', '', + 'Motivo reprovação 1; Motivo reprovação 2; Teste outro motivo reprovação'] ] +resultado_pcs_nao_apresentadas = [ + ['123456', 'Escola Teste', 'Escola Teste', 'DRE teste', '2019.3', 'NAO_APRESENTADA'], + ['123456', 'Escola Teste', 'Outra', 'DRE teste', '2019.3', 'NAO_APRESENTADA'], + ['123456', 'Escola Teste', 'Outra', 'DRE teste', '2020.1', 'NAO_APRESENTADA']] + + def test_dados_esperados_csv(queryset_ordered): dados = ExportacoesStatusPrestacoesContaService( queryset=queryset_ordered @@ -17,6 +25,103 @@ def test_dados_esperados_csv(queryset_ordered): assert dados == resultado_esperado + +def test_dados_esperados_csv_visao_dre(queryset_ordered, dre): + dados = ExportacoesStatusPrestacoesContaService( + queryset=queryset_ordered, + dre_uuid=f"{dre.uuid}" + + ).monta_dados() + + assert dados == resultado_esperado + + +def test_dados_esperados_csv_visao_dre_sem_pcs(queryset_ordered_dre_sem_pcs, dre_ipiranga): + dados = ExportacoesStatusPrestacoesContaService( + queryset=queryset_ordered_dre_sem_pcs, + dre_uuid=f"{dre_ipiranga.uuid}" + + ).monta_dados() + + assert dados == [] + + +def test_dados_esperados_pcs_nao_apresentadas_csv(queryset_ordered, periodo_factory): + data_inicio = datetime.date(2019, 1, 10) + data_final = datetime.date(2020, 12, 29) + + periodo_2019_3 = periodo_factory.create(referencia='2019.3', + data_inicio_realizacao_despesas=datetime.date(2019, 12, 1), + data_fim_realizacao_despesas=datetime.date(2019, 12, 31)) + + periodo_factory.create(periodo_anterior=periodo_2019_3, referencia='2020.1', + data_inicio_realizacao_despesas=datetime.date(2020, 1, 1), + data_fim_realizacao_despesas=datetime.date(2020, 12, 31)) + + servico = ExportacoesStatusPrestacoesContaService( + queryset=queryset_ordered, + data_inicio=data_inicio, + data_final=data_final + ) + + servico.define_periodos_selecionados_no_range_do_filtro_de_data() + pcs_nao_apresentadas = servico.monta_dados_pcs_nao_apresentadas() + + # Existem apenas 3 "pcs" não apresentadas, já que uma foi encerrada no periodo 2019.3 + assert resultado_pcs_nao_apresentadas == pcs_nao_apresentadas + + +def test_dados_esperados_pcs_nao_apresentadas_csv_visao_dre(queryset_ordered, periodo_factory, dre): + data_inicio = datetime.date(2019, 1, 10) + data_final = datetime.date(2020, 12, 29) + + periodo_2019_3 = periodo_factory.create(referencia='2019.3', + data_inicio_realizacao_despesas=datetime.date(2019, 12, 1), + data_fim_realizacao_despesas=datetime.date(2019, 12, 31)) + + periodo_factory.create(periodo_anterior=periodo_2019_3, referencia='2020.1', + data_inicio_realizacao_despesas=datetime.date(2020, 1, 1), + data_fim_realizacao_despesas=datetime.date(2020, 12, 31)) + + servico = ExportacoesStatusPrestacoesContaService( + queryset=queryset_ordered, + data_inicio=data_inicio, + data_final=data_final, + dre_uuid=f"{dre.uuid}" + ) + + servico.define_periodos_selecionados_no_range_do_filtro_de_data() + pcs_nao_apresentadas = servico.monta_dados_pcs_nao_apresentadas() + + # Existem apenas 3 "pcs" não apresentadas, já que uma foi encerrada no periodo 2019.3 + assert resultado_pcs_nao_apresentadas == pcs_nao_apresentadas + + +def test_dados_esperados_pcs_nao_apresentadas_csv_visao_dre_sem_pcs(queryset_ordered_dre_sem_pcs, periodo_factory, dre_ipiranga): + data_inicio = datetime.date(2019, 1, 10) + data_final = datetime.date(2020, 12, 29) + + periodo_2019_3 = periodo_factory.create(referencia='2019.3', + data_inicio_realizacao_despesas=datetime.date(2019, 12, 1), + data_fim_realizacao_despesas=datetime.date(2019, 12, 31)) + + periodo_factory.create(periodo_anterior=periodo_2019_3, referencia='2020.1', + data_inicio_realizacao_despesas=datetime.date(2020, 1, 1), + data_fim_realizacao_despesas=datetime.date(2020, 12, 31)) + + servico = ExportacoesStatusPrestacoesContaService( + queryset=queryset_ordered_dre_sem_pcs, + data_inicio=data_inicio, + data_final=data_final, + dre_uuid=f"{dre_ipiranga.uuid}" + ) + + servico.define_periodos_selecionados_no_range_do_filtro_de_data() + pcs_nao_apresentadas = servico.monta_dados_pcs_nao_apresentadas() + + assert pcs_nao_apresentadas == [] + + def test_cabecalho(queryset_ordered): dados = ExportacoesStatusPrestacoesContaService(queryset=queryset_ordered) @@ -26,6 +131,7 @@ def test_cabecalho(queryset_ordered): 'Código EOL', 'Nome Unidade', 'Nome Associação', + 'DRE', 'Referência do Período da PC', 'Status da PC', 'Descrição do motivo aprovação com ressalvas', @@ -44,6 +150,16 @@ def test_rodape(ambiente): assert dados == resultado_esperado + +def test_info_arquivo_gerado(ambiente): + dados = ExportacoesStatusPrestacoesContaService(user='12345').texto_info_arquivo_gerado() + + data_atual = datetime.datetime.now().strftime("%d/%m/%Y às %H:%M:%S") + resultado_esperado = f"Arquivo gerado via {ambiente.prefixo} pelo usuário 12345 em {data_atual}" + + assert dados == resultado_esperado + + def test_filtra_range_data_fora_do_range(queryset_ordered): data_inicio = datetime.date(2020, 2, 10) data_final = datetime.date(2020, 5, 10) @@ -56,6 +172,7 @@ def test_filtra_range_data_fora_do_range(queryset_ordered): assert queryset_filtrado.count() == 0 + def test_filtra_range_data_dentro_do_range(queryset_ordered): data_inicio = datetime.date.today() data_final = datetime.date.today() @@ -68,6 +185,7 @@ def test_filtra_range_data_dentro_do_range(queryset_ordered): assert queryset_filtrado.count() == len(queryset_ordered) + def test_filtra_range_data_com_data_inicio_e_sem_data_final(queryset_ordered): data_inicio = datetime.date.today() @@ -78,6 +196,7 @@ def test_filtra_range_data_com_data_inicio_e_sem_data_final(queryset_ordered): assert queryset_filtrado.count() == len(queryset_ordered) + def test_filtra_range_data_sem_data_inicio_e_com_data_final(queryset_ordered): data_final = datetime.date.today() @@ -90,9 +209,10 @@ def test_filtra_range_data_sem_data_inicio_e_com_data_final(queryset_ordered): assert queryset_filtrado.count() == len(queryset_ordered) + def test_filtra_range_data_sem_data_inicio_e_sem_data_final(queryset_ordered): queryset_filtrado = ExportacoesStatusPrestacoesContaService( queryset=queryset_ordered ).filtra_range_data('criado_em') - assert queryset_filtrado.count() == len(queryset_ordered) \ No newline at end of file + assert queryset_filtrado.count() == len(queryset_ordered)