diff --git a/README.md b/README.md index a333d6f..78e280f 100644 --- a/README.md +++ b/README.md @@ -278,4 +278,4 @@ gunicorn main:app \ --- -uvicorn main:app --host 0.0.0.0 --port 8000 +uvicorn main:app --host 0.0.0.0 --port 8000 --reload diff --git a/actions/data/text.py b/actions/data/text.py index 5d57037..0816aa0 100644 --- a/actions/data/text.py +++ b/actions/data/text.py @@ -1,5 +1,4 @@ -# Importa a biblioteca nativa 'zlib' usada para descompressão de dados binários. -import base64 +# Importa a biblioteca nativa 'zlib' usada para compressão/descompressão de dados binários. import zlib # Importa a função 'rtf_to_text' da biblioteca 'striprtf', @@ -7,7 +6,7 @@ import zlib from striprtf.striprtf import rtf_to_text -# Define uma classe utilitária chamada 'String', contendo apenas métodos estáticos. +# Define uma classe utilitária chamada 'Text', contendo apenas métodos estáticos. # Essa abordagem permite o uso direto sem necessidade de instanciar a classe. class Text: @staticmethod @@ -23,71 +22,56 @@ class Text: - Representado como bytes puros """ # Verifica se o valor recebido é nulo, vazio ou None. - # Se for, retorna string vazia para evitar erros de processamento. if not vf_string: return "" # Caso seja um objeto tipo stream (ex: campo BLOB do Firebird) - # Campos BLOB geralmente possuem o método `.read()` para leitura de bytes. if hasattr(vf_string, "read"): - vf_string = vf_string.read() # Lê o conteúdo completo do stream + vf_string = vf_string.read() - # Garante que o valor trabalhado é uma sequência de bytes (não string) - # Se o dado já for texto (str), converte para bytes em codificação Latin-1, - # que é compatível com ISO-8859-1 usado por sistemas Delphi/Firebird. + # Garante que o valor trabalhado é uma sequência de bytes if isinstance(vf_string, str): vf_bytes = vf_string.encode("latin1", errors="ignore") else: - vf_bytes = vf_string # Já está em bytes, então apenas reaproveita + vf_bytes = vf_string - # Detecta se o conteúdo foi compactado com zlib. - # A assinatura padrão do formato zlib começa com bytes: 0x78 0x9C ou 0x78 0xDA. + # Detecta assinatura zlib (0x78 0x9C ou 0x78 0xDA) is_zlib = ( len(vf_bytes) > 2 and vf_bytes[0] == 0x78 and vf_bytes[1] in (0x9C, 0xDA) ) - # Se a detecção confirmar que o conteúdo é zlib, tenta descompactar. + # Se for zlib, tenta descompactar if is_zlib: try: - # Descompacta os bytes e decodifica o texto usando ISO-8859-1 (ANSI), - # que preserva corretamente acentuação e caracteres especiais. - text = zlib.decompress(vf_bytes).decode("iso-8859-1", errors="ignore") - return text + return zlib.decompress(vf_bytes).decode("iso-8859-1", errors="ignore") except Exception: - # Caso falhe (por dados corrompidos ou não comprimidos de fato), - # o fluxo continua normalmente sem interromper a execução. pass - # Se não for zlib, tenta tratar o conteúdo como texto puro (não compactado) + # Caso não seja zlib, trata como texto puro try: - # Decodifica os bytes diretamente de ISO-8859-1 (padrão usado pelo Delphi) return vf_bytes.decode("iso-8859-1", errors="ignore") except Exception: - # Como fallback, converte para string bruta para evitar falhas. return str(vf_string) - # >>> NOVO MÉTODO <<< @staticmethod - def compress(text, *, encoding: str = "iso-8859-1", as_base64: bool = True): + def compress(text, *, encoding: str = "iso-8859-1"): """ - Comprime texto/dados com zlib. + Comprime texto/dados com zlib SEM Base64. Parâmetros: text: str | bytes | stream (com .read()) encoding: encoding usado quando 'text' for str (padrão: ISO-8859-1) - as_base64: se True, retorna string Base64 do conteúdo comprimido; - caso False, retorna bytes comprimidos. Retorno: - - bytes (zlib) quando as_base64=False - - str (Base64) quando as_base64=True + - bytes comprimidos (zlib) Observações: - - Use o mesmo 'encoding' ao descomprimir para simetria. - - Ideal para armazenar em BLOB ou trafegar seguro (Base64). + - Ideal para armazenamento direto em BLOB (Firebird, PostgreSQL, etc.) + - Evita overhead e custo do Base64 + - Totalmente compatível com 'decompress' """ if text is None or text == "": - return "" if as_base64 else b"" + return b"" # Se for stream (ex.: BLOB do Firebird) if hasattr(text, "read"): @@ -101,14 +85,8 @@ class Text: else: raw_bytes = bytes(raw) - # Comprime com zlib - comp = zlib.compress(raw_bytes) - - # Opcional: codifica em Base64 para transporte/JSON - if as_base64: - return base64.b64encode(comp).decode("ascii") - - return comp + # Comprime com zlib e retorna bytes + return zlib.compress(raw_bytes) @staticmethod def to_text(raw_text: str) -> str: @@ -117,25 +95,17 @@ class Text: Finalidade: - Detectar automaticamente se o conteúdo está em formato RTF. - - Converter para texto plano usando a função 'rtf_to_text' (da striprtf). - - Retornar uma string limpa e pronta para ser usada em APIs, logs, etc. + - Converter para texto plano usando a função 'rtf_to_text'. + - Retornar uma string limpa e pronta para uso. """ - # Verifica se o texto recebido está vazio ou None — retorna vazio se sim. if not raw_text: return "" - # Verifica se o texto começa com o cabeçalho padrão de arquivos RTF. - # Exemplo: "{\\rtf1\\ansi..." indica conteúdo em formato RTF. + # Detecta cabeçalho RTF if raw_text.strip().startswith("{\\rtf"): try: - # Converte o RTF em texto simples, preservando acentuação e quebras de linha. - text = rtf_to_text(raw_text) - # Remove espaços em branco extras nas extremidades. - return text.strip() + return rtf_to_text(raw_text).strip() except Exception: - # Se ocorrer erro na conversão (ex: RTF inválido), - # retorna o conteúdo original sem alterações. return raw_text - # Caso o texto não seja RTF, apenas remove espaços em branco extras e retorna. return raw_text.strip() diff --git a/actions/jwt/create_token.py b/actions/jwt/create_token.py index caca25d..5f1bb11 100644 --- a/actions/jwt/create_token.py +++ b/actions/jwt/create_token.py @@ -5,32 +5,30 @@ from pytz import timezone from abstracts.action import BaseAction from actions.config.config import Config + class CreateToken(BaseAction): def __init__(self): + # Busca as configurações da aplicação - self.config = Config.get('app.json') + self.config = Config.get("app.json") # Cria o timedelta com base na config self.access_token_expire = timedelta( minutes=self.config.jwt.expire.minute, hours=self.config.jwt.expire.hours, - days=self.config.jwt.expire.days + days=self.config.jwt.expire.days, ) - def execute(self, tipo_token: str, data : str) -> str: - - sp = timezone('America/Sao_Paulo') + def execute(self, tipo_token: str, data: str) -> str: + + sp = timezone("America/Sao_Paulo") agora = datetime.now(tz=sp) expira = agora + self.access_token_expire # Define os dados do token - payload = { - 'type' : tipo_token, - 'exp' : expira, - 'iat' : agora, - 'data' : str(data) - } + payload = {"type": tipo_token, "exp": expira, "iat": agora, "data": str(data)} # Retorna os dados codificados - return jwt.encode(payload, self.config.jwt.token, algorithm=self.config.jwt.algorithm) - \ No newline at end of file + return jwt.encode( + payload, self.config.jwt.token, algorithm=self.config.jwt.algorithm + ) diff --git a/packages/v1/servicos/balcao/actions/t_servico_itempedido/t_servico_itempedido_certidao_save_action.py b/packages/v1/servicos/balcao/actions/t_servico_itempedido/t_servico_itempedido_certidao_save_action.py new file mode 100644 index 0000000..056814a --- /dev/null +++ b/packages/v1/servicos/balcao/actions/t_servico_itempedido/t_servico_itempedido_certidao_save_action.py @@ -0,0 +1,40 @@ +from abstracts.action import BaseAction +from packages.v1.servicos.balcao.repositories.t_servico_itempedido.t_servico_itempedido_certidao_save_repository import ( + TServicoItemPedidoCertidaoSaveRepository, +) +from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import ( + TServicoItemPedidoCertidaoSaveSchema, +) + + +class TServicoItemPedidoCertidaSaveAction(BaseAction): + """ + Serviço responsável por encapsular a lógica de negócio para a operação + de salvamento de um novo registro na tabela T_SERVICO_ITEMPEDIDO. + """ + + def execute(self, data: TServicoItemPedidoCertidaoSaveSchema): + """ + Executa a operação de salvamento. + + Args: + t_servico_itempedido_schema (TServicoItemPedidoSchema): + O esquema com os dados a serem persistidos. + + Returns: + O resultado da operação de salvamento. + """ + # ---------------------------------------------------- + # Instanciamento do repositório + # ---------------------------------------------------- + save_repository = TServicoItemPedidoCertidaoSaveRepository() + + # ---------------------------------------------------- + # Execução do repositório + # ---------------------------------------------------- + response = save_repository.execute(data) + + # ---------------------------------------------------- + # Retorno da informação + # ---------------------------------------------------- + return response diff --git a/packages/v1/servicos/balcao/actions/t_servico_itempedido/t_servico_itempedido_create_certidao_action.py b/packages/v1/servicos/balcao/actions/t_servico_itempedido/t_servico_itempedido_create_certidao_action.py index 5f3050f..4544cbf 100644 --- a/packages/v1/servicos/balcao/actions/t_servico_itempedido/t_servico_itempedido_create_certidao_action.py +++ b/packages/v1/servicos/balcao/actions/t_servico_itempedido/t_servico_itempedido_create_certidao_action.py @@ -1,5 +1,6 @@ import os +from docx import Document from fastapi import HTTPException, status from abstracts.action import BaseAction from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import ( @@ -18,8 +19,13 @@ class TServicoItemPedidoCreateCertidaoAction(BaseAction): def execute(self, data: TServicoItemPedidoCreateCertidaoSchema): - # 1. Decodifica o texto - texto = Text.decompress(data.certidao_texto) + texto = None + + # Verifica se existe conteudo a ser escrito + if data.certidao_texto: + + # 1. Decodifica o texto + texto = Text.decompress(data.certidao_texto) # 2. Configuração do caminho e nome diretorio = "./storage/temp/" @@ -31,9 +37,18 @@ class TServicoItemPedidoCreateCertidaoAction(BaseAction): # 3. Garante que a pasta existe os.makedirs(diretorio, exist_ok=True) - # 4. Escreve o texto em disco - with open(caminho_completo, "wb") as f: - f.write(texto.encode("utf-8")) + if texto: + # 4. Escreve o texto em disco + with open(caminho_completo, "wb") as f: + f.write(texto.encode("utf-8")) + + else: + + # 1. Instancia o objeto (cria a estrutura XML em memória) + document = Document() + + # 2. Salva o arquivo fisicamente + document.save(caminho_completo) # 5. VALIDAÇÃO: Verifica se o arquivo existe e se tem conteúdo if not os.path.exists(caminho_completo): diff --git a/packages/v1/servicos/balcao/controllers/t_servico_itempedido_controller.py b/packages/v1/servicos/balcao/controllers/t_servico_itempedido_controller.py index cb3d8e7..3c2ad60 100644 --- a/packages/v1/servicos/balcao/controllers/t_servico_itempedido_controller.py +++ b/packages/v1/servicos/balcao/controllers/t_servico_itempedido_controller.py @@ -1,6 +1,7 @@ from actions.dynamic_import.dynamic_import import DynamicImport from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import ( TServicoItemIndexSchema, + TServicoItemPedidoCertidaoSaveSchema, TServicoItemPedidoSaveSchema, TServicoItemPedidoSituacaoSchema, TServicoItemPedidoUpdateSchema, @@ -99,6 +100,23 @@ class TServicoItemPedidoController: "data": self.show_service.execute(t_servico_itempedido_id_schema), } + # ---------------------------------------------------- + # Busca um registro específico de T_SERVICO_ITEMPEDIDO pelo ID + # ---------------------------------------------------- + def certidao_save(self, data: TServicoItemPedidoCertidaoSaveSchema): + + # Importação da classe desejada + save_service = self.dynamic_import.service( + "t_servico_itempedido_certidao_save_service", + "TServicoItemPedidoCertidaoSaveService", + ) + + # Instância da classe service + self.save_service = save_service() + + # Execução da busca + return self.save_service.execute(data) + # ---------------------------------------------------- # Cadastra um novo registro em T_SERVICO_ITEMPEDIDO # ---------------------------------------------------- diff --git a/packages/v1/servicos/balcao/endpoints/t_servico_itempedido_endpoint.py b/packages/v1/servicos/balcao/endpoints/t_servico_itempedido_endpoint.py index f815e24..176a958 100644 --- a/packages/v1/servicos/balcao/endpoints/t_servico_itempedido_endpoint.py +++ b/packages/v1/servicos/balcao/endpoints/t_servico_itempedido_endpoint.py @@ -1,5 +1,5 @@ # Importação de bibliotecas -from fastapi import APIRouter, Depends, status +from fastapi import APIRouter, Depends, status, Request from actions.data.dict_to_namespace import dict_to_namespace from actions.jwt.get_current_user import get_current_user from packages.v1.servicos.balcao.controllers.t_servico_itempedido_controller import ( @@ -7,6 +7,7 @@ from packages.v1.servicos.balcao.controllers.t_servico_itempedido_controller imp ) from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import ( TServicoItemIndexSchema, + TServicoItemPedidoCertidaoSaveSchema, TServicoItemPedidoSaveSchema, TServicoItemPedidoSituacaoSchema, TServicoItemPedidoUpdateSchema, @@ -210,6 +211,30 @@ async def ativar( return response +# ---------------------------------------------------- +# Gera o arquivo em disco para Manipulação +# ---------------------------------------------------- +@router.post( + "/{servico_itempedido_id}/certidao/salvar", + status_code=status.HTTP_200_OK, + summary="Atualiza o texto da certidão", + response_description="Atualiza o texto da certidão", +) +async def certidao_save(servico_itempedido_id: int, request: Request): + + # Obtem os dados enviados + data = await request.json() + + response = t_servico_itempedido_controller.certidao_save( + TServicoItemPedidoCertidaoSaveSchema( + servico_itempedido_id=servico_itempedido_id, data=data + ) + ) + + # return response + return response + + # ---------------------------------------------------- # Exclui um registro de T_SERVICO_ITEMPEDIDO # ---------------------------------------------------- diff --git a/packages/v1/servicos/balcao/repositories/t_ato/t_ato_index_repository.py b/packages/v1/servicos/balcao/repositories/t_ato/t_ato_index_repository.py index 5e1e394..95c20c0 100644 --- a/packages/v1/servicos/balcao/repositories/t_ato/t_ato_index_repository.py +++ b/packages/v1/servicos/balcao/repositories/t_ato/t_ato_index_repository.py @@ -18,16 +18,19 @@ class TAtoIndexRepository(BaseRepository): # Montagem do SQL sql = """ SELECT TA.ATO_ID, - TA.PROTOCOLO , + TA.PROTOCOLO, + TAT.DESCRICAO, TA.DATA_LAVRATURA, + TA.DATA_ABERTURA, TLA.NUMERO_LIVRO, - TA.FOLHA_INICIAL , + TA.FOLHA_INICIAL, TA.FOLHA_FINAL FROM T_ATO TA JOIN T_LIVRO_ANDAMENTO tla ON TA.LIVRO_ANDAMENTO_ID = TLA.LIVRO_ANDAMENTO_ID + JOIN T_ATO_TIPO TAT ON TA.ATO_TIPO_ID = TAT.ATO_TIPO_ID ORDER BY - TA.ATO_ID ASC """ + TA.ATO_ID DESC """ # Execução do sql response = self.fetch_all(sql) diff --git a/packages/v1/servicos/balcao/repositories/t_servico_itempedido/t_servico_itempedido_certidao_save_repository.py b/packages/v1/servicos/balcao/repositories/t_servico_itempedido/t_servico_itempedido_certidao_save_repository.py new file mode 100644 index 0000000..02d48a5 --- /dev/null +++ b/packages/v1/servicos/balcao/repositories/t_servico_itempedido/t_servico_itempedido_certidao_save_repository.py @@ -0,0 +1,51 @@ +from fastapi import HTTPException, status +from abstracts.repository import BaseRepository +from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import ( + TServicoItemPedidoCertidaoSaveSchema, +) + + +class TServicoItemPedidoCertidaoSaveRepository(BaseRepository): + """ + Repositório responsável pela operação de salvamento de um novo registro + na tabela T_SERVICO_ITEMPEDIDO. + """ + + def execute(self, data: TServicoItemPedidoCertidaoSaveSchema): + """ + Executa a operação de salvamento no banco de dados. + + Args: + t_servico_itempedido_save_schema (TServicoItemPedidoSchema): O esquema com os dados a serem salvos. + + Returns: + O registro recém-criado. + + Raises: + HTTPException: Caso ocorra um erro na execução da query. + """ + try: + + # ---------------------------------------------------- + # Preenchimento dos parâmetros + # ---------------------------------------------------- + params = data.model_dump(exclude_unset=True) + + # ---------------------------------------------------- + # Montagem do SQL dinâmico + # ---------------------------------------------------- + sql = "UPDATE T_SERVICO_ITEMPEDIDO TSI SET TSI.CERTIDAO_TEXTO = :certidao_texto WHERE TSI.SERVICO_ITEMPEDIDO_ID = :servico_itempedido_id RETURNING *" + + # ---------------------------------------------------- + # Execução do SQL e retorno do registro + # ---------------------------------------------------- + return self.run_and_return(sql, params) + + except Exception as e: + # ---------------------------------------------------- + # Tratamento de erros e lançamento de exceção HTTP + # ---------------------------------------------------- + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Erro ao salvar registro em T_SERVICO_ITEMPEDIDO: {e}", + ) diff --git a/packages/v1/servicos/balcao/schemas/t_servico_itempedido_schema.py b/packages/v1/servicos/balcao/schemas/t_servico_itempedido_schema.py index c7e33fd..dc56d6a 100644 --- a/packages/v1/servicos/balcao/schemas/t_servico_itempedido_schema.py +++ b/packages/v1/servicos/balcao/schemas/t_servico_itempedido_schema.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Optional +from typing import Any, Optional from decimal import Decimal from datetime import datetime @@ -109,6 +109,16 @@ class TServicoItemPedidoSaveSituacaoSchema(BaseModel): from_attributes = True +class TServicoItemPedidoCertidaoSaveSchema(BaseModel): + + servico_itempedido_id: int = None + data: object + certidao_texto: Optional[bytes] = None + + class Config: + from_attributes = True + + class TServicoItemPedidoSaveSchema(TServicoItemPedidoSchema): class Config: @@ -140,7 +150,7 @@ class TServicoItemPedidoSaveSchema_(TServicoItemPedidoSchema): class TServicoItemPedidoCreateCertidaoSchema(BaseModel): servico_itempedido_id: int - certidao_texto: bytes + certidao_texto: Optional[Any] = None class Config: from_attributes = True diff --git a/packages/v1/servicos/balcao/services/t_servico_itempedido/go/t_servico_itempedido_certidao_save_service.py b/packages/v1/servicos/balcao/services/t_servico_itempedido/go/t_servico_itempedido_certidao_save_service.py new file mode 100644 index 0000000..e7e2147 --- /dev/null +++ b/packages/v1/servicos/balcao/services/t_servico_itempedido/go/t_servico_itempedido_certidao_save_service.py @@ -0,0 +1,86 @@ +from pathlib import Path +import tempfile +import os +import requests +from fastapi import HTTPException, status +from actions.data.text import Text +from packages.v1.servicos.balcao.actions.t_servico_itempedido.t_servico_itempedido_certidao_save_action import ( + TServicoItemPedidoCertidaSaveAction, +) +from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import ( + TServicoItemPedidoCertidaoSaveSchema, +) + + +class TServicoItemPedidoCertidaoSaveService: + + def execute(self, data: TServicoItemPedidoCertidaoSaveSchema): + + # ---------------------------------------------------- + # 1. Apenas status FINAL COM SAVE + # ---------------------------------------------------- + if data.data.get("status") != 2: + return {"error": 0} + + file_url = data.data.get("url") + if not file_url: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="URL do arquivo não informada.", + ) + + # ---------------------------------------------------- + # 2. Download binário + # ---------------------------------------------------- + response = requests.get(file_url, timeout=30) + response.raise_for_status() + + arquivo_bytes = response.content + + if not arquivo_bytes: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Arquivo retornado está vazio.", + ) + + # ---------------------------------------------------- + # 3. Validação RTF mínima + # ---------------------------------------------------- + if not arquivo_bytes.lstrip().startswith(b"{\\rtf"): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Conteúdo retornado não é um RTF válido.", + ) + + # ---------------------------------------------------- + # 4. Persistência no banco + # ---------------------------------------------------- + data.certidao_texto = Text.compress(arquivo_bytes) + + save_action = TServicoItemPedidoCertidaSaveAction() + + if not save_action.execute(data): + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Erro ao salvar certidão no banco.", + ) + + # ---------------------------------------------------- + # 5. Escrita em disco (atomic) + # ---------------------------------------------------- + destino = Path("./storage/temp") + destino.mkdir(parents=True, exist_ok=True) + + arquivo_final = destino / data.data["key"] + + with tempfile.NamedTemporaryFile(delete=False, dir=destino) as tmp: + tmp.write(arquivo_bytes) + tmp.flush() + temp_name = tmp.name + + os.replace(temp_name, arquivo_final) + + # ---------------------------------------------------- + # 6. Retorno esperado pelo OnlyOffice + # ---------------------------------------------------- + return {"error": 0} diff --git a/packages/v1/servicos/balcao/services/t_servico_itempedido/go/t_servico_itempedido_index_service.py b/packages/v1/servicos/balcao/services/t_servico_itempedido/go/t_servico_itempedido_index_service.py index 6facf3e..7eab310 100644 --- a/packages/v1/servicos/balcao/services/t_servico_itempedido/go/t_servico_itempedido_index_service.py +++ b/packages/v1/servicos/balcao/services/t_servico_itempedido/go/t_servico_itempedido_index_service.py @@ -1,5 +1,6 @@ from types import SimpleNamespace +from docx import Document from fastapi import HTTPException, status from packages.v1.servicos.balcao.actions.t_servico_itempedido.t_servico_itempedido_create_certidao_action import ( TServicoItemPedidoCreateCertidaoAction, @@ -92,6 +93,16 @@ class TServicoItemPedidoIndexService: ) ) + else: + + # Cria o documento editavel em disco + item.certidao_texto = create_certidao_action.execute( + TServicoItemPedidoCreateCertidaoSchema( + servico_itempedido_id=item.servico_itempedido_id, + certidao_texto=None, + ) + ) + # Adiciona o objeto processado na lista final data.append(item)