Versão 1.0

This commit is contained in:
Ricardo 2026-09-14 10:13:17 -03:00
commit 13bcea2077
26 changed files with 1546 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.env
.venv/
__pycache__/
*.pyc
*.pyo
*.log

25
main.py Normal file
View file

@ -0,0 +1,25 @@
import logging
import time
from dotenv import load_dotenv
from src.config.logging_config import setup_logging
from src.database.connection import db_instance
from src.services.fat_estoque import fat_estoque
INTERVALO_MINUTOS = 5
load_dotenv()
setup_logging()
logging.info("Aplicacao iniciada - rodando a cada %d minutos", INTERVALO_MINUTOS)
while True:
try:
for conn in db_instance.get_db():
fat_estoque(conn)
except Exception as e:
logging.critical("Erro fatal: %s", e, exc_info=True)
logging.info("Aguardando %d minutos para proxima execucao", INTERVALO_MINUTOS)
time.sleep(INTERVALO_MINUTOS * 60)

2
pyproject.toml Normal file
View file

@ -0,0 +1,2 @@
[tool.pyright]
typeCheckingMode = "basic"

BIN
requirements.txt Normal file

Binary file not shown.

0
src/__init__.py Normal file
View file

60
src/config/README.md Normal file
View file

@ -0,0 +1,60 @@
# Config
Configuracoes e logging do projeto.
## Arquivos
- `settings.py` - Reservado para centralizacao de configuracoes futuras
- `logging_config.py` - Configuracao do sistema de logging com rotacao diaria
## Variaveis de ambiente
Todas as variaveis sao carregadas do arquivo `.env` na raiz do projeto via `load_dotenv()` no `main.py`. Cada modulo acessa as variaveis que precisa via `os.getenv()`.
## Logging
O modulo `logging_config.py` configura o logging da aplicacao inteira com:
- **Rotacao diaria**: a meia-noite o arquivo `fat_estoque.log` e renomeado para `fat_estoque-YYYY-MM-DD.log`
- **Cleanup automatico**: logs com mais de 21 dias sao removidos
- **Formato**: `LEVEL|nome|timestamp|mensagem|arquivo|linha`
Exemplo de linha de log:
```
INFO|root|2026-03-23 14:30:00|Aplicacao iniciada|main.py|10
```
## Senha do banco (DB_PASS)
A senha do banco nao fica em texto puro no `.env`. Ela e criptografada usando o script `src/utils/crypto.py`.
### Como gerar a senha criptografada
1. Escolha um numero de deslocamento (qualquer inteiro)
2. Rode o script passando a senha real e o deslocamento:
```bash
python src/utils/crypto.py SuaSenhaAqui 5
```
3. A saida sera algo como:
```
Original: SuaSenhaAqui
Criptografada: XzfXjsmfFvzn
Deslocamento: 5
Coloque no .env:
DB_PASS=XzfXjsmfFvzn
CAESAR_SHIFT=5
```
4. Copie os valores gerados para o `.env`:
```env
DB_PASS=XzfXjsmfFvzn
CAESAR_SHIFT=5
```
A aplicacao descriptografa a senha automaticamente na hora de conectar ao banco.

0
src/config/__init__.py Normal file
View file

View file

