From 13bcea207723b981a0b04a1cbbfee5d8936f165c Mon Sep 17 00:00:00 2001 From: Ricardo Date: Mon, 14 Sep 2026 10:13:17 -0300 Subject: [PATCH] =?UTF-8?q?Vers=C3=A3o=201.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + main.py | 25 + pyproject.toml | 2 + requirements.txt | Bin 0 -> 732 bytes src/__init__.py | 0 src/config/README.md | 60 ++ src/config/__init__.py | 0 src/config/logging_config.py | 42 ++ src/config/settings.py | 0 src/database/__init__.py | 0 src/database/connection.py | 73 +++ src/holmes/__init__.py | 0 src/holmes/client.py | 842 +++++++++++++++++++++++++ src/models/__init__.py | 0 src/models/erro_holmes.py | 8 + src/models/estoque_homes.py | 9 + src/models/proposta.py | 69 ++ src/models/resultado.py | 5 + src/repositories/__init__.py | 0 src/repositories/estoque_repository.py | 93 +++ src/repositories/queries.py | 188 ++++++ src/services/__init__.py | 0 src/services/fat_estoque.py | 70 ++ src/utils/__init__.py | 0 src/utils/crypto.py | 39 ++ src/utils/utils.py | 15 + 26 files changed, 1546 insertions(+) create mode 100644 .gitignore create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/config/README.md create mode 100644 src/config/__init__.py create mode 100644 src/config/logging_config.py create mode 100644 src/config/settings.py create mode 100644 src/database/__init__.py create mode 100644 src/database/connection.py create mode 100644 src/holmes/__init__.py create mode 100644 src/holmes/client.py create mode 100644 src/models/__init__.py create mode 100644 src/models/erro_holmes.py create mode 100644 src/models/estoque_homes.py create mode 100644 src/models/proposta.py create mode 100644 src/models/resultado.py create mode 100644 src/repositories/__init__.py create mode 100644 src/repositories/estoque_repository.py create mode 100644 src/repositories/queries.py create mode 100644 src/services/__init__.py create mode 100644 src/services/fat_estoque.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/crypto.py create mode 100644 src/utils/utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c204ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +.venv/ +__pycache__/ +*.pyc +*.pyo +*.log \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..6a7b63f --- /dev/null +++ b/main.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f80a45f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.pyright] +typeCheckingMode = "basic" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3d75222fedbe176a6e008641113c661f10a96345 GIT binary patch literal 732 zcmY+C>rR7E5QO))iI0LQl-BseXK5(VORRv`%BkSz^6J2N}y{QeZy@U(Wd zf~u{ug!h+q{2%O@XJrreNUiMN?yTf{58`YGO8tyZW?K-M*Ji{W76?tx3@5aD*lTza zSMTkWZx{ z2XQPhI~b*2Pv{(c+*s(=T?e)e9pyLcT}i7&F#)C1p3@OaI60*wrVlb|n9oMvF(IkH z*^S=;hHyJHDP(jK?vCb 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() diff --git a/src/holmes/__init__.py b/src/holmes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/holmes/client.py b/src/holmes/client.py new file mode 100644 index 0000000..990b145 --- /dev/null +++ b/src/holmes/client.py @@ -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)) diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/models/erro_holmes.py b/src/models/erro_holmes.py new file mode 100644 index 0000000..dca7857 --- /dev/null +++ b/src/models/erro_holmes.py @@ -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} diff --git a/src/models/estoque_homes.py b/src/models/estoque_homes.py new file mode 100644 index 0000000..41deca3 --- /dev/null +++ b/src/models/estoque_homes.py @@ -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 diff --git a/src/models/proposta.py b/src/models/proposta.py new file mode 100644 index 0000000..98a1a92 --- /dev/null +++ b/src/models/proposta.py @@ -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, + } \ No newline at end of file diff --git a/src/models/resultado.py b/src/models/resultado.py new file mode 100644 index 0000000..7bc06ce --- /dev/null +++ b/src/models/resultado.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class Resultado(BaseModel): + sucesso: bool + mensagem: str diff --git a/src/repositories/__init__.py b/src/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/repositories/estoque_repository.py b/src/repositories/estoque_repository.py new file mode 100644 index 0000000..a5e8eca --- /dev/null +++ b/src/repositories/estoque_repository.py @@ -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)) \ No newline at end of file diff --git a/src/repositories/queries.py b/src/repositories/queries.py new file mode 100644 index 0000000..e2a7480 --- /dev/null +++ b/src/repositories/queries.py @@ -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()) +""" diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/fat_estoque.py b/src/services/fat_estoque.py new file mode 100644 index 0000000..7f0add2 --- /dev/null +++ b/src/services/fat_estoque.py @@ -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") \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/crypto.py b/src/utils/crypto.py new file mode 100644 index 0000000..9c31941 --- /dev/null +++ b/src/utils/crypto.py @@ -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 ") + 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}") diff --git a/src/utils/utils.py b/src/utils/utils.py new file mode 100644 index 0000000..7465816 --- /dev/null +++ b/src/utils/utils.py @@ -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 \ No newline at end of file