Versão 1.0
This commit is contained in:
commit
13bcea2077
26 changed files with 1546 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
.env
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.log
|
||||||
25
main.py
Normal file
25
main.py
Normal 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
2
pyproject.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[tool.pyright]
|
||||||
|
typeCheckingMode = "basic"
|
||||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
60
src/config/README.md
Normal file
60
src/config/README.md
Normal 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
0
src/config/__init__.py
Normal file
42
src/config/logging_config.py
Normal file
42
src/config/logging_config.py
Normal 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
0
src/config/settings.py
Normal file
0
src/database/__init__.py
Normal file
0
src/database/__init__.py
Normal file
73
src/database/connection.py
Normal file
73
src/database/connection.py
Normal 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
0
src/holmes/__init__.py
Normal file
842
src/holmes/client.py
Normal file
842
src/holmes/client.py
Normal 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
0
src/models/__init__.py
Normal file
8
src/models/erro_holmes.py
Normal file
8
src/models/erro_holmes.py
Normal 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}
|
||||||
9
src/models/estoque_homes.py
Normal file
9
src/models/estoque_homes.py
Normal 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
69
src/models/proposta.py
Normal 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
5
src/models/resultado.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Resultado(BaseModel):
|
||||||
|
sucesso: bool
|
||||||
|
mensagem: str
|
||||||
0
src/repositories/__init__.py
Normal file
0
src/repositories/__init__.py
Normal file
93
src/repositories/estoque_repository.py
Normal file
93
src/repositories/estoque_repository.py
Normal 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
188
src/repositories/queries.py
Normal 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
0
src/services/__init__.py
Normal file
70
src/services/fat_estoque.py
Normal file
70
src/services/fat_estoque.py
Normal 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
0
src/utils/__init__.py
Normal file
39
src/utils/crypto.py
Normal file
39
src/utils/crypto.py
Normal 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
15
src/utils/utils.py
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue