Reestruturação nos arquivos plano e changelog

This commit is contained in:
Gabriel 2026-09-15 09:43:18 -03:00
parent 78c62d2edb
commit 7d885b8a03
4 changed files with 43 additions and 44 deletions

View File

@ -379,17 +379,15 @@ Usuário viu, pelo dropdown do sino, notificações de ferramenta antigas (algum
### 88. Não Conformidades (Relatórios > Qualidade) — nova aplicação
Pedido (2026-08-27): evoluir a skill do Claude `analise-ncs` (ver `projects/Controladoria - Analise NCs/`), que gerava sob demanda um Excel de 7 abas a partir de dois arquivos exportados do Sigsistem (ocorrências .xlsx + ações .xls, esse último HTML disfarçado), pra uma aplicação dentro do Portal com gestão **contínua**: identificar novas ocorrências sem análise, análises sem ação, acompanhamentos recentes sem tratativa da Qualidade, priorizar vencidas/vencendo em 7 dias, permitir marcar manualmente o que foi tratado, e um dashboard (ações abertas, motivos de abertura, clientes/colaboradores com maior incidência).
Evolui a skill do Claude `analise-ncs` (Excel sob demanda a partir de dois exports do Sigsistem) pra uma gestão contínua dentro do Portal: reabertura automática por diff quando algo muda desde o último tratamento, histórico completo de acompanhamentos e um dashboard (ações vencidas/vencendo, motivos de abertura, clientes/colaboradores com maior incidência). Nasce restrita ao perfil "Inovação" (mesmo padrão adotado depois pro Dashboard Contábil). Histórico técnico completo (models, pacote `portal_api/nao_conformidades/`, e as rodadas seguintes de refinamento) em `portal_api/nao_conformidades/CHANGELOG.md`/`CLAUDE.md` — a partir daqui, rodadas específicas desta aplicação não duplicam entrada aqui (ver nota no topo deste arquivo).
Decisões de negócio confirmadas antes de implementar: status de tratativa é controle interno do Portal (sem escrita de volta no Sigsistem), mas reabre sozinho quando uma importação nova traz algo diferente do que existia no momento do tratamento; guardar o histórico completo de acompanhamentos (não só o último, como a skill fazia); colaborador com maior incidência = `Indicado para Descrever Análise da Ocorrência`; motivo de abertura = `Tipo(s) de Causa(s) da Ocorrência`; permissão de toggle único **restrita por padrão** (só Integração e Inovação nasce com acesso — diferente de "Relatório Setorial", que herda automático via `SECTORAL_KEYS`).
### 92. Dashboard Contábil (Relatórios > Contabilidade) — nova aplicação
Nova aplicação em Relatórios > Qualidade (subgrupo novo em `catalogo.py`, mesmo formato de subgrupo-com-uma-tool já usado em Auditorias) — pacote Python puro `portal_api/nao_conformidades/` (sem pandas, mesmo padrão de `planos_saude`/`indicadores`), 4 models novos, 3 `ModelViewSet` + dashboard, tela nova com 3 abas. Núcleo técnico: reabertura automática por diff (`nao_conformidades/diff.py`) — cada ocorrência/ação tratada guarda um snapshot congelado, comparado a cada importação nova contra o estado atual (ação por data do último acompanhamento; ocorrência por hash de texto da análise ou ação nova aberta). Validado de ponta a ponta com dados reais da amostra fornecida: totais batem com o relatório de referência da skill em tudo, exceto "Total de Ações Abertas" (a skill original contava também linhas sem nenhuma ação de fato aberta — artefato do script, decisão deliberada de não replicar); reimportação idempotente; dois bugs de robustez corrigidos (`assunto` virou `TextField` — um export real trouxe 395 caracteres; e um `SerializerMethodField` que chamava uma propriedade inexistente no model). Interação em navegador não testada nesta rodada (sem ferramenta de automação de browser disponível) — pipeline/API validados via chamadas HTTP diretas, tela construída seguindo os padrões visuais já validados de `importacao-plano-saude.html`/`indicador-desempenho.html`. Detalhe completo em `portal_api/nao_conformidades/CLAUDE.md`.
Nova aplicação em Relatórios > Contabilidade (nasce restrita ao perfil "Inovação", mesmo padrão de "Não Conformidades"): o contador anexa o PDF de Balancete + DRE (modelo Questor), a ferramenta extrai as contas/linhas e roda um motor de regras de auditoria, com revisão de achados/observações. Subgrupo "Contabilidade" novo em `catalogo.py` dentro de `relatorios`. Histórico técnico completo (fórmulas, models, desafio de extração do PDF) e das rodadas seguintes (94+, incluindo o botão "Gerar Dashboard") em `portal_api/dashboard_contabil/CHANGELOG.md`/`CLAUDE.md` — a partir daqui, rodadas específicas desta aplicação não duplicam entrada aqui (ver nota no topo deste arquivo).
### 89. Não Conformidades — redesign de Dashboard/Gestão a partir de teste real na tela
### 93. Menu "Relatórios" — remoção do placeholder "Relatório Setorial"
Depois de testar a rodada 88 na tela, o usuário deu uma lista concreta de feedback de UX. Resumo (detalhe completo em `portal_api/nao_conformidades/CHANGELOG.md`): Dashboard virou a aba padrão ao abrir; 4 cards e as 3 tabelas de ranking ficaram clicáveis (drill-down direto pra Gestão já filtrada); rankings de Clientes/Colaboradores passaram a mostrar a contagem quebrada por tipo com cor (NC/Reclamação/Outros) em vez de restringir o filtro, a pedido explícito do usuário; dois KPIs de tempo médio novos (preenchimento de análise, abertura de ação) — um terceiro (resolução de ações) foi descartado antes de implementar porque o campo correspondente está 100% vazio no export real, e o usuário confirmou pra não criar um card sem dado; seletor de período virou chips em vez de `<select>`. Na Gestão: toda linha ganhou um botão de expandir (mesmo padrão de `.cc-painel-fiscal__toggle`) que revela ocorrência completa ou o histórico de acompanhamentos (mais recente primeiro) — substituiu o modal de histórico, que ficava colado no botão "Marcar como tratado"; a coluna única "Status" de Ações virou duas (Prazo e Tratativa), com filtros em chips combináveis; um campo de busca único passou a filtrar as 3 listas da Gestão de uma vez, por colaborador/empresa/assunto.
Dois bugs reais corrigidos durante o teste: uma corrida (`ativarTab()` tinha efeito colateral de carregamento que corria em paralelo com o carregamento explícito de um drill-down, e o que terminasse por último vencia — às vezes mostrando o filtro errado) e um alinhamento de texto (célula de painel de detalhe herdava `text-align:right` de `.pa-table td:last-child` por ser sempre "a única/última" célula da linha). Instalado Playwright ad-hoc neste ambiente pra validar a tela de ponta a ponta (login, drill-downs, expandir, alternar tratativa, filtros combinados) — não é dependência do projeto, só ferramenta de teste desta sessão.
Pedido (2026-09-14): remover o item "Relatório Setorial" (`href="#"`, sem tela própria) do menu Relatórios, já que o tópico agora tem itens de verdade ("Qualidade" → Não Conformidades, "Contabilidade" → Relatório Contábil). Removido de `MODULE_APPS["relatorios"]` em `catalogo.py` e do `<li data-app="relatorio-setorial">` hardcoded nas 13 páginas-shell (o menu é replicado por template, não um componente único — ver "Ordem de `<script>`" no `CLAUDE.md` raiz pro mesmo padrão de duplicação entre shells). O item "Indicadores" (outro placeholder `href="#"` dentro de Relatórios) não foi tocado, não fazia parte do pedido. Perfis já existentes no banco podem manter uma chave órfã `"relatorio-setorial"` dentro de `permissoes["relatorios"]["apps"]` — inofensiva, mesmo padrão já visto quando "Gerar Contrato"/"Gerar Procuração" foram removidos de Geradoc (ver comentário em `catalogo.py`).
## Limitações conhecidas / decisões assumidas
@ -441,35 +439,3 @@ combinados) — ver `portal_api/nao_conformidades/CHANGELOG.md`. Falta só o
upload real do formulário "Nova Importação" pelo navegador (testado até
agora só via API), e a conferência do usuário sobre o resultado visual do
redesign da rodada 89 e dos novos cards de tempo médio da rodada 90.
### 90. Não Conformidades — tempo médio de execução da ação e ciclo completo
O usuário trouxe um export do Sigsistem diferente do original ("com
finalizadas" — traz o histórico de ações já concluídas, 3474 linhas contra
287 do export padrão, `Data de Finalização` preenchida em 3187), viabilizando
o terceiro KPI de tempo médio que tinha sido descartado na rodada 89 por
falta de dado real. `tempos_medios` do dashboard ganhou uma 3ª etapa
(execução da ação, abertura → finalização) e um KPI de ciclo completo
(emissão da ocorrência → finalização da ação), cada um com o tamanho da
amostra (`n`) junto da média — inclusive retroativo às 2 etapas que já
existiam. A execução também é quebrada por categoria de ação
(Correção/Ação Corretiva/Outros), porque a média geral (~62 dias) escondia
uma variância enorme (1 a ~793 dias) entre tipos bem diferentes. No
frontend, uma primeira versão trocou os cards por um funil visual com setas
entre etapas — revertida no mesmo dia a pedido do usuário, que preferiu
manter o card grid original (agora com 4 cards em vez de 2, cada um com o
`n` da amostra). Detalhe completo em
`portal_api/nao_conformidades/CHANGELOG.md` (rodada 90) e
`portal_api/nao_conformidades/CLAUDE.md` (seção "Tempos médios").
### 91. Não Conformidades — upload travando com arquivo real grande (upsert virou lote)
Usuário reportou "Erro ao processar a solicitação." ao reprocessar os mesmos dois arquivos "com finalizadas" da rodada 90, e perguntou se seria a extensão `.xls`/`.xlsx` ou incompatibilidade com o ambiente Linux de produção. Nenhum dos dois: reproduzido localmente com os arquivos reais, o upload funcionava, só levava ~60s — tempo o bastante pra estourar o timeout de um worker WSGI em produção (gunicorn, 30s por padrão) ou disparar o autoreload do `manage.py runserver` em dev, derrubando a conexão sem nenhum problema de dado por trás. Causa raiz: o upsert (rodada 88) fazia uma ida ao banco por item (`update_or_create`/`get_or_create`), aceitável nos ~300 registros da amostra inicial mas não nos ~1700 ocorrências/~3400 ações/~9000 acompanhamentos do export real (quase 28 mil idas ao banco). Reescrito pra lote (`bulk_create`/`bulk_update`) e pra pular quem não mudou nada desde a última importação (reimportação periódica traz de volta o histórico inteiro, não só o novo) — reimportar o mesmo arquivo sem mudança real caiu de ~60s pra ~4s. Nenhuma migração, nenhuma mudança de contrato de API. Detalhe completo em `portal_api/nao_conformidades/CHANGELOG.md` (rodada 91) e `portal_api/nao_conformidades/CLAUDE.md` (seção "Upsert").
### 92. Dashboard Contábil (Relatórios > Contabilidade) — nova aplicação
Nova aplicação em Relatórios > Contabilidade (nasce restrita ao perfil "Inovação", mesmo padrão de "Não Conformidades"): o contador anexa o PDF de Balancete + DRE (modelo Questor), a ferramenta extrai as contas/linhas e roda um motor de regras de auditoria, com revisão de achados/observações. Subgrupo "Contabilidade" novo em `catalogo.py` dentro de `relatorios`. Histórico técnico completo (fórmulas, models, desafio de extração do PDF) e das rodadas seguintes (94+, incluindo o botão "Gerar Dashboard") em `portal_api/dashboard_contabil/CHANGELOG.md`/`CLAUDE.md` — a partir daqui, rodadas específicas desta aplicação não duplicam entrada aqui (ver nota no topo deste arquivo).
### 93. Menu "Relatórios" — remoção do placeholder "Relatório Setorial"
Pedido (2026-09-14): remover o item "Relatório Setorial" (`href="#"`, sem tela própria) do menu Relatórios, já que o tópico agora tem itens de verdade ("Qualidade" → Não Conformidades, "Contabilidade" → Relatório Contábil). Removido de `MODULE_APPS["relatorios"]` em `catalogo.py` e do `<li data-app="relatorio-setorial">` hardcoded nas 13 páginas-shell (o menu é replicado por template, não um componente único — ver "Ordem de `<script>`" no `CLAUDE.md` raiz pro mesmo padrão de duplicação entre shells). O item "Indicadores" (outro placeholder `href="#"` dentro de Relatórios) não foi tocado, não fazia parte do pedido. Perfis já existentes no banco podem manter uma chave órfã `"relatorio-setorial"` dentro de `permissoes["relatorios"]["apps"]` — inofensiva, mesmo padrão já visto quando "Gerar Contrato"/"Gerar Procuração" foram removidos de Geradoc (ver comentário em `catalogo.py`).

