feat(OnlyOffice): Ajustes diversos no editor de texto

This commit is contained in:
Keven 2025-12-26 15:51:31 -03:00
parent 6ccf74e687
commit 1e34ce9e9a
12 changed files with 304 additions and 77 deletions

View file

@ -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

View file

@ -1,5 +1,4 @@
# Importa a biblioteca nativa 'zlib' usada para descompressão de dados binários. # Importa a biblioteca nativa 'zlib' usada para compressão/descompressão de dados binários.
import base64
import zlib import zlib
# Importa a função 'rtf_to_text' da biblioteca 'striprtf', # Importa a função 'rtf_to_text' da biblioteca 'striprtf',
@ -7,7 +6,7 @@ import zlib
from striprtf.striprtf import rtf_to_text 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. # Essa abordagem permite o uso direto sem necessidade de instanciar a classe.
class Text: class Text:
@staticmethod @staticmethod
@ -23,71 +22,56 @@ class Text:
- Representado como bytes puros - Representado como bytes puros
""" """
# Verifica se o valor recebido é nulo, vazio ou None. # Verifica se o valor recebido é nulo, vazio ou None.
# Se for, retorna string vazia para evitar erros de processamento.
if not vf_string: if not vf_string:
return "" return ""
# Caso seja um objeto tipo stream (ex: campo BLOB do Firebird) # 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"): 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) # Garante que o valor trabalhado é uma sequência de bytes
# 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.
if isinstance(vf_string, str): if isinstance(vf_string, str):
vf_bytes = vf_string.encode("latin1", errors="ignore") vf_bytes = vf_string.encode("latin1", errors="ignore")
else: 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. # Detecta assinatura zlib (0x78 0x9C ou 0x78 0xDA)
# A assinatura padrão do formato zlib começa com bytes: 0x78 0x9C ou 0x78 0xDA.
is_zlib = ( is_zlib = (
len(vf_bytes) > 2 and vf_bytes[0] == 0x78 and vf_bytes[1] in (0x9C, 0xDA) 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: if is_zlib:
try: try:
# Descompacta os bytes e decodifica o texto usando ISO-8859-1 (ANSI), return zlib.decompress(vf_bytes).decode("iso-8859-1", errors="ignore")
# que preserva corretamente acentuação e caracteres especiais.
text = zlib.decompress(vf_bytes).decode("iso-8859-1", errors="ignore")
return text
except Exception: except Exception:
# Caso falhe (por dados corrompidos ou não comprimidos de fato),
# o fluxo continua normalmente sem interromper a execução.
pass 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: try:
# Decodifica os bytes diretamente de ISO-8859-1 (padrão usado pelo Delphi)
return vf_bytes.decode("iso-8859-1", errors="ignore") return vf_bytes.decode("iso-8859-1", errors="ignore")
except Exception: except Exception:
# Como fallback, converte para string bruta para evitar falhas.
return str(vf_string) return str(vf_string)
# >>> NOVO MÉTODO <<<
@staticmethod @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: Parâmetros:
text: str | bytes | stream (com .read()) text: str | bytes | stream (com .read())
encoding: encoding usado quando 'text' for str (padrão: ISO-8859-1) 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: Retorno:
- bytes (zlib) quando as_base64=False - bytes comprimidos (zlib)
- str (Base64) quando as_base64=True
Observações: Observações:
- Use o mesmo 'encoding' ao descomprimir para simetria. - Ideal para armazenamento direto em BLOB (Firebird, PostgreSQL, etc.)
- Ideal para armazenar em BLOB ou trafegar seguro (Base64). - Evita overhead e custo do Base64
- Totalmente compatível com 'decompress'
""" """
if text is None or text == "": if text is None or text == "":
return "" if as_base64 else b"" return b""
# Se for stream (ex.: BLOB do Firebird) # Se for stream (ex.: BLOB do Firebird)
if hasattr(text, "read"): if hasattr(text, "read"):
@ -101,14 +85,8 @@ class Text:
else: else:
raw_bytes = bytes(raw) raw_bytes = bytes(raw)
# Comprime com zlib # Comprime com zlib e retorna bytes
comp = zlib.compress(raw_bytes) return zlib.compress(raw_bytes)
# Opcional: codifica em Base64 para transporte/JSON
if as_base64:
return base64.b64encode(comp).decode("ascii")
return comp
@staticmethod @staticmethod
def to_text(raw_text: str) -> str: def to_text(raw_text: str) -> str:
@ -117,25 +95,17 @@ class Text:
Finalidade: Finalidade:
- Detectar automaticamente se o conteúdo está em formato RTF. - Detectar automaticamente se o conteúdo está em formato RTF.
- Converter para texto plano usando a função 'rtf_to_text' (da striprtf). - Converter para texto plano usando a função 'rtf_to_text'.
- Retornar uma string limpa e pronta para ser usada em APIs, logs, etc. - 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: if not raw_text:
return "" return ""
# Verifica se o texto começa com o cabeçalho padrão de arquivos RTF. # Detecta cabeçalho RTF
# Exemplo: "{\\rtf1\\ansi..." indica conteúdo em formato RTF.
if raw_text.strip().startswith("{\\rtf"): if raw_text.strip().startswith("{\\rtf"):
try: try:
# Converte o RTF em texto simples, preservando acentuação e quebras de linha. return rtf_to_text(raw_text).strip()
text = rtf_to_text(raw_text)
# Remove espaços em branco extras nas extremidades.
return text.strip()
except Exception: except Exception:
# Se ocorrer erro na conversão (ex: RTF inválido),
# retorna o conteúdo original sem alterações.
return raw_text return raw_text
# Caso o texto não seja RTF, apenas remove espaços em branco extras e retorna.
return raw_text.strip() return raw_text.strip()

View file

@ -5,32 +5,30 @@ from pytz import timezone
from abstracts.action import BaseAction from abstracts.action import BaseAction
from actions.config.config import Config from actions.config.config import Config
class CreateToken(BaseAction): class CreateToken(BaseAction):
def __init__(self): def __init__(self):
# Busca as configurações da aplicação # 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 # Cria o timedelta com base na config
self.access_token_expire = timedelta( self.access_token_expire = timedelta(
minutes=self.config.jwt.expire.minute, minutes=self.config.jwt.expire.minute,
hours=self.config.jwt.expire.hours, 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: def execute(self, tipo_token: str, data: str) -> str:
sp = timezone('America/Sao_Paulo') sp = timezone("America/Sao_Paulo")
agora = datetime.now(tz=sp) agora = datetime.now(tz=sp)
expira = agora + self.access_token_expire expira = agora + self.access_token_expire
# Define os dados do token # Define os dados do token
payload = { payload = {"type": tipo_token, "exp": expira, "iat": agora, "data": str(data)}
'type' : tipo_token,
'exp' : expira,
'iat' : agora,
'data' : str(data)
}
# Retorna os dados codificados # Retorna os dados codificados
return jwt.encode(payload, self.config.jwt.token, algorithm=self.config.jwt.algorithm) return jwt.encode(
payload, self.config.jwt.token, algorithm=self.config.jwt.algorithm
)

View file

@ -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

View file

@ -1,5 +1,6 @@
import os import os
from docx import Document
from fastapi import HTTPException, status from fastapi import HTTPException, status
from abstracts.action import BaseAction from abstracts.action import BaseAction
from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import ( from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import (
@ -18,8 +19,13 @@ class TServicoItemPedidoCreateCertidaoAction(BaseAction):
def execute(self, data: TServicoItemPedidoCreateCertidaoSchema): def execute(self, data: TServicoItemPedidoCreateCertidaoSchema):
# 1. Decodifica o texto texto = None
texto = Text.decompress(data.certidao_texto)
# 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 # 2. Configuração do caminho e nome
diretorio = "./storage/temp/" diretorio = "./storage/temp/"
@ -31,9 +37,18 @@ class TServicoItemPedidoCreateCertidaoAction(BaseAction):
# 3. Garante que a pasta existe # 3. Garante que a pasta existe
os.makedirs(diretorio, exist_ok=True) os.makedirs(diretorio, exist_ok=True)
# 4. Escreve o texto em disco if texto:
with open(caminho_completo, "wb") as f: # 4. Escreve o texto em disco
f.write(texto.encode("utf-8")) 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 # 5. VALIDAÇÃO: Verifica se o arquivo existe e se tem conteúdo
if not os.path.exists(caminho_completo): if not os.path.exists(caminho_completo):

View file

@ -1,6 +1,7 @@
from actions.dynamic_import.dynamic_import import DynamicImport from actions.dynamic_import.dynamic_import import DynamicImport
from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import ( from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import (
TServicoItemIndexSchema, TServicoItemIndexSchema,
TServicoItemPedidoCertidaoSaveSchema,
TServicoItemPedidoSaveSchema, TServicoItemPedidoSaveSchema,
TServicoItemPedidoSituacaoSchema, TServicoItemPedidoSituacaoSchema,
TServicoItemPedidoUpdateSchema, TServicoItemPedidoUpdateSchema,
@ -99,6 +100,23 @@ class TServicoItemPedidoController:
"data": self.show_service.execute(t_servico_itempedido_id_schema), "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 # Cadastra um novo registro em T_SERVICO_ITEMPEDIDO
# ---------------------------------------------------- # ----------------------------------------------------

View file

@ -1,5 +1,5 @@
# Importação de bibliotecas # 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.data.dict_to_namespace import dict_to_namespace
from actions.jwt.get_current_user import get_current_user from actions.jwt.get_current_user import get_current_user
from packages.v1.servicos.balcao.controllers.t_servico_itempedido_controller import ( 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 ( from packages.v1.servicos.balcao.schemas.t_servico_itempedido_schema import (
TServicoItemIndexSchema, TServicoItemIndexSchema,
TServicoItemPedidoCertidaoSaveSchema,
TServicoItemPedidoSaveSchema, TServicoItemPedidoSaveSchema,
TServicoItemPedidoSituacaoSchema, TServicoItemPedidoSituacaoSchema,
TServicoItemPedidoUpdateSchema, TServicoItemPedidoUpdateSchema,
@ -210,6 +211,30 @@ async def ativar(
return response 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 # Exclui um registro de T_SERVICO_ITEMPEDIDO
# ---------------------------------------------------- # ----------------------------------------------------

View file

@ -18,16 +18,19 @@ class TAtoIndexRepository(BaseRepository):
# Montagem do SQL # Montagem do SQL
sql = """ SELECT sql = """ SELECT
TA.ATO_ID, TA.ATO_ID,
TA.PROTOCOLO , TA.PROTOCOLO,
TAT.DESCRICAO,
TA.DATA_LAVRATURA, TA.DATA_LAVRATURA,
TA.DATA_ABERTURA,
TLA.NUMERO_LIVRO, TLA.NUMERO_LIVRO,
TA.FOLHA_INICIAL , TA.FOLHA_INICIAL,
TA.FOLHA_FINAL TA.FOLHA_FINAL
FROM FROM
T_ATO TA T_ATO TA
JOIN T_LIVRO_ANDAMENTO tla ON TA.LIVRO_ANDAMENTO_ID = TLA.LIVRO_ANDAMENTO_ID 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 ORDER BY
TA.ATO_ID ASC """ TA.ATO_ID DESC """
# Execução do sql # Execução do sql
response = self.fetch_all(sql) response = self.fetch_all(sql)

View file

@ -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}",
)

View file

@ -1,5 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Any, Optional
from decimal import Decimal from decimal import Decimal
from datetime import datetime from datetime import datetime
@ -109,6 +109,16 @@ class TServicoItemPedidoSaveSituacaoSchema(BaseModel):
from_attributes = True 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 TServicoItemPedidoSaveSchema(TServicoItemPedidoSchema):
class Config: class Config:
@ -140,7 +150,7 @@ class TServicoItemPedidoSaveSchema_(TServicoItemPedidoSchema):
class TServicoItemPedidoCreateCertidaoSchema(BaseModel): class TServicoItemPedidoCreateCertidaoSchema(BaseModel):
servico_itempedido_id: int servico_itempedido_id: int
certidao_texto: bytes certidao_texto: Optional[Any] = None
class Config: class Config:
from_attributes = True from_attributes = True

View file

@ -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}

View file

@ -1,5 +1,6 @@
from types import SimpleNamespace from types import SimpleNamespace
from docx import Document
from fastapi import HTTPException, status from fastapi import HTTPException, status
from packages.v1.servicos.balcao.actions.t_servico_itempedido.t_servico_itempedido_create_certidao_action import ( from packages.v1.servicos.balcao.actions.t_servico_itempedido.t_servico_itempedido_create_certidao_action import (
TServicoItemPedidoCreateCertidaoAction, 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 # Adiciona o objeto processado na lista final
data.append(item) data.append(item)