@ -0,0 +1,42 @@
import logging
from datetime import datetime, timedelta
from pathlib import Path
def _cleanup_old_logs(log_dir: Path, base_name: str, backup_count: int):
"""Remove logs mais antigos que backup_count dias."""
cutoff = datetime.now().date() - timedelta(days=backup_count)
for f in log_dir.glob(f"{base_name}-*.log"):
try:
date_str = f.stem.replace(f"{base_name}-", "")
file_date = datetime.strptime(date_str, "%Y-%m-%d").date()
if file_date < cutoff:
f.unlink()
except (ValueError, OSError):
pass
def setup_logging(log_level: int = logging.INFO):
log_dir = Path("logs")
log_dir.mkdir(parents=True, exist_ok=True)
base_name = "fat_estoque"
backup_count = 21
today = datetime.now().strftime("%Y-%m-%d")
log_path = log_dir / f"{base_name}-{today}.log"
formatter = logging.Formatter(
"%(levelname)s|%(name)s|%(asctime)s|%(message)s|%(filename)s|%(lineno)d",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler = logging.FileHandler(str(log_path), mode="a", encoding="utf-8")
file_handler.setFormatter(formatter)
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
if root_logger.hasHandlers():
root_logger.handlers.clear()
root_logger.addHandler(file_handler)
_cleanup_old_logs(log_dir, base_name, backup_count)

0
src/config/settings.py Normal file
View file

0
src/database/__init__.py Normal file
View file

View file

@ -0,0 +1,73 @@
import os
import pyodbc
from src.utils.crypto import caesar_decrypt
class SqlServerDB:
"""Conexao com o SQL Server via pyodbc.
O pool e gerenciado pelo Driver Manager do ODBC (pyodbc.pooling, ligado por
padrao), que reaproveita conexoes com a mesma string de conexao. Por isso
nao existe um pool explicito como o do oracledb.
"""
def __init__(self):
self._connection_string = None
def _build_connection_string(self) -> str:
"""Monta a string de conexao a partir do .env (e guarda em cache)."""
if self._connection_string:
return self._connection_string
server = os.getenv('DB_SERVER', '')
port = os.getenv('DB_PORT', '1433')
database = os.getenv('DB_DATABASE', '')
user = os.getenv('DB_USER', '')
faltando = [
nome for nome, valor in (
('DB_SERVER', server),
('DB_DATABASE', database),
('DB_USER', user),
) if not valor
]
if faltando:
raise ConnectionError(
f"Variaveis de ambiente nao configuradas: {', '.join(faltando)}"
)
password = caesar_decrypt(
os.getenv('DB_PASS', ''),
int(os.getenv('CAESAR_SHIFT', '0'))
)
self._connection_string = ';'.join([
f"DRIVER={{{os.getenv('DB_DRIVER', 'ODBC Driver 17 for SQL Server')}}}",
f"SERVER={server},{port}",
f"DATABASE={database}",
f"UID={user}",
f"PWD={password}",
f"Encrypt={os.getenv('DB_ENCRYPT', 'yes')}",
f"TrustServerCertificate={os.getenv('DB_TRUST_CERT', 'no')}",
])
return self._connection_string
def connect(self) -> pyodbc.Connection:
"""Abre uma conexao nova (ou reaproveita uma do pool do ODBC)."""
return pyodbc.connect(
self._build_connection_string(),
timeout=int(os.getenv('DB_LOGIN_TIMEOUT', '30'))
)
def get_db(self):
"""Entrega uma conexao e devolve ao pool do ODBC ao finalizar."""
connection = self.connect()
try:
yield connection
finally:
connection.close()
db_instance = SqlServerDB()

0
src/holmes/__init__.py Normal file
View file

842
src/holmes/client.py Normal file
View file

@ -0,0 +1,842 @@
import json
import logging
import os
import requests
from src.models.erro_holmes import ErroHolmes
from src.models.estoque_homes import EstoqueHolmes
from src.models.proposta import Proposta
from src.models.resultado import Resultado
from src.repositories.estoque_repository import EstoqueRepository
from src.utils.utils import extrair_proposta
class HolmesAPI:
def __init__(self):
self.headers = {
# "authorization": self._token_usuario(),
"api_token": 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNmE4MzBjMzcwNDdlNzI1MTVlMDU1ZDFhIiwiaWF0IjoxNzg2OTczMjM5fQ.K-R2E6v7JjCJDJJfDhyZ8nwQwMiaVfbF2BtUvW-sNwQ',
"Content-Type": "application/json"
}
def _somente_digitos(self, valor) -> str:
return ''.join(c for c in str(valor or '') if c.isdigit())
def _token_usuario(self) -> str:
url = "https://app-api.holmesdoc.io/v1/session"
body = {"email": os.getenv('HOLMES_USER'), "password": os.getenv('HOLMES_PASS')}
headers = {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"}
response = requests.post(url, json=body, headers=headers)
response.raise_for_status()
dados = response.json()
if "token" not in dados:
logging.error(
f"Login Holmes sem token (HTTP {response.status_code}): {str(dados)[:500]}"
)
raise RuntimeError("Login Holmes não retornou token")
return dados["token"]
def _build_payload_novos(self, proposta: Proposta) -> dict:
start_event = 'StartEvent_1'
campos = [
("e88ea290-8ce4-11f1-9f28-e51e49945850", proposta.empresa_holmes),
("ed2d30f0-8ce4-11f1-9f28-e51e49945850", proposta.tipo_pessoa),
("f2fe24d0-8ce4-11f1-9f28-e51e49945850", proposta.usado_na_troca),
("2aafcff0-8cf4-11f1-9f28-e51e49945850", proposta.forma_pagamento),
("fad87d40-8ce4-11f1-9f28-e51e49945850", proposta.proposta_apollo),
("fdfd63f0-8ce4-11f1-9f28-e51e49945850", proposta.veiculo),
("01109760-8ce5-11f1-9f28-e51e49945850", proposta.chassi),
("04ab7160-8ce5-11f1-9f28-e51e49945850", proposta.des_modelo),
("1b0974c0-8ce5-11f1-9f28-e51e49945850", proposta.valor_proposta),
("23914b40-8ce5-11f1-9f28-e51e49945850", proposta.nome),
("27b419f0-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.cpfcnpj)),
("398ac070-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.celular)),
("3e7bb490-8ce5-11f1-9f28-e51e49945850", proposta.email),
("6873d880-9ccc-11f1-b919-f777409ec14c", proposta.vendedor_holmes),
]
payload = {
"workflow": {
"start_event": start_event,
"property_values": [
{"id": prop_id, "value": valor}
for prop_id, valor in campos
if valor is not None and str(valor).strip() != ''
],
"whats": "",
"documents": [],
"test": False
}
}
logging.debug("Payload proposta %s: %s", proposta.proposta_apollo, payload)
return payload
def _build_payload_usados(self, proposta: Proposta) -> dict:
start_event = 'StartEvent_1'
campos = [
("e88ea290-8ce4-11f1-9f28-e51e49945850", proposta.empresa_holmes),
("ed2d30f0-8ce4-11f1-9f28-e51e49945850", proposta.tipo_pessoa),
("2aafcff0-8cf4-11f1-9f28-e51e49945850", proposta.forma_pagamento),
("f2fe24d0-8ce4-11f1-9f28-e51e49945850", proposta.usado_na_troca),
("fad87d40-8ce4-11f1-9f28-e51e49945850", proposta.proposta_apollo),
("fdfd63f0-8ce4-11f1-9f28-e51e49945850", proposta.veiculo),
("01109760-8ce5-11f1-9f28-e51e49945850", proposta.chassi),
("04ab7160-8ce5-11f1-9f28-e51e49945850", proposta.des_modelo),
("08b38c20-8ce5-11f1-9f28-e51e49945850", proposta.placa),
("1b0974c0-8ce5-11f1-9f28-e51e49945850", proposta.valor_proposta),
("23914b40-8ce5-11f1-9f28-e51e49945850", proposta.nome),
("27b419f0-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.cpfcnpj)),
("398ac070-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.celular)),
("3e7bb490-8ce5-11f1-9f28-e51e49945850", proposta.email),
("19c78c10-9d92-11f1-ad38-41765eab56e3", proposta.vendedor_holmes),
]
payload = {
"workflow": {
"start_event": start_event,
"property_values": [
{"id": prop_id, "value": valor}
for prop_id, valor in campos
if valor is not None and str(valor).strip() != ''
],
"whats": "",
"documents": [],
"test": False
}
}
logging.debug("Payload proposta %s: %s", proposta.proposta_apollo, payload)
return payload
def _build_payload_imobilizado(self, proposta: Proposta) -> dict:
start_event = 'StartEvent_1'
campos = [
("e88ea290-8ce4-11f1-9f28-e51e49945850", proposta.empresa_holmes),
("ed2d30f0-8ce4-11f1-9f28-e51e49945850", proposta.tipo_pessoa),
("f2fe24d0-8ce4-11f1-9f28-e51e49945850", proposta.usado_na_troca),
("2aafcff0-8cf4-11f1-9f28-e51e49945850", proposta.forma_pagamento),
("fad87d40-8ce4-11f1-9f28-e51e49945850", proposta.proposta_apollo),
("fdfd63f0-8ce4-11f1-9f28-e51e49945850", proposta.veiculo),
("01109760-8ce5-11f1-9f28-e51e49945850", proposta.chassi),
("04ab7160-8ce5-11f1-9f28-e51e49945850", proposta.des_modelo),
("08b38c20-8ce5-11f1-9f28-e51e49945850", proposta.placa),
("1b0974c0-8ce5-11f1-9f28-e51e49945850", proposta.valor_proposta),
("23914b40-8ce5-11f1-9f28-e51e49945850", proposta.nome),
("27b419f0-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.cpfcnpj)),
("398ac070-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.celular)),
("3e7bb490-8ce5-11f1-9f28-e51e49945850", proposta.email),
("d1b18d20-9d98-11f1-8298-cb4f6a31d606", proposta.vendedor_holmes),
]
payload = {
"workflow": {
"start_event": start_event,
"property_values": [
{"id": prop_id, "value": valor}
for prop_id, valor in campos
if valor is not None and str(valor).strip() != ''
],
"whats": "",
"documents": [],
"test": False
}
}
logging.debug("Payload proposta %s: %s", proposta.proposta_apollo, payload)
return payload
def _build_payload_venda_direta(self, proposta: Proposta) -> dict:
# Esse formulario tem IDs proprios (nao reaproveita os dos outros tres),
# nao tem Placa e traz a Observacao da proposta.
start_event = 'StartEvent_1'
campos = [
("37897690-8d03-11f1-9f28-e51e49945850", proposta.empresa_holmes),
("420c59c0-8d03-11f1-9f28-e51e49945850", proposta.tipo_pessoa),
("46da1900-8d04-11f1-9f28-e51e49945850", proposta.forma_pagamento),
("48ee6f80-8d03-11f1-9f28-e51e49945850", proposta.usado_na_troca),
("53933290-8d03-11f1-9f28-e51e49945850", proposta.proposta_apollo),
("58d81310-8d03-11f1-9f28-e51e49945850", proposta.veiculo),
("5b19c5b0-8d03-11f1-9f28-e51e49945850", proposta.chassi),
("5f92a3a0-8d03-11f1-9f28-e51e49945850", proposta.des_modelo),
("6b95e540-8d03-11f1-9f28-e51e49945850", proposta.valor_proposta),
("73c56290-8d03-11f1-9f28-e51e49945850", proposta.nome),
("79635f90-8d03-11f1-9f28-e51e49945850", self._somente_digitos(proposta.cpfcnpj)),
("85ea9e40-8d03-11f1-9f28-e51e49945850", self._somente_digitos(proposta.celular)),
("8f16f220-8d03-11f1-9f28-e51e49945850", proposta.email),
("cbe61910-a2de-11f1-aff4-4f1f04587741", proposta.vendedor_holmes),
("e7610290-a2de-11f1-aff4-4f1f04587741", proposta.observacao),
]
payload = {
"workflow": {
"start_event": start_event,
"property_values": [
{"id": prop_id, "value": valor}
for prop_id, valor in campos
if valor is not None and str(valor).strip() != ''
],
"whats": "",
"documents": [],
"test": False
}
}
logging.debug("Payload proposta %s: %s", proposta.proposta_apollo, payload)
return payload
def existe_holmes(self, chassi: str, proposta: str) -> bool:
"""Confere se o chassi já está lançado no holmes e caso tenha o mesmo chassi ele bate a proposta
Args:
chassi (str): Chassi do veiculo vendido
proposta (str): Proposta do veiculo
Returns:
bool: True caso existir e False caso não exista no holmes
"""
logging.info("Verificando se chassi %s (proposta %s) já existe no Holmes", chassi, proposta)
url = 'https://app-api.holmesdoc.io/v2/search'
body = {
"query": {
"from": 0,
"size": 50,
"context": "process",
"sort": "updated_at",
"order": "desc",
"groups": [
{
"match_all": True,
"terms": [
{
"value": f"{chassi}",
"type": "match_phrase",
"field": "_content"
}
]
}
]
},
"trash": False,
"deleted_by_me": False
}
resposta = requests.post(url, json=body, headers=self.headers)
resposta.raise_for_status()
dados = resposta.json()
if dados['total'] == 0:
logging.info("Chassi %s não encontrado no Holmes", chassi)
return False
PROCESSOS_VALIDOS = {
'Faturamento de Novos',
'Faturamento de Venda Direta',
'Faturamento de Test Drive',
'Faturamento de Usados',
}
docs_validos = [
doc for doc in dados.get('docs', [])
if doc.get('name') in PROCESSOS_VALIDOS
and doc.get('status') != 'canceled'
]
if not docs_validos:
logging.info("Chassi %s encontrado mas sem processos válidos (todos cancelados ou de outro template)", chassi)
return False
# proposta_holmes = extrair_proposta(docs_validos[0]['identifier'])
# if str(proposta_holmes) != str(proposta):
# logging.info("Chassi %s encontrado mas proposta diferente (Holmes: %s, Apollo: %s)", chassi, proposta_holmes, proposta)
# return False
logging.info("Chassi %s já existe no Holmes %s", chassi, proposta)
return True
def envia_fat_estoque_usado(self, proposta: Proposta, estoque_repo: EstoqueRepository) -> Resultado:
logging.info("Enviando proposta %s para Holmes (empresa=%s, revenda=%s)", proposta.proposta_apollo, proposta.empresa, proposta.revenda)
url = 'https://app-api.holmesdoc.io/v1/workflows/6a6cc58744c4ba86a63aebc1/start'
payload = self._build_payload_usados(proposta)
logging.debug("Payload montado para proposta %s", proposta.proposta_apollo)
response = requests.post(url, headers=self.headers, data=json.dumps(payload))
logging.debug("Holmes respondeu com status %s para proposta %s", response.status_code, proposta.proposta_apollo)
try:
if response.status_code != 200:
err_msg = f"Erro ao enviar para Holmes: {response.status_code} - {response.text or ''}"
err_msg = (err_msg[:3900] + "...") if len(err_msg) > 3900 else err_msg
logging.error("Falha ao enviar proposta %s: %s", proposta.proposta_apollo, err_msg)
estoque_repo.insere_erro(
ErroHolmes(
empresa = proposta.empresa,
revenda = proposta.revenda,
proposta_apollo = proposta.proposta_apollo,
motivo_erro= err_msg
)
)
return Resultado(sucesso=False, mensagem=err_msg)
else:
resposta = response.json()
logging.info("Proposta %s enviada com sucesso - processo Holmes: %s", proposta.proposta_apollo, resposta['id'])
estoque_repo.insere_estoque(
EstoqueHolmes(
empresa= proposta.empresa,
revenda= proposta.revenda,
proposta_apollo= proposta.proposta_apollo,
proesso_id= resposta['id'],
usuario_criou= 'SUPORTE SISTEMAS',
nome_processo= '[FAT] Faturamento de Estoque'
)
)
return Resultado(sucesso=True, mensagem=resposta['id'])
except Exception as e:
logging.error("Erro inesperado ao processar proposta %s: %s", proposta.proposta_apollo, e, exc_info=True)
return Resultado(sucesso=False, mensagem=str(e))
def envia_fat_estoque_novos(self, proposta: Proposta, estoque_repo: EstoqueRepository) -> Resultado:
logging.info("Enviando proposta %s para Holmes (empresa=%s, revenda=%s)", proposta.proposta_apollo, proposta.empresa, proposta.revenda)
url = 'https://app-api.holmesdoc.io/v1/workflows/6a6cbe67aff519bb6aa4b2d8/start'
payload = self._build_payload_novos(proposta)
logging.debug("Payload montado para proposta %s", proposta.proposta_apollo)
response = requests.post(url, headers=self.headers, data=json.dumps(payload))
logging.debug("Holmes respondeu com status %s para proposta %s", response.status_code, proposta.proposta_apollo)
try:
if response.status_code != 200:
err_msg = f"Erro ao enviar para Holmes: {response.status_code} - {response.text or ''}"
err_msg = (err_msg[:3900] + "...") if len(err_msg) > 3900 else err_msg
logging.error("Falha ao enviar proposta %s: %s", proposta.proposta_apollo, err_msg)
estoque_repo.insere_erro(
ErroHolmes(
empresa = proposta.empresa,
revenda = proposta.revenda,
proposta_apollo = proposta.proposta_apollo,
motivo_erro= err_msg
)
)
return Resultado(sucesso=False, mensagem=err_msg)
else:
resposta = response.json()
logging.info("Proposta %s enviada com sucesso - processo Holmes: %s", proposta.proposta_apollo, resposta['id'])
estoque_repo.insere_estoque(
EstoqueHolmes(
empresa= proposta.empresa,
revenda= proposta.revenda,
proposta_apollo= proposta.proposta_apollo,
proesso_id= resposta['id'],
usuario_criou= 'SUPORTE SISTEMAS',
nome_processo= '[FAT] Faturamento de Estoque'
)
)
return Resultado(sucesso=True, mensagem=resposta['id'])
except Exception as e:
logging.error("Erro inesperado ao processar proposta %s: %s", proposta.proposta_apollo, e, exc_info=True)
return Resultado(sucesso=False, mensagem=str(e))
def envia_fat_estoque_imobilizado(self, proposta: Proposta, estoque_repo: EstoqueRepository) -> Resultado:
logging.info("Enviando proposta %s para Holmes (empresa=%s, revenda=%s)", proposta.proposta_apollo, proposta.empresa, proposta.revenda)
url = 'https://app-api.holmesdoc.io/v1/workflows/6a6cc2a3dbf5173588777520/start'
payload = self._build_payload_imobilizado(proposta)
logging.debug("Payload montado para proposta %s", proposta.proposta_apollo)
response = requests.post(url, headers=self.headers, data=json.dumps(payload))
logging.debug("Holmes respondeu com status %s para proposta %s", response.status_code, proposta.proposta_apollo)
try:
if response.status_code != 200:
err_msg = f"Erro ao enviar para Holmes: {response.status_code} - {response.text or ''}"
err_msg = (err_msg[:3900] + "...") if len(err_msg) > 3900 else err_msg
logging.error("Falha ao enviar proposta %s: %s", proposta.proposta_apollo, err_msg)
estoque_repo.insere_erro(
ErroHolmes(
empresa = proposta.empresa,
revenda = proposta.revenda,
proposta_apollo = proposta.proposta_apollo,
motivo_erro= err_msg
)
)
return Resultado(sucesso=False, mensagem=err_msg)
else:
resposta = response.json()
logging.info("Proposta %s enviada com sucesso - processo Holmes: %s", proposta.proposta_apollo, resposta['id'])
estoque_repo.insere_estoque(
EstoqueHolmes(
empresa= proposta.empresa,
revenda= proposta.revenda,
proposta_apollo= proposta.proposta_apollo,
proesso_id= resposta['id'],
usuario_criou= 'SUPORTE SISTEMAS',
nome_processo= '[FAT] Faturamento de Estoque'
)
)
return Resultado(sucesso=True, mensagem=resposta['id'])
except Exception as e:
logging.error("Erro inesperado ao processar proposta %s: %s", proposta.proposta_apollo, e, exc_info=True)
return Resultado(sucesso=False, mensagem=str(e))
def envia_fat_estoque_venda_direta(self, proposta: Proposta, estoque_repo: EstoqueRepository) -> Resultado:
logging.info("Enviando proposta %s para Holmes (empresa=%s, revenda=%s)", proposta.proposta_apollo, proposta.empresa, proposta.revenda)
url = 'https://app-api.holmesdoc.io/v1/workflows/6a6cd6449341bbc7e45f29cf/start'
payload = self._build_payload_venda_direta(proposta)
logging.debug("Payload montado para proposta %s", proposta.proposta_apollo)
response = requests.post(url, headers=self.headers, data=json.dumps(payload))
logging.debug("Holmes respondeu com status %s para proposta %s", response.status_code, proposta.proposta_apollo)
try:
if response.status_code != 200:
err_msg = f"Erro ao enviar para Holmes: {response.status_code} - {response.text or ''}"
err_msg = (err_msg[:3900] + "...") if len(err_msg) > 3900 else err_msg
logging.error("Falha ao enviar proposta %s: %s", proposta.proposta_apollo, err_msg)
estoque_repo.insere_erro(
ErroHolmes(
empresa = proposta.empresa,
revenda = proposta.revenda,
proposta_apollo = proposta.proposta_apollo,
motivo_erro= err_msg
)
)
return Resultado(sucesso=False, mensagem=err_msg)
else:
resposta = response.json()
logging.info("Proposta %s enviada com sucesso - processo Holmes: %s", proposta.proposta_apollo, resposta['id'])
estoque_repo.insere_estoque(
EstoqueHolmes(
empresa= proposta.empresa,
revenda= proposta.revenda,
proposta_apollo= proposta.proposta_apollo,
proesso_id= resposta['id'],
usuario_criou= 'SUPORTE SISTEMAS',
nome_processo= '[FAT] Faturamento de Estoque'
)
)
return Resultado(sucesso=True, mensagem=resposta['id'])
except Exception as e:
logging.error("Erro inesperado ao processar proposta %s: %s", proposta.proposta_apollo, e, exc_info=True)
return Resultado(sucesso=False, mensagem=str(e))
# import json
# import logging
# import os
# import requests
# from src.models.erro_holmes import ErroHolmes
# from src.models.estoque_homes import EstoqueHolmes
# from src.models.proposta import Proposta
# from src.models.resultado import Resultado
# from src.repositories.estoque_repository import EstoqueRepository
# from src.utils.utils import extrair_proposta
# class HolmesAPI:
# def __init__(self):
# self.headers = {
# "authorization": self._token_usuario(),
# "Content-Type": "application/json"
# }
# def _somente_digitos(self, valor) -> str:
# return ''.join(c for c in str(valor or '') if c.isdigit())
# def _token_usuario(self) -> str:
# url = "https://app-api.holmesdoc.io/v1/session"
# body = {"email": os.getenv('HOLMES_USER'), "password": os.getenv('HOLMES_PASS')}
# headers = {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"}
# response = requests.post(url, json=body, headers=headers)
# response.raise_for_status()
# dados = response.json()
# if "token" not in dados:
# logging.error(
# f"Login Holmes sem token (HTTP {response.status_code}): {str(dados)[:500]}"
# )
# raise RuntimeError("Login Holmes não retornou token")
# return dados["token"]
# def _build_payload_novos(self, proposta: Proposta) -> dict:
# start_event = 'StartEvent_1'
# campos = [
# ("e88ea290-8ce4-11f1-9f28-e51e49945850", proposta.empresa_holmes),
# ("ed2d30f0-8ce4-11f1-9f28-e51e49945850", proposta.tipo_pessoa),
# ("f2fe24d0-8ce4-11f1-9f28-e51e49945850", proposta.usado_na_troca),
# ("2aafcff0-8cf4-11f1-9f28-e51e49945850", proposta.forma_pagamento),
# ("fad87d40-8ce4-11f1-9f28-e51e49945850", proposta.proposta_apollo),
# ("fdfd63f0-8ce4-11f1-9f28-e51e49945850", proposta.veiculo),
# ("01109760-8ce5-11f1-9f28-e51e49945850", proposta.chassi),
# ("04ab7160-8ce5-11f1-9f28-e51e49945850", proposta.des_modelo),
# ("1b0974c0-8ce5-11f1-9f28-e51e49945850", proposta.valor_proposta),
# ("23914b40-8ce5-11f1-9f28-e51e49945850", proposta.nome),
# ("27b419f0-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.cpfcnpj)),
# ("398ac070-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.celular)),
# ("3e7bb490-8ce5-11f1-9f28-e51e49945850", proposta.email),
# ("6873d880-9ccc-11f1-b919-f777409ec14c", proposta.vendedor_holmes),
# ]
# payload = {
# "workflow": {
# "start_event": start_event,
# "property_values": [
# {"id": prop_id, "value": valor}
# for prop_id, valor in campos
# if valor is not None and str(valor).strip() != ''
# ],
# "whats": "",
# "documents": [],
# "test": False
# }
# }
# logging.debug("Payload proposta %s: %s", proposta.proposta_apollo, payload)
# return payload
# def _build_payload_usados(self, proposta: Proposta) -> dict:
# start_event = 'StartEvent_1'
# campos = [
# ("e88ea290-8ce4-11f1-9f28-e51e49945850", proposta.empresa_holmes),
# ("ed2d30f0-8ce4-11f1-9f28-e51e49945850", proposta.tipo_pessoa),
# ("2aafcff0-8cf4-11f1-9f28-e51e49945850", proposta.forma_pagamento),
# ("f2fe24d0-8ce4-11f1-9f28-e51e49945850", proposta.usado_na_troca),
# ("fad87d40-8ce4-11f1-9f28-e51e49945850", proposta.proposta_apollo),
# ("fdfd63f0-8ce4-11f1-9f28-e51e49945850", proposta.veiculo),
# ("01109760-8ce5-11f1-9f28-e51e49945850", proposta.chassi),
# ("04ab7160-8ce5-11f1-9f28-e51e49945850", proposta.des_modelo),
# ("08b38c20-8ce5-11f1-9f28-e51e49945850", proposta.placa),
# ("1b0974c0-8ce5-11f1-9f28-e51e49945850", proposta.valor_proposta),
# ("23914b40-8ce5-11f1-9f28-e51e49945850", proposta.nome),
# ("27b419f0-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.cpfcnpj)),
# ("398ac070-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.celular)),
# ("3e7bb490-8ce5-11f1-9f28-e51e49945850", proposta.email),
# ("19c78c10-9d92-11f1-ad38-41765eab56e3", proposta.vendedor_holmes),
# ]
# payload = {
# "workflow": {
# "start_event": start_event,
# "property_values": [
# {"id": prop_id, "value": valor}
# for prop_id, valor in campos
# if valor is not None and str(valor).strip() != ''
# ],
# "whats": "",
# "documents": [],
# "test": False
# }
# }
# logging.debug("Payload proposta %s: %s", proposta.proposta_apollo, payload)
# return payload
# def _build_payload_imobilizado(self, proposta: Proposta) -> dict:
# start_event = 'StartEvent_1'
# campos = [
# ("e88ea290-8ce4-11f1-9f28-e51e49945850", proposta.empresa_holmes),
# ("ed2d30f0-8ce4-11f1-9f28-e51e49945850", proposta.tipo_pessoa),
# ("f2fe24d0-8ce4-11f1-9f28-e51e49945850", proposta.usado_na_troca),
# ("2aafcff0-8cf4-11f1-9f28-e51e49945850", proposta.forma_pagamento),
# ("fad87d40-8ce4-11f1-9f28-e51e49945850", proposta.proposta_apollo),
# ("fdfd63f0-8ce4-11f1-9f28-e51e49945850", proposta.veiculo),
# ("01109760-8ce5-11f1-9f28-e51e49945850", proposta.chassi),
# ("04ab7160-8ce5-11f1-9f28-e51e49945850", proposta.des_modelo),
# ("08b38c20-8ce5-11f1-9f28-e51e49945850", proposta.placa),
# ("1b0974c0-8ce5-11f1-9f28-e51e49945850", proposta.valor_proposta),
# ("23914b40-8ce5-11f1-9f28-e51e49945850", proposta.nome),
# ("27b419f0-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.cpfcnpj)),
# ("398ac070-8ce5-11f1-9f28-e51e49945850", self._somente_digitos(proposta.celular)),
# ("3e7bb490-8ce5-11f1-9f28-e51e49945850", proposta.email),
# ("d1b18d20-9d98-11f1-8298-cb4f6a31d606", proposta.vendedor_holmes),
# ]
# payload = {
# "workflow": {
# "start_event": start_event,
# "property_values": [
# {"id": prop_id, "value": valor}
# for prop_id, valor in campos
# if valor is not None and str(valor).strip() != ''
# ],
# "whats": "",
# "documents": [],
# "test": False
# }
# }
# logging.debug("Payload proposta %s: %s", proposta.proposta_apollo, payload)
# return payload
# def _build_payload_venda_direta(self, proposta: Proposta) -> dict:
# # Esse formulario tem IDs proprios (nao reaproveita os dos outros tres),
# # nao tem Placa e traz a Observacao da proposta.
# start_event = 'StartEvent_1'
# campos = [
# ("37897690-8d03-11f1-9f28-e51e49945850", proposta.empresa_holmes),
# ("420c59c0-8d03-11f1-9f28-e51e49945850", proposta.tipo_pessoa),
# ("46da1900-8d04-11f1-9f28-e51e49945850", proposta.forma_pagamento),
# ("48ee6f80-8d03-11f1-9f28-e51e49945850", proposta.usado_na_troca),
# ("53933290-8d03-11f1-9f28-e51e49945850", proposta.proposta_apollo),
# ("58d81310-8d03-11f1-9f28-e51e49945850", proposta.veiculo),
# ("5b19c5b0-8d03-11f1-9f28-e51e49945850", proposta.chassi),
# ("5f92a3a0-8d03-11f1-9f28-e51e49945850", proposta.des_modelo),
# ("6b95e540-8d03-11f1-9f28-e51e49945850", proposta.valor_proposta),
# ("73c56290-8d03-11f1-9f28-e51e49945850", proposta.nome),
# ("79635f90-8d03-11f1-9f28-e51e49945850", self._somente_digitos(proposta.cpfcnpj)),
# ("85ea9e40-8d03-11f1-9f28-e51e49945850", self._somente_digitos(proposta.celular)),
# ("8f16f220-8d03-11f1-9f28-e51e49945850", proposta.email),
# ("cbe61910-a2de-11f1-aff4-4f1f04587741", proposta.vendedor_holmes),
# ("e7610290-a2de-11f1-aff4-4f1f04587741", proposta.observacao),
# ]
# payload = {
# "workflow": {
# "start_event": start_event,
# "property_values": [
# {"id": prop_id, "value": valor}
# for prop_id, valor in campos
# if valor is not None and str(valor).strip() != ''
# ],
# "whats": "",
# "documents": [],
# "test": False
# }
# }
# logging.debug("Payload proposta %s: %s", proposta.proposta_apollo, payload)
# return payload
# def existe_holmes(self, chassi: str, proposta: str) -> bool:
# """Confere se o chassi já está lançado no holmes e caso tenha o mesmo chassi ele bate a proposta
# Args:
# chassi (str): Chassi do veiculo vendido
# proposta (str): Proposta do veiculo
# Returns:
# bool: True caso existir e False caso não exista no holmes
# """
# logging.info("Verificando se chassi %s (proposta %s) já existe no Holmes", chassi, proposta)
# url = 'https://app-api.holmesdoc.io/v2/search'
# body = {
# "query": {
# "from": 0,
# "size": 50,
# "context": "process",
# "sort": "updated_at",
# "order": "desc",
# "groups": [
# {
# "match_all": True,
# "terms": [
# {
# "value": f"{chassi}",
# "type": "match_phrase",
# "field": "_content"
# }
# ]
# }
# ]
# },
# "trash": False,
# "deleted_by_me": False
# }
# resposta = requests.post(url, json=body, headers=self.headers)
# resposta.raise_for_status()
# dados = resposta.json()
# if dados['total'] == 0:
# logging.info("Chassi %s não encontrado no Holmes", chassi)
# return False
# docs_validos = [
# doc for doc in dados['docs']
# if doc.get('name') == '[FAT] Faturamento de Estoque'
# and doc.get('status') != 'canceled'
# ]
# if not docs_validos:
# logging.info("Chassi %s encontrado mas sem processos válidos (todos cancelados ou de outro template)", chassi)
# return False
# proposta_holmes = extrair_proposta(docs_validos[0]['identifier'])
# if str(proposta_holmes) != str(proposta):
# logging.info("Chassi %s encontrado mas proposta diferente (Holmes: %s, Apollo: %s)", chassi, proposta_holmes, proposta)
# return False
# logging.info("Chassi %s já existe no Holmes com mesma proposta %s", chassi, proposta)
# return True
# def envia_fat_estoque_usado(self, proposta: Proposta, estoque_repo: EstoqueRepository) -> Resultado:
# logging.info("Enviando proposta %s para Holmes (empresa=%s, revenda=%s)", proposta.proposta_apollo, proposta.empresa, proposta.revenda)
# url = 'https://app-api.holmesdoc.io/v1/workflows/6a6cc58744c4ba86a63aebc1/start'
# payload = self._build_payload_usados(proposta)
# logging.debug("Payload montado para proposta %s", proposta.proposta_apollo)
# response = requests.post(url, headers=self.headers, data=json.dumps(payload))
# logging.debug("Holmes respondeu com status %s para proposta %s", response.status_code, proposta.proposta_apollo)
# try:
# if response.status_code != 200:
# err_msg = f"Erro ao enviar para Holmes: {response.status_code} - {response.text or ''}"
# err_msg = (err_msg[:3900] + "...") if len(err_msg) > 3900 else err_msg
# logging.error("Falha ao enviar proposta %s: %s", proposta.proposta_apollo, err_msg)
# estoque_repo.insere_erro(
# ErroHolmes(
# empresa = proposta.empresa,
# revenda = proposta.revenda,
# proposta_apollo = proposta.proposta_apollo,
# motivo_erro= err_msg
# )
# )
# return Resultado(sucesso=False, mensagem=err_msg)
# else:
# resposta = response.json()
# logging.info("Proposta %s enviada com sucesso - processo Holmes: %s", proposta.proposta_apollo, resposta['id'])
# estoque_repo.insere_estoque(
# EstoqueHolmes(
# empresa= proposta.empresa,
# revenda= proposta.revenda,
# proposta_apollo= proposta.proposta_apollo,
# proesso_id= resposta['id'],
# usuario_criou= 'SUPORTE SISTEMAS',
# nome_processo= '[FAT] Faturamento de Estoque'
# )
# )
# return Resultado(sucesso=True, mensagem=resposta['id'])
# except Exception as e:
# logging.error("Erro inesperado ao processar proposta %s: %s", proposta.proposta_apollo, e, exc_info=True)
# return Resultado(sucesso=False, mensagem=str(e))
# def envia_fat_estoque_novos(self, proposta: Proposta, estoque_repo: EstoqueRepository) -> Resultado:
# logging.info("Enviando proposta %s para Holmes (empresa=%s, revenda=%s)", proposta.proposta_apollo, proposta.empresa, proposta.revenda)
# url = 'https://app-api.holmesdoc.io/v1/workflows/6a6cbe67aff519bb6aa4b2d8/start'
# payload = self._build_payload_novos(proposta)
# logging.debug("Payload montado para proposta %s", proposta.proposta_apollo)
# response = requests.post(url, headers=self.headers, data=json.dumps(payload))
# logging.debug("Holmes respondeu com status %s para proposta %s", response.status_code, proposta.proposta_apollo)
# try:
# if response.status_code != 200:
# err_msg = f"Erro ao enviar para Holmes: {response.status_code} - {response.text or ''}"
# err_msg = (err_msg[:3900] + "...") if len(err_msg) > 3900 else err_msg
# logging.error("Falha ao enviar proposta %s: %s", proposta.proposta_apollo, err_msg)
# estoque_repo.insere_erro(
# ErroHolmes(
# empresa = proposta.empresa,
# revenda = proposta.revenda,
# proposta_apollo = proposta.proposta_apollo,
# motivo_erro= err_msg
# )
# )
# return Resultado(sucesso=False, mensagem=err_msg)
# else:
# resposta = response.json()
# logging.info("Proposta %s enviada com sucesso - processo Holmes: %s", proposta.proposta_apollo, resposta['id'])
# estoque_repo.insere_estoque(
# EstoqueHolmes(
# empresa= proposta.empresa,
# revenda= proposta.revenda,
# proposta_apollo= proposta.proposta_apollo,
# proesso_id= resposta['id'],
# usuario_criou= 'SUPORTE SISTEMAS',
# nome_processo= '[FAT] Faturamento de Estoque'
# )
# )
# return Resultado(sucesso=True, mensagem=resposta['id'])
# except Exception as e:
# logging.error("Erro inesperado ao processar proposta %s: %s", proposta.proposta_apollo, e, exc_info=True)
# return Resultado(sucesso=False, mensagem=str(e))
# def envia_fat_estoque_imobilizado(self, proposta: Proposta, estoque_repo: EstoqueRepository) -> Resultado:
# logging.info("Enviando proposta %s para Holmes (empresa=%s, revenda=%s)", proposta.proposta_apollo, proposta.empresa, proposta.revenda)
# url = 'https://app-api.holmesdoc.io/v1/workflows/6a6cc2a3dbf5173588777520/start'
# payload = self._build_payload_imobilizado(proposta)
# logging.debug("Payload montado para proposta %s", proposta.proposta_apollo)
# response = requests.post(url, headers=self.headers, data=json.dumps(payload))
# logging.debug("Holmes respondeu com status %s para proposta %s", response.status_code, proposta.proposta_apollo)
# try:
# if response.status_code != 200:
# err_msg = f"Erro ao enviar para Holmes: {response.status_code} - {response.text or ''}"
# err_msg = (err_msg[:3900] + "...") if len(err_msg) > 3900 else err_msg
# logging.error("Falha ao enviar proposta %s: %s", proposta.proposta_apollo, err_msg)
# estoque_repo.insere_erro(
# ErroHolmes(
# empresa = proposta.empresa,
# revenda = proposta.revenda,
# proposta_apollo = proposta.proposta_apollo,
# motivo_erro= err_msg
# )
# )
# return Resultado(sucesso=False, mensagem=err_msg)
# else:
# resposta = response.json()
# logging.info("Proposta %s enviada com sucesso - processo Holmes: %s", proposta.proposta_apollo, resposta['id'])
# estoque_repo.insere_estoque(
# EstoqueHolmes(
# empresa= proposta.empresa,
# revenda= proposta.revenda,
# proposta_apollo= proposta.proposta_apollo,
# proesso_id= resposta['id'],
# usuario_criou= 'SUPORTE SISTEMAS',
# nome_processo= '[FAT] Faturamento de Estoque'
# )
# )
# return Resultado(sucesso=True, mensagem=resposta['id'])
# except Exception as e:
# logging.error("Erro inesperado ao processar proposta %s: %s", proposta.proposta_apollo, e, exc_info=True)
# return Resultado(sucesso=False, mensagem=str(e))
# def envia_fat_estoque_venda_direta(self, proposta: Proposta, estoque_repo: EstoqueRepository) -> Resultado:
# logging.info("Enviando proposta %s para Holmes (empresa=%s, revenda=%s)", proposta.proposta_apollo, proposta.empresa, proposta.revenda)
# url = 'https://app-api.holmesdoc.io/v1/workflows/6a6cd6449341bbc7e45f29cf/start'
# payload = self._build_payload_venda_direta(proposta)
# logging.debug("Payload montado para proposta %s", proposta.proposta_apollo)
# response = requests.post(url, headers=self.headers, data=json.dumps(payload))
# logging.debug("Holmes respondeu com status %s para proposta %s", response.status_code, proposta.proposta_apollo)
# try:
# if response.status_code != 200:
# err_msg = f"Erro ao enviar para Holmes: {response.status_code} - {response.text or ''}"
# err_msg = (err_msg[:3900] + "...") if len(err_msg) > 3900 else err_msg
# logging.error("Falha ao enviar proposta %s: %s", proposta.proposta_apollo, err_msg)
# estoque_repo.insere_erro(
# ErroHolmes(
# empresa = proposta.empresa,
# revenda = proposta.revenda,
# proposta_apollo = proposta.proposta_apollo,
# motivo_erro= err_msg
# )
# )
# return Resultado(sucesso=False, mensagem=err_msg)
# else:
# resposta = response.json()
# logging.info("Proposta %s enviada com sucesso - processo Holmes: %s", proposta.proposta_apollo, resposta['id'])
# estoque_repo.insere_estoque(
# EstoqueHolmes(
# empresa= proposta.empresa,
# revenda= proposta.revenda,
# proposta_apollo= proposta.proposta_apollo,
# proesso_id= resposta['id'],
# usuario_criou= 'SUPORTE SISTEMAS',
# nome_processo= '[FAT] Faturamento de Estoque'
# )
# )
# return Resultado(sucesso=True, mensagem=resposta['id'])
# except Exception as e:
# logging.error("Erro inesperado ao processar proposta %s: %s", proposta.proposta_apollo, e, exc_info=True)
# return Resultado(sucesso=False, mensagem=str(e))

0
src/models/__init__.py Normal file
View file

View file

@ -0,0 +1,8 @@
from pydantic import BaseModel
class ErroHolmes(BaseModel):
empresa: str
revenda: str
proposta_apollo: str
motivo_erro: str
model_config = {"coerce_numbers_to_str": True}

View file

@ -0,0 +1,9 @@
from pydantic import BaseModel
class EstoqueHolmes(BaseModel):
empresa: str
revenda: str
proposta_apollo: str
proesso_id: str # Está errado assim pq fiz a coluna errada no banco e ja tem um monte de relatorio apontando pra ela, ent se mudar vai dar erro geral
usuario_criou: str
nome_processo: str

69
src/models/proposta.py Normal file
View file

@ -0,0 +1,69 @@
# from pydantic import BaseModel
# class Proposta(BaseModel):
# empresa: str
# revenda: str
# nome_cliente: str
# tipo_pessoa: str
# cpfcnpj: str | None = None
# celular_com_ddd: str | None = None
# email: str | None = None
# chassi: str
# usado_na_troca: str
# proposta_apollo: str
# unidade: str
# vendedor: str
# valor_proposta: str
# financiamento: str
# consorcio: str
# a_vista: str
# leasing: str
# vendedor_holmes: str | None = None
# qtd_usados_na_troca: str
# placas_usados: str | None = None
# novo_usado: str
# observacao: str
# val_usado_troca: str | None = None
# endereco_cliente: str
# des_cor: str
# des_veiculo: str
# model_config = {"coerce_numbers_to_str": True}
from pydantic import BaseModel, Field
class Proposta(BaseModel):
"""Uma linha da query `busca_propostas`.
Os `alias` sao os nomes das colunas que voltam do SELECT (em minusculo,
como o repository entrega). Os nomes dos campos sao os usados no resto
do codigo, entao a query pode renomear coluna sem quebrar o client.
"""
empresa: str
revenda: str
empresa_holmes: str | None = None
proposta_apollo: str = Field(alias='proposta')
tipo_pessoa: str | None = None
usado_na_troca: str = Field(default='')
forma_pagamento: str = Field(default='')
veiculo: str | None = None
chassi: str | None = None
des_modelo: str | None = None
placa: str | None = None
valor_proposta: float | None = Field(default=None, alias='val_proposta')
nome: str | None = None
cpfcnpj: str | None = None
celular: str | None = None
email: str | None = None
vendedor_holmes: str | None = Field(default=None, alias='vendedor')
observacao: str | None = None
novo_usado: str | None = None
tipo_venda: str | None = None
situacao: str | None = None
model_config = {
"coerce_numbers_to_str": True,
"populate_by_name": True,
}

5
src/models/resultado.py Normal file
View file

@ -0,0 +1,5 @@
from pydantic import BaseModel
class Resultado(BaseModel):
sucesso: bool
mensagem: str

View file

View file

@ -0,0 +1,93 @@
import logging
from pyodbc import Connection, DatabaseError
from src.models.proposta import Proposta
from src.models.resultado import Resultado
from src.models.erro_holmes import ErroHolmes
from src.models.estoque_homes import EstoqueHolmes
from src.repositories.queries import busca_propostas, insere_estoque_sql, insert_erro_sql
class EstoqueRepository:
def __init__(self, connection: Connection) -> None:
self.connection = connection
def buscar_propostas(self) -> list[Proposta] | None:
"""Devolve todas as propostas do Apollo que não estão integradas no Holmes
Returns:
list[Proposta] | None: Lista de propostas ou None em caso de erro
"""
try:
cursor = self.connection.cursor()
cursor.execute(busca_propostas)
if cursor.description is None:
return None
columns = [str(col[0]).lower() for col in cursor.description]
return [Proposta(**dict(zip(columns, row))) for row in cursor.fetchall()]
except DatabaseError as e:
logging.error(f"Erro SQL Server ao buscar propostas: {e}")
return None
except Exception as e:
logging.error(f"Erro estranho: {e}")
return None
def insere_estoque(self, dados: EstoqueHolmes) -> Resultado:
"""Insere no banco de dados os dados da proposta criada no holmes
Args:
dados (EstoqueHolmes): Merge dos dados que vem do Holmes e os que foram usados para criar ele.
Returns:
Resultado: Apenas um tipo de dados que retorna se deu certo ou não e uma mensagem caso de erro
"""
try:
logging.debug(f"Inserindo estoque: processo {dados.proesso_id}")
cursor = self.connection.cursor()
cursor.execute(
insere_estoque_sql,
(
dados.empresa,
dados.revenda,
dados.proposta_apollo,
dados.proesso_id,
),
)
self.connection.commit()
logging.debug(f"Estoque inserido com sucesso: processo {dados.proesso_id}")
return Resultado(sucesso=True, mensagem='Sucesso chefia')
except DatabaseError as e:
logging.error(f"Erro SQL Server ao inserir estoque: {e}")
return Resultado(sucesso=False, mensagem=str(e))
except Exception as e:
logging.error(f"Erro ao inserir estoque: {e}")
return Resultado(sucesso=False, mensagem=str(e))
def insere_erro(self, dados:ErroHolmes) -> Resultado:
try:
logging.debug(f"Inserindo erro: proposta {dados.empresa}{dados.revenda}{dados.proposta_apollo}")
cursor = self.connection.cursor()
cursor.execute(
insert_erro_sql,
(
dados.empresa,
dados.revenda,
dados.proposta_apollo,
dados.motivo_erro,
),
)
self.connection.commit()
logging.debug(f"Erro inserido com sucesso: proposta {dados.empresa}{dados.revenda}{dados.proposta_apollo}")
return Resultado(sucesso=True, mensagem='Sucesso chefia')
except DatabaseError as e:
logging.error(f"Erro SQL Server ao inserir erro: {e}")
return Resultado(sucesso=False, mensagem=str(e))
except Exception as e:
logging.error(f"Erro ao inserir erro: {e}")
return Resultado(sucesso=False, mensagem=str(e))

188
src/repositories/queries.py Normal file
View file

@ -0,0 +1,188 @@
busca_propostas = r"""
WITH base AS (
SELECT
vp.dta_emissao,
VP.EMPRESA,
VP.REVENDA,
HE.ID_HOLMES "EMPRESA_HOLMES",
CASE
WHEN FC.FISJUR = 'F' THEN '6a6c9fc01e646b3f0aa51275'
WHEN FC.FISJUR = 'J' THEN '6a6c9fc614dccd2513dd11dc'
END TIPO_PESSOA,
CASE
WHEN VAP.PROPOSTA IS NULL THEN '6a6c9c41f8b8f070a4753db9' -- Não
ELSE '6a6c9c39dbf5173588753b46' -- Sim
END USADO_NA_TROCA,
CASE
WHEN MAX(CASE WHEN vpg.DES_PAGAMENTO LIKE '%FINANCIAMENTO%' THEN 1 ELSE 0 END) = 1
THEN '6a6cbc7023ac8f4423ac37a8'
ELSE '6a6cbc79609495f4cc5deb92'
END FORMA_PAGAMENTO,
vp.OBSERVACAO,
CASE WHEN vp.TIPO_VENDA = 'D' THEN MAX(c.chassi_obs) END AS CHASSI_OBS,
VP.PROPOSTA,
vv.VEICULO,
vv.CHASSI,
ofs.placa,
vm.DES_MODELO,
SUM(vpg.VAL_PAGAMENTO) VAL_PROPOSTA,
fc.NOME,
COALESCE(
RIGHT(REPLICATE('0', 11) + CAST(fpf.CPF AS VARCHAR(20)), 11),
LTRIM(RTRIM(fpj.CGC))
) AS CPFCNPJ,
CONCAT(fc.DDD_CELULAR,' ',fc.CELULAR) CELULAR,
COALESCE(fc.E_MAIL_CASA , fc.E_MAIL_TRABALHO) EMAIL,
hu.ID_HOLMES VENDEDOR,
vv.NOVO_USADO,
vp.TIPO_VENDA,
vv.SITUACAO
FROM [apollo-podium-db].[dbo].VEI_PROPOSTA vp
/* ---------- extração do chassi da OBSERVACAO (T-SQL puro) ---------- */
OUTER APPLY (SELECT t = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
vp.OBSERVACAO, CHAR(13),' '), CHAR(10),' '), CHAR(9),' '), ':',' '), ';',' ')) x1
OUTER APPLY (SELECT t = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
x1.t, '/',' '), '\',' '), '.',' '), ',',' '), '-',' ')) x2
OUTER APPLY (SELECT t = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
x2.t, '(',' '), ')',' '), '*',' '), '#',' '), '+',' ')) x3
OUTER APPLY (SELECT t = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
x3.t, '_',' '), '=',' '), '|',' '), '<',' '), '>',' ')) x4
OUTER APPLY (SELECT t = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
x4.t, '!',' '), '?',' '), '%',' '), '$',' '), '&',' ')) x5
OUTER APPLY (SELECT txt = ' ' + UPPER(REPLACE(REPLACE(x5.t, '"',' '), CHAR(39),' ')) + ' ') o
OUTER APPLY (
SELECT pos = CASE
WHEN CHARINDEX('CHASSIS', o.txt) > 0 THEN CHARINDEX('CHASSIS', o.txt) + 7
WHEN CHARINDEX('CHASSI', o.txt) > 0 THEN CHARINDEX('CHASSI', o.txt) + 6
WHEN CHARINDEX('CHASS', o.txt) > 0 THEN CHARINDEX('CHASS', o.txt) + 5
WHEN PATINDEX('%[^A-Z0-9]CH[^A-Z0-9]%', o.txt) > 0
THEN PATINDEX('%[^A-Z0-9]CH[^A-Z0-9]%', o.txt) + 3
ELSE 0 END
) k
OUTER APPLY (SELECT rest = LTRIM(SUBSTRING(o.txt, k.pos, 60)) + ' ') r
OUTER APPLY (SELECT tok1 = LEFT(r.rest, CHARINDEX(' ', r.rest) - 1),
rest2 = LTRIM(SUBSTRING(r.rest, CHARINDEX(' ', r.rest) + 1, 60)) + ' ') a
OUTER APPLY (SELECT tok2 = LEFT(a.rest2, CHARINDEX(' ', a.rest2) - 1)) b
OUTER APPLY (SELECT solo = LTRIM(RTRIM(o.txt))) s
OUTER APPLY (
SELECT chassi_obs = CASE
-- sem âncora: observação inteira é um único token (ex.: "YKF6034")
WHEN k.pos = 0 THEN
CASE WHEN s.solo <> ''
AND s.solo NOT LIKE '%[^A-Z0-9]%'
AND LEN(s.solo) BETWEEN 5 AND 17
AND s.solo LIKE '%[0-9]%' THEN s.solo
ELSE NULL END
WHEN a.tok1 = '' OR a.tok1 LIKE '%[^A-Z0-9]%' THEN NULL
-- token curto + bloco numérico logo depois: "KX2 0241", "KX1 8232"
WHEN LEN(a.tok1) <= 4
AND b.tok2 <> ''
AND b.tok2 NOT LIKE '%[^0-9]%'
AND LEN(b.tok2) BETWEEN 3 AND 6 THEN a.tok1 + b.tok2
-- token único: 5 a 17 alfanuméricos, obrigatoriamente com dígito
WHEN LEN(a.tok1) BETWEEN 5 AND 17
AND a.tok1 LIKE '%[0-9]%' THEN a.tok1
ELSE NULL END
) c
LEFT JOIN [apollo-podium-db].[dbo].CAC_CONTATO CC ON VP.EMPRESA = CC.EMPRESA AND VP.REVENDA = CC.REVENDA AND VP.CONTATO = CC.CONTATO
LEFT JOIN [apollo-podium-db].[dbo].FAT_CLIENTE fc ON FC.CLIENTE = CC.CLIENTE
LEFT JOIN [apollo-podium-db].[dbo].FAT_PESSOA_FISICA fpf ON fpf.cliente = fc.cliente
LEFT JOIN [apollo-podium-db].[dbo].FAT_PESSOA_JURIDICA fpj ON fpj.cliente = fc.cliente
LEFT JOIN HOLMES_EMPRESAS HE ON HE.EMPRESA_APOLLO = VP.EMPRESA AND HE.REVENDA_APOLLO = VP.REVENDA
LEFT JOIN [apollo-podium-db].[dbo].FAT_VENDEDOR FV ON FV.VENDEDOR = VP.VENDEDOR AND FV.EMPRESA = VP.EMPRESA AND FV.REVENDA = VP.REVENDA
LEFT JOIN HOLMES_USUARIOS HU ON HU.USUARIO_APOLLO = FV.USUARIO
left join HOLMES_FAT_ESTOQUE hfe on hfe.empresa = vp.empresa and hfe.revenda = vp.revenda and hfe.proposta_apollo = vp.proposta
left join FAT_ESTOQUE_ERROS hee on hee.empresa = vp.empresa and hee.revenda = vp.revenda and hee.proposta_apollo = vp.proposta
/* ---------- veículo: código exato; senão chassi completo (17), igualdade pura. Pega o mais recente ---------- */
OUTER APPLY (
SELECT TOP 1 v.VEICULO, v.CHASSI, v.EMPRESA, v.MODELO, v.NOVO_USADO, v.SITUACAO
FROM [apollo-podium-db].[dbo].vei_veiculo v
WHERE v.empresa = vp.empresa
AND (
v.VEICULO = vp.VEICULO
OR (
LEN(c.chassi_obs) = 17
AND v.CHASSI = c.chassi_obs
AND v.SITUACAO IN ('TD','VD')
)
)
ORDER BY CASE WHEN v.VEICULO = vp.VEICULO THEN 0 ELSE 1 END,
v.DTA_ENTRADA DESC,
v.VEICULO DESC
) vv
LEFT JOIN [apollo-podium-db].[dbo].VEI_PAGAMENTO vpg ON vpg.EMPRESA = vp.EMPRESA AND vpg.REVENDA = vp.REVENDA AND vpg.PROPOSTA = vp.PROPOSTA
left join [apollo-podium-db].[dbo].VEI_MODELO vm on vm.EMPRESA = vv.EMPRESA and vm.MODELO = vv.MODELO
left join [apollo-podium-db].[dbo].VEI_NEGOCIACAO vneg on vneg.empresa = vp.empresa and vneg.revenda = vp.revenda and vneg.proposta = vp.proposta
OUTER APPLY (
SELECT TOP 1 x.placa
FROM [apollo-podium-db].[dbo].OFI_FICHA_SEGUIMENTO x
WHERE x.chassi = vv.chassi
AND x.placa IS NOT NULL
ORDER BY x.placa DESC
) ofs
LEFT JOIN (
SELECT EMPRESA, REVENDA, PROPOSTA
FROM [apollo-podium-db].[dbo].VEI_AVALIACAO_PROPOSTA
GROUP BY EMPRESA, REVENDA, PROPOSTA
) VAP
ON VAP.EMPRESA = vp.EMPRESA
AND VAP.REVENDA = vp.REVENDA
AND VAP.PROPOSTA = vp.PROPOSTA
WHERE
vp.situacao IN ('2','5','7') AND
vp.dta_emissao >= GETDATE() - 1 and
((vv.situacao in ('TM','TF','ES','IM') and vp.tipo_venda = 'N') or ((vv.situacao in ('TD','VD') or vv.situacao is null) and vp.tipo_venda = 'D')) and
hee.dta_insercao is null and
(hfe.dta_insercao is null or vneg.DTA_APR_GERENCIAL > hfe.DTA_ALTERACAO_STATUS)
GROUP BY
vp.situacao ,
vp.dta_emissao,
VP.EMPRESA,
VP.REVENDA,
HE.ID_HOLMES,
FC.FISJUR,
vp.OBSERVACAO,
VAP.PROPOSTA,
VP.PROPOSTA,
vv.VEICULO,
vv.CHASSI,
ofs.placa,
vm.DES_MODELO,
fc.NOME,
fpf.CPF,
fpj.CGC,
fc.DDD_CELULAR,
fc.CELULAR,
COALESCE(fc.E_MAIL_CASA , fc.E_MAIL_TRABALHO),
hu.ID_HOLMES,
vv.NOVO_USADO,
vp.TIPO_VENDA,
vv.SITUACAO
),
marcado AS (
SELECT b.*,
CASE WHEN b.CHASSI_OBS IS NULL THEN NULL
ELSE COUNT(*) OVER (PARTITION BY b.EMPRESA, b.CHASSI_OBS)
END AS QTD_CHASSI
FROM base b
)
SELECT
*
FROM
marcado
WHERE
CHASSI_OBS IS NULL OR
QTD_CHASSI = 1
"""
insere_estoque_sql = """
INSERT INTO HOLMES_FAT_ESTOQUE
(EMPRESA, REVENDA, PROPOSTA_APOLLO, ID_PROCESSO, DTA_INSERCAO)
VALUES (?, ?, ?, ?, GETDATE())
"""
insert_erro_sql = """
INSERT INTO FAT_ESTOQUE_ERROS
(EMPRESA, REVENDA, PROPOSTA_APOLLO, MOTIVO_ERRO, DTA_INSERCAO)
VALUES (?, ?, ?, ?, GETDATE())
"""

0
src/services/__init__.py Normal file
View file

View file

@ -0,0 +1,70 @@
import logging
from src.holmes.client import HolmesAPI
from src.models.erro_holmes import ErroHolmes
from src.repositories.estoque_repository import EstoqueRepository
def fat_estoque(conn):
logging.info("Iniciando faturamento de estoque")
estoqueRepo = EstoqueRepository(conn)
holmes = HolmesAPI()
propostas = estoqueRepo.buscar_propostas()
if not propostas:
logging.info("Nenhuma proposta pendente encontrada")
return False
logging.info("%d proposta(s) encontrada(s)", len(propostas))
for proposta in propostas:
logging.debug("Processando proposta %s (chassi=%s)", proposta.proposta_apollo, proposta.chassi)
if not proposta.vendedor_holmes:
logging.warning("Proposta %s sem vendedor_holmes, pulando", proposta.proposta_apollo)
estoqueRepo.insere_erro(
ErroHolmes(
empresa=proposta.empresa,
revenda=proposta.revenda,
proposta_apollo=proposta.proposta_apollo,
motivo_erro="Vendedor não cadastrado no Holmes"
)
)
continue
existe = holmes.existe_holmes(proposta.chassi, proposta.proposta_apollo)
if existe:
logging.warning("Proposta %s ja existe no Holmes, pulando", proposta.proposta_apollo)
estoqueRepo.insere_erro(
ErroHolmes(
empresa= proposta.empresa,
revenda = proposta.revenda,
proposta_apollo= proposta.proposta_apollo,
motivo_erro= "Processo já existe"
)
)
continue
if proposta.tipo_pessoa == 'O':
logging.warning(f"{proposta.empresa}.{proposta.revenda} {proposta.proposta_apollo} possui cliente O, favor alterar")
estoqueRepo.insere_erro(
ErroHolmes(
empresa= proposta.empresa,
revenda = proposta.revenda,
proposta_apollo= proposta.proposta_apollo,
motivo_erro= "Processo com cliente O"
)
)
continue
if proposta.tipo_venda == 'N' and proposta.situacao == 'IM':
resultado = holmes.envia_fat_estoque_imobilizado(proposta, estoqueRepo)
elif proposta.tipo_venda == 'N' and proposta.novo_usado == 'N' and proposta.situacao != 'IM':
resultado = holmes.envia_fat_estoque_novos(proposta, estoqueRepo)
elif proposta.tipo_venda == 'N' and proposta.novo_usado == 'U'and proposta.situacao != 'IM':
resultado = holmes.envia_fat_estoque_usado(proposta, estoqueRepo)
elif proposta.tipo_venda == 'D':
resultado = holmes.envia_fat_estoque_venda_direta(proposta, estoqueRepo)
if not resultado.sucesso:
logging.error("Proposta %s falhou: %s", proposta.proposta_apollo, resultado.mensagem)
logging.info("Faturamento de estoque finalizado")

0
src/utils/__init__.py Normal file
View file

39
src/utils/crypto.py Normal file
View file

@ -0,0 +1,39 @@
"""Cifra de Cesar para criptografar/descriptografar senhas."""
def caesar_encrypt(text: str, shift: int) -> str:
"""Criptografa o texto usando cifra de Cesar."""
result = []
for char in text:
if char.isalpha():
base = ord('A') if char.isupper() else ord('a')
result.append(chr((ord(char) - base + shift) % 26 + base))
elif char.isdigit():
result.append(str((int(char) + shift) % 10))
else:
result.append(char)
return ''.join(result)
def caesar_decrypt(text: str, shift: int) -> str:
"""Descriptografa o texto usando cifra de Cesar."""
return caesar_encrypt(text, -shift)
if __name__ == '__main__':
import sys
if len(sys.argv) < 3:
print("Uso: python crypto.py <senha> <deslocamento>")
print("Exemplo: python crypto.py MinhaSenha123 3")
sys.exit(1)
senha = sys.argv[1]
shift = int(sys.argv[2])
encrypted = caesar_encrypt(senha, shift)
print(f"Original: {senha}")
print(f"Criptografada: {encrypted}")
print(f"Deslocamento: {shift}")
print("\nColoque no .env:")
print(f"DB_PASS={encrypted}")
print(f"CAESAR_SHIFT={shift}")

15
src/utils/utils.py Normal file
View file

@ -0,0 +1,15 @@
def extrair_proposta(identifier: str | None) -> str | None:
"""
Extrai o numero da proposta do campo 'identifier'.
Retorna None se nao encontrar ou se identifier for None/vazio.
Ex.: "Chassi: XXX | Proposta: 1234 | Unidade: 99"
"""
if not identifier or not isinstance(identifier, str):
return None
for parte in identifier.split("|"):
parte = parte.strip()
if parte.lower().startswith("proposta:"):
valor = parte.split(":", 1)[1].strip()
return valor or None
return None