View File

@ -326,3 +326,12 @@ Pedido do usuário: a Amil Odonto está cadastrada duas vezes no Questor, com `c
Usuário reportou (relatório "OPS - Coparticipação Analítico", competência 08/2026) que a mensalidade extraída de um dependente vinha menor que a real: 492,66 em vez dos 558,39 impressos em "VI mensal."/"VI.Pré-Estab." na linha-resumo da pessoa. A ferramenta já tratava esse cenário pra titular desde um bug anterior (rodada 08/2026, motivo "Variação de custo") — a linha extra "Reajuste - <motivo>" que antecede "Preço pré-estabelecido" precisa ser somada a ela, já que o "Preço pré-estabelecido" vem sem o reajuste embutido.
Causa: `_REAJUSTE_RE` (`operadoras/itamed/saude.py`) só reconhecia o motivo fixo "Variação de custo" — a linha do dependente ("Reajuste - Mudança de faixa" 65,73, referente a mudança de faixa etária) não batia no regex e ficava de fora da soma (492,66 + 65,73 = 558,39, batendo com o valor correto). Corrigido generalizando o regex pra casar qualquer texto depois de "Reajuste -" (`(?P<motivo>Reajuste\s*-\s*.+?)\s+(?P<valor>[\d.,]+)\s*$`), usando o motivo capturado como `rubrica` do lançamento em vez do texto fixo "Reajuste - Variação de custo" — a lógica de somar como `tipo_lancamento="mensalidade"` não mudou, só passou a valer pra qualquer motivo de reajuste, não só um.
### Rodada 94 — Nome do arquivo gerado por "Nº Empresa - Nº Operadora"
Pedido do usuário (2026-09-15): o nome do arquivo baixado em "Gerar Arquivo" era sempre genérico (`importacao_plano_saude_<id>.zip` quando 2 tipos de lançamento juntos, `<tipo>.csv` — ex. `mensalidade.csv` — quando só 1), sem nenhuma referência à empresa/operadora daquela execução. Ficava confuso identificar, já no Downloads, duas execuções da mesma empresa com operadoras diferentes (o cenário concreto reportado).
- **`_nome_base_arquivo_gerado_plano_saude(importacao, todas_linhas, prefixo_fallback)`** (nova função módulo-level, `views.py`, logo depois de `_monta_csv_linhas_plano_saude`): monta `"<código empresa> - <código operadora>"` (ex.: `"1970 - 5060"`) — o código de empresa vem da primeira `ImportacaoPlanoSaudeLinha`/`ImportacaoPlanoSaudeDePaulaLinha` não vazia da importação (todas as linhas de uma importação compartilham o mesmo código, mesma premissa já usada por `_codigo_empresa_da_importacao` em `serializers.py`) e o de operadora vem de `pipeline.OPERADORAS[importacao.operadora]["codigo_operadora"]`. Sem um dos dois disponíveis, cai pro nome genérico de sempre com o id (`f"{prefixo_fallback}_{importacao.id}"`), pra nunca gerar um nome vazio ou colidir entre duas importações.
- **`ImportacaoPlanoSaudeViewSet.gerar()`**: o `.zip` (2 tipos) passou a se chamar `"<nome_base>.zip"` em vez de `"importacao_plano_saude_<id>.zip"`; o `.csv` único (1 tipo) passou a se chamar `"<nome_base> - <tipo>.csv"` (ex.: `"1970 - 5060 - mensalidade.csv"`) em vez de só `"<tipo>.csv"` — mesma ambiguidade existia aí também (duas execuções da mesma empresa com operadoras diferentes e só 1 tipo cada geravam dois `mensalidade.csv` indistinguíveis). Os nomes dos arquivos **dentro** do `.zip` (`mensalidade.csv`/`coparticipacao.csv`) não mudaram, já estão desambiguados pelo nome do próprio zip.
- **`ImportacaoPlanoSaudeDePaulaViewSet.gerar()`** recebeu o mesmo ajuste (`prefixo_fallback="importacao_plano_saude_de_paula"`, preservando o fallback distinto de antes) — os dois `gerar()` compartilham exatamente o mesmo código de geração de arquivo, então ficaria inconsistente resolver só um dos dois.
- Sem migração, sem mudança de contrato de API — só o nome do arquivo no header `Content-Disposition` da resposta binária.

View File

@ -92,7 +92,7 @@ Essa divisão é calculada por `matcher._calcula_valores(valor_total, regra)` (c
`ImportacaoPlanoSaudeViewSet` (`/api/importacoes-plano-saude/`, `PermissaoApp("utilitarios", "importacao-plano-saude")` pra todos os métodos):
- `create()` (multipart, `ImportacaoPlanoSaudeCreateSerializer` valida a entrada) resolve a planilha padrão (upload **ou** busca no Questor — ver "Planilha padrão via Questor (SQL)" abaixo), salva o model + um `ImportacaoPlanoSaudeArquivoOperadora` por arquivo em `arquivo_operadora` (lista, ver "Múltiplos arquivos de operadora" abaixo) e roda `pipeline.processa_importacao()` **de forma síncrona** usando os caminhos de todos os arquivos da operadora + a lista de `LinhaSistema` já resolvida — sem fila/Celery, o arquivo típico processa em menos de um request. Se o processamento falhar (PDF num layout desconhecido etc.), apaga os arquivos recém-salvos (planilha + todos os da operadora) + o registro órfão e devolve 400.
- `GET /operadoras/` (`@action` sem detail) devolve `pipeline.lista_operadoras()` — fonte única pro combobox pesquisável "Operadora" do formulário (`#ips-operadora-combo`, mesmo padrão de "Regra de custeio salva" — ver "Regras de custeio salvas" abaixo), sem duplicar a lista em JS. `label` já vem no formato `"<código> - <Nome>"` (ex.: `"3755 - Itamed Saúde"`) — o código é o de cadastro da operadora no Questor, pedido explícito do usuário pra identificar a operadora sem ambiguidade (útil quando duas operadoras têm nome parecido); editar em `pipeline.OPERADORAS`, não formatar o código separadamente no frontend.
- `POST /{id}/gerar/` monta o(s) CSV(s) a partir das **linhas já salvas** (isto é, já com qualquer edição feita na revisão — não reprocessa os arquivos originais) usando `leiaute_sistema.CABECALHO`; 1 tipo de lançamento vira um `.csv` direto, 2 tipos (mensalidade + coparticipação) viram um `.zip` com um `.csv` por tipo (`zipfile` em memória). Sempre marca `status="concluida"` (+ `concluida_em`) — pode ser chamada de novo enquanto `concluida` (regera o mesmo arquivo a partir do que já está salvo), mas a partir daí toda edição de linha/auditoria/alteração fica bloqueada até reabrir (ver `reabrir()` abaixo e "Trava de edição pós-conclusão").
- `POST /{id}/gerar/` monta o(s) CSV(s) a partir das **linhas já salvas** (isto é, já com qualquer edição feita na revisão — não reprocessa os arquivos originais) usando `leiaute_sistema.CABECALHO`; 1 tipo de lançamento vira um `.csv` direto, 2 tipos (mensalidade + coparticipação) viram um `.zip` com um `.csv` por tipo (`zipfile` em memória). Sempre marca `status="concluida"` (+ `concluida_em`) — pode ser chamada de novo enquanto `concluida` (regera o mesmo arquivo a partir do que já está salvo), mas a partir daí toda edição de linha/auditoria/alteração fica bloqueada até reabrir (ver `reabrir()` abaixo e "Trava de edição pós-conclusão"). **Nome do arquivo baixado** (`_nome_base_arquivo_gerado_plano_saude()`, `views.py`, reaproveitada também por `ImportacaoPlanoSaudeDePaulaViewSet.gerar()`): `"<código empresa> - <código operadora>"` (ex.: `"1970 - 5060.zip"`, `"1970 - 5060 - mensalidade.csv"`) — pedido explícito do usuário, o nome genérico antigo (`importacao_plano_saude_<id>.zip`) não dava pra distinguir, já baixado, duas execuções da mesma empresa com operadoras diferentes. O código de empresa vem da primeira `ImportacaoPlanoSaudeLinha` não vazia (todas as linhas de uma importação compartilham o mesmo código, mesma premissa de `_codigo_empresa_da_importacao` em `serializers.py`) e o de operadora vem de `pipeline.OPERADORAS[importacao.operadora]["codigo_operadora"]`; sem um dos dois (ex.: nenhuma linha com código de empresa preenchido), cai pro nome genérico de sempre com o id (`importacao_plano_saude_<id>`/`importacao_plano_saude_de_paula_<id>`), pra nunca gerar um nome vazio ou colidir entre duas importações.
- `POST /{id}/reabrir/` volta `status="revisao"` (zera `concluida_em`) — contrapartida de `gerar()`, é o único jeito de voltar a editar uma importação concluída. Botão "Editar" na tela de Revisão, visível só quando `status === "concluida"`.
`ImportacaoPlanoSaudeLinhaViewSet` (`/api/importacoes-plano-saude-linhas/{id}/`, só GET/PATCH): edição de uma linha por vez, disparada por `blur`/`change` de cada `<input>` na tela de revisão — mesma permissão de toggle único, sem checagem de "dono". `create()`/`partial_update()`/`destroy()` recusam (400) se a importação já estiver `concluida` — ver "Trava de edição pós-conclusão" abaixo.

View File

@ -3,6 +3,7 @@ import csv
import dataclasses
import hashlib
import io
import itertools
import os
import re
import tempfile
@ -853,6 +854,23 @@ def _monta_csv_linhas_plano_saude(linhas: Iterable[ImportacaoPlanoSaudeLinha]) -
return buffer.getvalue().encode("utf-8-sig")
def _nome_base_arquivo_gerado_plano_saude(importacao: Any, todas_linhas: Iterable[Any], prefixo_fallback: str) -> str:
""""<código empresa> - <código operadora>" (ex.: "1970 - 5060") pra nomear
o(s) arquivo(s) que gerar() devolve — antes era sempre "<prefixo_fallback>_<id>",
que não dava pra distinguir duas execuções da mesma empresa com operadoras
diferentes. Todas as linhas de uma importação compartilham o mesmo código de
empresa (mesma premissa de _codigo_empresa_da_importacao em serializers.py),
então a primeira linha não vazia já identifica a empresa da execução inteira.
Cai pro nome genérico de sempre (com o id, pra continuar único) se a empresa
ou a operadora não puderem ser identificadas (ex.: nenhuma linha com código
de empresa preenchido)."""
codigo_empresa = next((l.codigo_empresa for l in todas_linhas if l.codigo_empresa), "")
codigo_operadora = planos_saude_pipeline.OPERADORAS.get(importacao.operadora, {}).get("codigo_operadora", "")
if codigo_empresa and codigo_operadora:
return f"{codigo_empresa} - {codigo_operadora}"
return f"{prefixo_fallback}_{importacao.id}"
# Campos editáveis de ImportacaoPlanoSaudeLinha (sem tipo_lancamento/ordem,
# guardados à parte em ImportacaoPlanoSaudeAlteracao) — usado tanto pra
# detectar qual campo mudou num PATCH (ImportacaoPlanoSaudeLinhaViewSet.perform_update)
@ -1207,6 +1225,9 @@ class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
linhas_por_tipo.setdefault(linha.tipo_lancamento, []).append(linha)
arquivos = {tipo: _monta_csv_linhas_plano_saude(linhas) for tipo, linhas in linhas_por_tipo.items()}
nome_base = _nome_base_arquivo_gerado_plano_saude(
importacao, itertools.chain.from_iterable(linhas_por_tipo.values()), "importacao_plano_saude"
)
importacao.status = ImportacaoPlanoSaude.STATUS_CONCLUIDA
importacao.concluida_em = timezone.now()
@ -1215,7 +1236,7 @@ class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
if len(arquivos) == 1:
(tipo, conteudo), = arquivos.items()
response = HttpResponse(conteudo, content_type="text/csv; charset=utf-8-sig")
response["Content-Disposition"] = f'attachment; filename="{tipo}.csv"'
response["Content-Disposition"] = f'attachment; filename="{nome_base} - {tipo}.csv"'
return response
buffer = io.BytesIO()
@ -1223,7 +1244,7 @@ class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
for tipo, conteudo in arquivos.items():
zf.writestr(f"{tipo}.csv", conteudo)
response = HttpResponse(buffer.getvalue(), content_type="application/zip")
response["Content-Disposition"] = f'attachment; filename="importacao_plano_saude_{importacao.id}.zip"'
response["Content-Disposition"] = f'attachment; filename="{nome_base}.zip"'
return response
@action(detail=True, methods=["post"])
@ -1824,6 +1845,9 @@ class ImportacaoPlanoSaudeDePaulaViewSet(viewsets.ModelViewSet):
linhas_por_tipo.setdefault(linha.tipo_lancamento, []).append(linha)
arquivos = {tipo: _monta_csv_linhas_plano_saude(linhas) for tipo, linhas in linhas_por_tipo.items()}
nome_base = _nome_base_arquivo_gerado_plano_saude(
importacao, itertools.chain.from_iterable(linhas_por_tipo.values()), "importacao_plano_saude_de_paula"
)
importacao.status = ImportacaoPlanoSaudeDePaula.STATUS_CONCLUIDA
importacao.concluida_em = timezone.now()
@ -1832,7 +1856,7 @@ class ImportacaoPlanoSaudeDePaulaViewSet(viewsets.ModelViewSet):
if len(arquivos) == 1:
(tipo, conteudo), = arquivos.items()
response = HttpResponse(conteudo, content_type="text/csv; charset=utf-8-sig")
response["Content-Disposition"] = f'attachment; filename="{tipo}.csv"'
response["Content-Disposition"] = f'attachment; filename="{nome_base} - {tipo}.csv"'
return response
buffer = io.BytesIO()
@ -1840,7 +1864,7 @@ class ImportacaoPlanoSaudeDePaulaViewSet(viewsets.ModelViewSet):
for tipo, conteudo in arquivos.items():
zf.writestr(f"{tipo}.csv", conteudo)
response = HttpResponse(buffer.getvalue(), content_type="application/zip")
response["Content-Disposition"] = f'attachment; filename="importacao_plano_saude_de_paula_{importacao.id}.zip"'
response["Content-Disposition"] = f'attachment; filename="{nome_base}.zip"'
return response
@action(detail=True, methods=["post"])