1427 lines
61 KiB
Python
1427 lines
61 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Script para acessar a API do Grupo Boticário - Sale Receipts
|
||
Busca recibos de vendas por data específica
|
||
"""
|
||
|
||
import requests
|
||
import pyodbc
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
import threading
|
||
import time
|
||
|
||
# ==============================
|
||
# Configurações de paralelismo
|
||
# ==============================
|
||
MAX_WORKERS = 5 # Número de requisições paralelas
|
||
PAGE_SIZE = 50 # Itens por página (padrão da API)
|
||
MAX_RETRIES = 5 # Máximo de tentativas por página
|
||
RETRY_DELAY = 5 # Segundos entre tentativas
|
||
FINAL_RETRY_DELAY = 30 # Segundos entre rodadas de retry final
|
||
|
||
# Lock para print thread-safe
|
||
print_lock = threading.Lock()
|
||
|
||
|
||
class BoticarioAPI:
|
||
"""Cliente para acessar a API do Grupo Boticário"""
|
||
|
||
def __init__(self, bearer_token: str):
|
||
"""
|
||
Inicializa o cliente da API
|
||
|
||
Args:
|
||
bearer_token: Token de autenticação Bearer
|
||
"""
|
||
self.base_url = "https://api.grupoboticario.com.br"
|
||
self.bearer_token = bearer_token
|
||
self.headers = {
|
||
"Authorization": f"Bearer {bearer_token}",
|
||
"Content-Type": "application/json",
|
||
"Accept": "application/json"
|
||
}
|
||
self.token_lock = threading.Lock()
|
||
|
||
def refresh_token(self):
|
||
"""Renova o token de autenticação buscando do banco de dados"""
|
||
with self.token_lock:
|
||
print("\n🔄 Renovando token de autenticação...")
|
||
new_token = get_bearer_token_from_database()
|
||
if new_token:
|
||
self.bearer_token = new_token
|
||
self.headers["Authorization"] = f"Bearer {new_token}"
|
||
print("✅ Token renovado com sucesso!")
|
||
return True
|
||
else:
|
||
print("❌ Falha ao renovar token")
|
||
return False
|
||
|
||
def update_token(self, new_token: str):
|
||
"""Atualiza o token manualmente"""
|
||
with self.token_lock:
|
||
self.bearer_token = new_token
|
||
self.headers["Authorization"] = f"Bearer {new_token}"
|
||
|
||
def get_sale_receipts_page(self, sale_date: str, start: int = 0, silent: bool = False) -> Optional[Dict]:
|
||
"""
|
||
Busca uma página de recibos de vendas por data
|
||
|
||
Args:
|
||
sale_date: Data da venda no formato YYYY-MM-DD
|
||
start: Índice inicial para paginação (padrão: 0)
|
||
silent: Se True, não imprime logs (para uso em paralelo)
|
||
|
||
Returns:
|
||
Resposta da API ou None em caso de erro
|
||
Retorna {"token_expired": True} se o token expirou
|
||
"""
|
||
endpoint = "/global/v1/franchising/gb-stores-data/sale/receipts"
|
||
url = f"{self.base_url}{endpoint}"
|
||
|
||
params = {
|
||
"receipt.saleDate": sale_date,
|
||
"start": start
|
||
}
|
||
|
||
try:
|
||
if not silent:
|
||
print(f"Fazendo requisição para: {url}")
|
||
print(f"Parâmetros: {params}")
|
||
|
||
response = requests.get(
|
||
url=url,
|
||
headers=self.headers,
|
||
params=params,
|
||
timeout=60
|
||
)
|
||
|
||
if not silent:
|
||
print(f"Status Code: {response.status_code}")
|
||
|
||
if response.status_code == 200:
|
||
return response.json()
|
||
elif response.status_code in [401, 403]:
|
||
# Token expirado ou inválido
|
||
if not silent:
|
||
print(f"⚠️ Token expirado ou inválido (HTTP {response.status_code})")
|
||
return {"token_expired": True}
|
||
else:
|
||
if not silent:
|
||
print(f"Erro na requisição: {response.status_code}")
|
||
print(f"Resposta: {response.text}")
|
||
return None
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
if not silent:
|
||
print(f"Erro na requisição: {e}")
|
||
return None
|
||
|
||
def fetch_page_with_retry(self, sale_date: str, start: int, total_pages: int, page_num: int) -> Dict:
|
||
"""
|
||
Busca uma página específica com retry e retorna resultado
|
||
|
||
Args:
|
||
sale_date: Data da venda
|
||
start: Índice inicial
|
||
total_pages: Total de páginas (para log)
|
||
page_num: Número da página atual (para log)
|
||
|
||
Returns:
|
||
Dict com 'success', 'start', 'items', 'token_expired'
|
||
"""
|
||
for attempt in range(1, MAX_RETRIES + 1):
|
||
try:
|
||
result = self.get_sale_receipts_page(sale_date, start=start, silent=True)
|
||
|
||
# Verificar se token expirou
|
||
if result and result.get('token_expired'):
|
||
with print_lock:
|
||
print(f" ⚠️ Página {page_num} (start={start}): Token expirado!")
|
||
return {"success": False, "start": start, "items": [], "token_expired": True}
|
||
|
||
if result and result.get('items') is not None:
|
||
items = result.get('items', [])
|
||
|
||
with print_lock:
|
||
print(f" ✓ Página {page_num}/{total_pages}: {len(items)} recibos (start={start})")
|
||
|
||
return {"success": True, "start": start, "items": items, "token_expired": False}
|
||
else:
|
||
with print_lock:
|
||
print(f" ✗ Página {page_num} (start={start}): Erro - Tentativa {attempt}/{MAX_RETRIES}")
|
||
|
||
except Exception as e:
|
||
with print_lock:
|
||
print(f" ✗ Página {page_num} (start={start}): {str(e)[:50]} - Tentativa {attempt}/{MAX_RETRIES}")
|
||
|
||
if attempt < MAX_RETRIES:
|
||
time.sleep(RETRY_DELAY)
|
||
|
||
return {"success": False, "start": start, "items": [], "token_expired": False}
|
||
|
||
def get_all_sale_receipts(self, sale_date: str) -> Optional[Dict]:
|
||
"""
|
||
Busca TODOS os recibos de vendas por data usando requisições PARALELAS
|
||
Com renovação automática de token quando expirar
|
||
|
||
Args:
|
||
sale_date: Data da venda no formato YYYY-MM-DD
|
||
|
||
Returns:
|
||
Dicionário com todos os recibos consolidados ou None em caso de erro
|
||
"""
|
||
print(f"🔍 Iniciando busca paralela de recibos para {sale_date}")
|
||
|
||
# Primeira requisição para descobrir o total
|
||
first_page = self.get_sale_receipts_page(sale_date, start=0)
|
||
|
||
# Se token expirou na primeira requisição, renovar e tentar novamente
|
||
if first_page and first_page.get('token_expired'):
|
||
print("⚠️ Token expirado na primeira requisição. Renovando...")
|
||
if self.refresh_token():
|
||
first_page = self.get_sale_receipts_page(sale_date, start=0)
|
||
else:
|
||
print("❌ Falha ao renovar token")
|
||
return None
|
||
|
||
if not first_page:
|
||
print("❌ Erro na primeira requisição")
|
||
return None
|
||
|
||
# Extrair informações de paginação
|
||
total = first_page.get('total', 0)
|
||
first_items = first_page.get('items', [])
|
||
page_size = len(first_items) if first_items else PAGE_SIZE
|
||
|
||
print(f"📊 Informações iniciais:")
|
||
print(f" - Total de recibos: {total}")
|
||
print(f" - Itens por página: {page_size}")
|
||
|
||
# Se já temos todos os registros, retornar
|
||
if len(first_items) >= total:
|
||
print("✅ Todos os registros obtidos na primeira requisição")
|
||
return first_page
|
||
|
||
# Calcular páginas necessárias
|
||
total_pages = (total + page_size - 1) // page_size
|
||
starts = [i * page_size for i in range(total_pages)]
|
||
|
||
print(f" - Total de páginas: {total_pages}")
|
||
print(f" - Requisições paralelas: {MAX_WORKERS}")
|
||
print(f"\n📥 Iniciando download paralelo...")
|
||
|
||
# Coletar todos os items
|
||
all_items = []
|
||
failed_pages = []
|
||
token_expired_detected = False
|
||
|
||
# Buscar páginas em paralelo
|
||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||
futures = {
|
||
executor.submit(
|
||
self.fetch_page_with_retry,
|
||
sale_date,
|
||
start,
|
||
total_pages,
|
||
(start // page_size) + 1
|
||
): start for start in starts
|
||
}
|
||
|
||
for future in as_completed(futures):
|
||
result = future.result()
|
||
|
||
if result["success"]:
|
||
all_items.extend(result["items"])
|
||
else:
|
||
failed_pages.append(result["start"])
|
||
if result.get("token_expired"):
|
||
token_expired_detected = True
|
||
|
||
# Se detectou token expirado, renovar e reprocessar páginas que falharam
|
||
if token_expired_detected and failed_pages:
|
||
print("\n🔄 Token expirado detectado. Renovando token...")
|
||
if self.refresh_token():
|
||
print(f"✅ Token renovado. Reprocessando {len(failed_pages)} páginas...")
|
||
|
||
# Retry para páginas que falharam (com token renovado se necessário)
|
||
retry_round = 0
|
||
max_retry_rounds = 10 # Limite de rodadas de retry
|
||
while failed_pages and retry_round < max_retry_rounds:
|
||
retry_round += 1
|
||
print(f"\n ⚠ Rodada de retry #{retry_round}: {len(failed_pages)} páginas falharam")
|
||
print(f" Aguardando {FINAL_RETRY_DELAY} segundos...")
|
||
time.sleep(FINAL_RETRY_DELAY)
|
||
|
||
still_failed = []
|
||
token_expired_in_retry = False
|
||
|
||
for start in failed_pages:
|
||
page_num = (start // page_size) + 1
|
||
result = self.fetch_page_with_retry(sale_date, start, total_pages, page_num)
|
||
if result["success"]:
|
||
all_items.extend(result["items"])
|
||
else:
|
||
still_failed.append(start)
|
||
if result.get("token_expired"):
|
||
token_expired_in_retry = True
|
||
|
||
# Se token expirou durante retry, renovar
|
||
if token_expired_in_retry and still_failed:
|
||
print("\n🔄 Token expirou durante retry. Renovando...")
|
||
self.refresh_token()
|
||
|
||
failed_pages = still_failed
|
||
|
||
if failed_pages:
|
||
print(f" Ainda restam {len(failed_pages)} páginas com falha. Tentando novamente...")
|
||
|
||
# Criar resposta consolidada
|
||
consolidated_response = {
|
||
"start": 0,
|
||
"count": len(all_items),
|
||
"total": total,
|
||
"items": all_items
|
||
}
|
||
|
||
print(f"\n🎉 Download paralelo concluído!")
|
||
print(f" - Total de registros obtidos: {len(all_items)}")
|
||
print(f" - Total esperado: {total}")
|
||
print(f" - Sucesso: {'✅' if len(all_items) == total else '⚠️'}")
|
||
|
||
if failed_pages:
|
||
print(f" - ⚠️ Páginas não recuperadas: {len(failed_pages)}")
|
||
|
||
return consolidated_response
|
||
|
||
|
||
|
||
def connect_to_database(self):
|
||
"""
|
||
Conecta ao banco de dados SQL Server
|
||
|
||
Returns:
|
||
Conexão pyodbc ou None em caso de erro
|
||
"""
|
||
# Lista de drivers para tentar em ordem de preferência
|
||
drivers = [
|
||
'ODBC Driver 18 for SQL Server',
|
||
'ODBC Driver 17 for SQL Server',
|
||
'ODBC Driver 13 for SQL Server',
|
||
'ODBC Driver 11 for SQL Server',
|
||
'SQL Server Native Client 11.0',
|
||
'SQL Server Native Client 10.0',
|
||
'SQL Server'
|
||
]
|
||
|
||
print("🔍 Verificando drivers ODBC disponíveis...")
|
||
available_drivers = pyodbc.drivers()
|
||
print(f"Drivers encontrados: {available_drivers}")
|
||
|
||
for driver in drivers:
|
||
if driver in available_drivers:
|
||
print(f"🔄 Tentando conectar com driver: {driver}")
|
||
try:
|
||
connection_string = (
|
||
f'DRIVER={{{driver}}};'
|
||
'SERVER=10.77.77.10;'
|
||
'DATABASE=GINSENG;'
|
||
'UID=supginseng;'
|
||
'PWD=Ginseng@;'
|
||
'PORT=1433;'
|
||
'TrustServerCertificate=yes;'
|
||
'Encrypt=yes'
|
||
)
|
||
|
||
conn = pyodbc.connect(connection_string)
|
||
print(f"✅ Conexão estabelecida com sucesso usando: {driver}")
|
||
return conn
|
||
|
||
except Exception as e:
|
||
print(f"❌ Falha com {driver}: {e}")
|
||
continue
|
||
|
||
# Se nenhum driver funcionou, tentar sem TrustServerCertificate e Encrypt
|
||
print("🔄 Tentando conexão sem SSL...")
|
||
for driver in drivers:
|
||
if driver in available_drivers:
|
||
try:
|
||
connection_string = (
|
||
f'DRIVER={{{driver}}};'
|
||
'SERVER=10.77.77.10;'
|
||
'DATABASE=GINSENG;'
|
||
'UID=supginseng;'
|
||
'PWD=Ginseng@;'
|
||
'PORT=1433'
|
||
)
|
||
|
||
conn = pyodbc.connect(connection_string)
|
||
print(f"✅ Conexão estabelecida sem SSL usando: {driver}")
|
||
return conn
|
||
|
||
except Exception as e:
|
||
print(f"❌ Falha sem SSL com {driver}: {e}")
|
||
continue
|
||
|
||
print("❌ Não foi possível conectar com nenhum driver disponível")
|
||
print("💡 Sugestão: Instale o Microsoft ODBC Driver for SQL Server")
|
||
print(" Download: https://docs.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server")
|
||
return None
|
||
|
||
def check_and_install_odbc_driver(self):
|
||
"""
|
||
Verifica se há drivers ODBC disponíveis e fornece instruções de instalação
|
||
"""
|
||
print("\n🔧 DIAGNÓSTICO DE DRIVERS ODBC")
|
||
print("="*40)
|
||
|
||
available_drivers = pyodbc.drivers()
|
||
print(f"Drivers ODBC encontrados: {len(available_drivers)}")
|
||
|
||
if available_drivers:
|
||
for i, driver in enumerate(available_drivers, 1):
|
||
print(f" {i}. {driver}")
|
||
else:
|
||
print(" ❌ Nenhum driver ODBC encontrado!")
|
||
|
||
# Verificar se há drivers SQL Server específicos
|
||
sql_server_drivers = [d for d in available_drivers if 'SQL Server' in d]
|
||
|
||
if not sql_server_drivers:
|
||
print("\n❌ Nenhum driver SQL Server encontrado!")
|
||
print("\n💡 SOLUÇÕES:")
|
||
print("1. Instalar Microsoft ODBC Driver 18 for SQL Server:")
|
||
print(" https://go.microsoft.com/fwlink/?linkid=2249006")
|
||
print("\n2. Ou executar no PowerShell como Administrador:")
|
||
print(" winget install Microsoft.ODBCDriver.18.SQLServer")
|
||
print("\n3. Ou baixar manualmente:")
|
||
print(" https://docs.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server")
|
||
return False
|
||
else:
|
||
print(f"\n✅ Encontrados {len(sql_server_drivers)} driver(s) SQL Server:")
|
||
for driver in sql_server_drivers:
|
||
print(f" • {driver}")
|
||
return True
|
||
|
||
def insert_receipts_to_database(self, receipts_data: Dict, skip_duplicates: bool = True) -> bool:
|
||
"""
|
||
Insere os dados dos recibos no banco de dados SQL Server
|
||
|
||
Args:
|
||
receipts_data: Dados dos recibos retornados pela API
|
||
skip_duplicates: Se True, pula registros duplicados; se False, tenta inserir todos
|
||
|
||
Returns:
|
||
True se inserção foi bem-sucedida, False caso contrário
|
||
"""
|
||
conn = self.connect_to_database()
|
||
if not conn:
|
||
return False
|
||
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
# Detectar a data de venda dos dados
|
||
sale_date = self.get_sale_date_from_data(receipts_data)
|
||
|
||
if not sale_date:
|
||
print("❌ Não foi possível detectar a data de venda. Abortando inserção.")
|
||
return False
|
||
|
||
# Deletar dados existentes para essa data
|
||
print(f"\n🧹 LIMPEZA DE DADOS EXISTENTES")
|
||
print("="*40)
|
||
delete_success = self.delete_existing_data_by_date(sale_date)
|
||
|
||
if not delete_success:
|
||
print("❌ Falha ao deletar dados existentes. Abortando inserção.")
|
||
return False
|
||
|
||
# Obter lista de items
|
||
items = receipts_data.get('items', [])
|
||
total_records = 0
|
||
batch_size = 1000
|
||
|
||
print(f"\n🔄 INICIANDO INSERÇÃO DE NOVOS DADOS")
|
||
print("="*40)
|
||
print(f"📅 Data: {sale_date}")
|
||
print(f"📊 Recibos para processar: {len(items)}")
|
||
print(f"📦 Processando em lotes de {batch_size} registros")
|
||
if skip_duplicates:
|
||
print("⚠️ Modo: Pular registros duplicados")
|
||
else:
|
||
print("🔄 Modo: Inserir todos os registros")
|
||
|
||
# Processar em lotes usando inserção em massa
|
||
total_batches = (len(items) + batch_size - 1) // batch_size
|
||
|
||
for batch_num in range(total_batches):
|
||
start_idx = batch_num * batch_size
|
||
end_idx = min(start_idx + batch_size, len(items))
|
||
batch_items = items[start_idx:end_idx]
|
||
|
||
print(f"\n📦 Processando lote {batch_num + 1}/{total_batches} ({len(batch_items)} recibos)")
|
||
|
||
try:
|
||
# Preparar dados do lote para inserção em massa
|
||
batch_values = self._prepare_batch_data(batch_items)
|
||
|
||
if batch_values:
|
||
# Preparar SQL para inserção em massa
|
||
fields = [
|
||
# Campos do cupom (29 campos)
|
||
'cupomId', 'cupomReceiptSequence', 'cupomCashRegisterNumber', 'cupomStoreId', 'cupomCoo',
|
||
'cupomEmployeeId', 'cupomEmployeeName', 'cupomValue', 'cupomAdditionalValue', 'cupomDiscountValue',
|
||
'cupomItemsQuantity', 'cupomUnitsQuantity', 'cupomCancelledItemsQuantity', 'cupomCancelledItemsValue',
|
||
'cupomSaleType', 'cupomCancelledUnitsQuantity', 'cupomSaleDate', 'cupomInvoiceXMLStatus',
|
||
'cupomReceiptOpeningDateTime', 'cupomReceiptClosingDateTime', 'cupomEletronicKey', 'cupomSaleOrderId',
|
||
'cupomExternalId', 'cupomDiscountReason', 'cupomLoyaltyDiscountValue', 'cupomCancellingReason',
|
||
'cupomCancelledReceiptSequence', 'cupomChannel', 'cupomChannelDescription',
|
||
# Campos do item (27 campos)
|
||
'itemId', 'itemCancelled', 'itemProductId', 'itemSellerId', 'itemSellerName', 'itemQuantity',
|
||
'itemUnitValue', 'itemGrossValue', 'itemAdditionalValue', 'itemDiscountValue', 'itemTotalValue',
|
||
'itemTabelaA', 'itemNcm', 'itemNcmExcecao', 'itemNatureza', 'itemCfop', 'itemCsosn',
|
||
'itemCstICMS', 'itemAliquotaICMS', 'itemValorReducaoAliquotaICMS', 'itemValorICMSDesonerado',
|
||
'itemValorFecop', 'itemAliquotaFecop', 'itemCstPIS', 'itemAliquotaPIS', 'itemCstCOFINS', 'itemAliquotaCOFINS',
|
||
# Campos do payment (16 campos)
|
||
'paymentId', 'paymentMethodId', 'paymentMethodDescription', 'paymentValue', 'paymentChange',
|
||
'paymentInstallmentQuantity', 'paymentCheckIssuer', 'paymentCardAuthorization', 'paymentCardFlag',
|
||
'paymentCardFlagDescription', 'paymentCardModality', 'paymentRedeAdquirente', 'paymentNsu',
|
||
'paymentAuthorizationNsu', 'paymentNsuCancelling', 'paymentCardBinNumber',
|
||
# Campos do tef (12 campos)
|
||
'teftransId', 'teftransSequential', 'teftransPaymentMethodDescription', 'teftransValue',
|
||
'teftransCardModality', 'teftransCancellingModality', 'teftransCardType', 'teftranSitefNsu',
|
||
'teftransAuthorizerHostNsu', 'teftransAuthorizationCode', 'teftransInstallmentQuantity', 'teftransFirstIntallmentDate'
|
||
]
|
||
|
||
fields_str = ', '.join([f'[{field}]' for field in fields])
|
||
placeholders = ', '.join(['?'] * len(fields))
|
||
|
||
sql = f"""
|
||
INSERT INTO [GINSENG].[dbo].[rgb_sale_receipts] (
|
||
{fields_str}
|
||
) VALUES (
|
||
{placeholders}
|
||
)
|
||
"""
|
||
|
||
# Inserção em massa usando executemany
|
||
print(f" 🚀 Inserindo {len(batch_values)} registros em massa...")
|
||
cursor.executemany(sql, batch_values)
|
||
|
||
batch_inserted = len(batch_values)
|
||
total_records += batch_inserted
|
||
|
||
print(f" ✅ Lote {batch_num + 1} concluído: {batch_inserted} registros inseridos")
|
||
else:
|
||
print(f" ⚠️ Lote {batch_num + 1} vazio - nenhum registro para inserir")
|
||
|
||
except Exception as e:
|
||
print(f" ❌ Erro no lote {batch_num + 1}: {e}")
|
||
if not skip_duplicates:
|
||
# Se não está pulando duplicatas, continuar com próximo lote
|
||
continue
|
||
else:
|
||
raise e
|
||
|
||
# Commit do lote
|
||
conn.commit()
|
||
|
||
print(f"\n🎉 INSERÇÃO EM LOTES COMPLETA!")
|
||
print(f" 📊 Total de registros inseridos: {total_records}")
|
||
print(f" 📈 Total de recibos processados: {len(items)}")
|
||
print(f" <20> Total de lotes processados: {total_batches}")
|
||
print(f" ⚡ Inserção otimizada em lotes de {batch_size} registros")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"❌ Erro durante inserção no banco: {e}")
|
||
conn.rollback()
|
||
return False
|
||
finally:
|
||
conn.close()
|
||
print("🔌 Conexão com banco de dados fechada")
|
||
|
||
def delete_existing_data_by_date(self, sale_date: str) -> bool:
|
||
"""
|
||
Deleta todos os registros existentes para uma data específica
|
||
|
||
Args:
|
||
sale_date: Data da venda no formato YYYY-MM-DD
|
||
|
||
Returns:
|
||
True se deleção foi bem-sucedida, False caso contrário
|
||
"""
|
||
conn = self.connect_to_database()
|
||
if not conn:
|
||
return False
|
||
|
||
try:
|
||
cursor = conn.cursor()
|
||
|
||
# Primeiro, verificar quantos registros existem para essa data
|
||
count_sql = """
|
||
SELECT COUNT(*) FROM [GINSENG].[dbo].[rgb_sale_receipts]
|
||
WHERE [cupomSaleDate] = ?
|
||
"""
|
||
cursor.execute(count_sql, (sale_date,))
|
||
existing_count = cursor.fetchone()[0]
|
||
|
||
if existing_count > 0:
|
||
print(f"🗑️ Encontrados {existing_count} registros existentes para a data {sale_date}")
|
||
print(f"🔄 Deletando registros antigos...")
|
||
|
||
# Deletar registros existentes
|
||
delete_sql = """
|
||
DELETE FROM [GINSENG].[dbo].[rgb_sale_receipts]
|
||
WHERE [cupomSaleDate] = ?
|
||
"""
|
||
cursor.execute(delete_sql, (sale_date,))
|
||
deleted_count = cursor.rowcount
|
||
|
||
# Commit da deleção
|
||
conn.commit()
|
||
|
||
print(f"✅ {deleted_count} registros deletados com sucesso!")
|
||
return True
|
||
else:
|
||
print(f"ℹ️ Nenhum registro existente encontrado para a data {sale_date}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"❌ Erro ao deletar dados existentes: {e}")
|
||
conn.rollback()
|
||
return False
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_sale_date_from_data(self, receipts_data: Dict) -> str:
|
||
"""
|
||
Extrai e valida a data de venda dos dados dos recibos
|
||
|
||
Args:
|
||
receipts_data: Dados dos recibos retornados pela API
|
||
|
||
Returns:
|
||
Data da venda no formato YYYY-MM-DD ou None se não encontrada
|
||
"""
|
||
items = receipts_data.get('items', [])
|
||
|
||
if not items:
|
||
print("⚠️ Nenhum recibo encontrado nos dados")
|
||
return None
|
||
|
||
# Pegar a data do primeiro recibo
|
||
first_receipt = items[0]
|
||
sale_date = first_receipt.get('saleDate')
|
||
|
||
if not sale_date:
|
||
print("⚠️ Não foi possível detectar a data de venda dos dados")
|
||
return None
|
||
|
||
# Validar se todos os recibos são da mesma data
|
||
different_dates = []
|
||
for i, receipt in enumerate(items):
|
||
receipt_date = receipt.get('saleDate')
|
||
if receipt_date != sale_date:
|
||
different_dates.append(f"Recibo {i+1}: {receipt_date}")
|
||
|
||
if different_dates:
|
||
print(f"⚠️ ATENÇÃO: Encontradas datas diferentes nos recibos!")
|
||
print(f" Data principal: {sale_date}")
|
||
for diff_date in different_dates[:5]: # Mostrar apenas os primeiros 5
|
||
print(f" {diff_date}")
|
||
if len(different_dates) > 5:
|
||
print(f" ... e mais {len(different_dates) - 5} recibos com datas diferentes")
|
||
|
||
print(f"🔄 Usando data principal: {sale_date}")
|
||
|
||
print(f"📅 Data de venda detectada: {sale_date}")
|
||
return sale_date
|
||
|
||
def _prepare_batch_data(self, receipts_batch: List[Dict]) -> List[List]:
|
||
"""
|
||
Prepara dados de um lote de recibos para inserção em massa
|
||
|
||
Args:
|
||
receipts_batch: Lista de recibos para processar
|
||
|
||
Returns:
|
||
Lista de listas com valores para inserção
|
||
"""
|
||
batch_values = []
|
||
|
||
for receipt in receipts_batch:
|
||
# Processar cada recibo e extrair todos os registros (linhas)
|
||
receipt_records = self._extract_receipt_records(receipt)
|
||
batch_values.extend(receipt_records)
|
||
|
||
return batch_values
|
||
|
||
def _extract_receipt_records(self, receipt: Dict) -> List[List]:
|
||
"""
|
||
Extrai todos os registros (linhas) de um recibo
|
||
Cada item vira uma linha separada
|
||
"""
|
||
records = []
|
||
|
||
# Dados do cupom (serão repetidos para cada item)
|
||
cupom_data = self._extract_cupom_data(receipt)
|
||
|
||
# Dados consolidados de payments e tef
|
||
consolidated_payment_data = self._consolidate_payments(receipt.get('payments', []))
|
||
consolidated_tef_data = self._consolidate_tef_transactions(receipt.get('tefTransactionItems', []))
|
||
|
||
# Processar items
|
||
items = receipt.get('items', [])
|
||
|
||
if not items:
|
||
# Se não há items, criar uma linha apenas com dados do cupom
|
||
record_values = self._build_record_values(cupom_data, None, consolidated_payment_data, consolidated_tef_data)
|
||
records.append(record_values)
|
||
else:
|
||
# Para cada item, criar uma linha
|
||
for item in items:
|
||
item_data = self._extract_item_data(item)
|
||
record_values = self._build_record_values(cupom_data, item_data, consolidated_payment_data, consolidated_tef_data)
|
||
records.append(record_values)
|
||
|
||
return records
|
||
|
||
def _extract_cupom_data(self, receipt: Dict) -> Dict:
|
||
"""Extrai dados do cupom"""
|
||
return {
|
||
'cupomId': receipt.get('id'),
|
||
'cupomReceiptSequence': receipt.get('receiptSequence'),
|
||
'cupomCashRegisterNumber': receipt.get('cashRegisterNumber'),
|
||
'cupomStoreId': receipt.get('storeId'),
|
||
'cupomCoo': receipt.get('coo'),
|
||
'cupomEmployeeId': receipt.get('employeeId'),
|
||
'cupomEmployeeName': receipt.get('employeeName'),
|
||
'cupomValue': receipt.get('value'),
|
||
'cupomAdditionalValue': receipt.get('additionalValue'),
|
||
'cupomDiscountValue': receipt.get('discountValue'),
|
||
'cupomItemsQuantity': receipt.get('itemsQuantity'),
|
||
'cupomUnitsQuantity': receipt.get('unitsQuantity'),
|
||
'cupomCancelledItemsQuantity': receipt.get('cancelledItemsQuantity'),
|
||
'cupomCancelledItemsValue': receipt.get('cancelledItemsValue'),
|
||
'cupomSaleType': receipt.get('saleType'),
|
||
'cupomCancelledUnitsQuantity': receipt.get('cancelledUnitsQuantity'),
|
||
'cupomSaleDate': receipt.get('saleDate'),
|
||
'cupomInvoiceXMLStatus': receipt.get('invoiceXMLStatus'),
|
||
'cupomReceiptOpeningDateTime': receipt.get('receiptOpeningDateTime'),
|
||
'cupomReceiptClosingDateTime': receipt.get('receiptClosingDateTime'),
|
||
'cupomEletronicKey': receipt.get('eletronicKey'),
|
||
'cupomSaleOrderId': receipt.get('saleOrderId'),
|
||
'cupomExternalId': receipt.get('externalId'),
|
||
'cupomDiscountReason': receipt.get('discountReason'),
|
||
'cupomLoyaltyDiscountValue': receipt.get('loyaltyDiscountValue'),
|
||
'cupomCancellingReason': receipt.get('cancellingReason'),
|
||
'cupomCancelledReceiptSequence': receipt.get('cancelledReceiptSequence'),
|
||
'cupomChannel': receipt.get('channel'),
|
||
'cupomChannelDescription': receipt.get('channelDescription')
|
||
}
|
||
|
||
def _extract_item_data(self, item: Dict) -> Dict:
|
||
"""Extrai dados do item"""
|
||
return {
|
||
'itemId': item.get('id'),
|
||
'itemCancelled': item.get('cancelled'),
|
||
'itemProductId': item.get('productId'),
|
||
'itemSellerId': item.get('sellerId'),
|
||
'itemSellerName': item.get('sellerName'),
|
||
'itemQuantity': item.get('quantity'),
|
||
'itemUnitValue': item.get('unitValue'),
|
||
'itemGrossValue': item.get('grossValue'),
|
||
'itemAdditionalValue': item.get('additionalValue'),
|
||
'itemDiscountValue': item.get('discountValue'),
|
||
'itemTotalValue': item.get('totalValue'),
|
||
'itemTabelaA': item.get('tabelaA'),
|
||
'itemNcm': item.get('ncm'),
|
||
'itemNcmExcecao': item.get('ncmExcecao'),
|
||
'itemNatureza': item.get('natureza'),
|
||
'itemCfop': item.get('cfop'),
|
||
'itemCsosn': item.get('csosn'),
|
||
'itemCstICMS': item.get('cstICMS'),
|
||
'itemAliquotaICMS': item.get('aliquotaICMS'),
|
||
'itemValorReducaoAliquotaICMS': item.get('valorReducaoAliquotaICMS'),
|
||
'itemValorICMSDesonerado': item.get('valorICMSDesonerado'),
|
||
'itemValorFecop': item.get('valorFecop'),
|
||
'itemAliquotaFecop': item.get('aliquotaFecop'),
|
||
'itemCstPIS': item.get('cstPIS'),
|
||
'itemAliquotaPIS': item.get('aliquotaPIS'),
|
||
'itemCstCOFINS': item.get('cstCOFINS'),
|
||
'itemAliquotaCOFINS': item.get('aliquotaCOFINS')
|
||
}
|
||
|
||
def _build_record_values(self, cupom_data: Dict, item_data: Dict, payment_data: Dict, tef_data: Dict) -> List:
|
||
"""
|
||
Constrói uma lista de valores na ordem correta para inserção
|
||
"""
|
||
values = []
|
||
|
||
# Campos do cupom (29 valores)
|
||
cupom_fields = [
|
||
'cupomId', 'cupomReceiptSequence', 'cupomCashRegisterNumber', 'cupomStoreId', 'cupomCoo',
|
||
'cupomEmployeeId', 'cupomEmployeeName', 'cupomValue', 'cupomAdditionalValue', 'cupomDiscountValue',
|
||
'cupomItemsQuantity', 'cupomUnitsQuantity', 'cupomCancelledItemsQuantity', 'cupomCancelledItemsValue',
|
||
'cupomSaleType', 'cupomCancelledUnitsQuantity', 'cupomSaleDate', 'cupomInvoiceXMLStatus',
|
||
'cupomReceiptOpeningDateTime', 'cupomReceiptClosingDateTime', 'cupomEletronicKey', 'cupomSaleOrderId',
|
||
'cupomExternalId', 'cupomDiscountReason', 'cupomLoyaltyDiscountValue', 'cupomCancellingReason',
|
||
'cupomCancelledReceiptSequence', 'cupomChannel', 'cupomChannelDescription'
|
||
]
|
||
|
||
for field in cupom_fields:
|
||
values.append(cupom_data.get(field) if cupom_data else None)
|
||
|
||
# Campos do item (27 valores)
|
||
item_fields = [
|
||
'itemId', 'itemCancelled', 'itemProductId', 'itemSellerId', 'itemSellerName', 'itemQuantity',
|
||
'itemUnitValue', 'itemGrossValue', 'itemAdditionalValue', 'itemDiscountValue', 'itemTotalValue',
|
||
'itemTabelaA', 'itemNcm', 'itemNcmExcecao', 'itemNatureza', 'itemCfop', 'itemCsosn',
|
||
'itemCstICMS', 'itemAliquotaICMS', 'itemValorReducaoAliquotaICMS', 'itemValorICMSDesonerado',
|
||
'itemValorFecop', 'itemAliquotaFecop', 'itemCstPIS', 'itemAliquotaPIS', 'itemCstCOFINS', 'itemAliquotaCOFINS'
|
||
]
|
||
|
||
for field in item_fields:
|
||
values.append(item_data.get(field) if item_data else None)
|
||
|
||
# Campos do pagamento (16 valores)
|
||
payment_fields = [
|
||
'paymentId', 'paymentMethodId', 'paymentMethodDescription', 'paymentValue', 'paymentChange',
|
||
'paymentInstallmentQuantity', 'paymentCheckIssuer', 'paymentCardAuthorization', 'paymentCardFlag',
|
||
'paymentCardFlagDescription', 'paymentCardModality', 'paymentRedeAdquirente', 'paymentNsu',
|
||
'paymentAuthorizationNsu', 'paymentNsuCancelling', 'paymentCardBinNumber'
|
||
]
|
||
|
||
for field in payment_fields:
|
||
values.append(payment_data.get(field) if payment_data else None)
|
||
|
||
# Campos do TEF (12 valores)
|
||
tef_fields = [
|
||
'teftransId', 'teftransSequential', 'teftransPaymentMethodDescription', 'teftransValue',
|
||
'teftransCardModality', 'teftransCancellingModality', 'teftransCardType', 'teftranSitefNsu',
|
||
'teftransAuthorizerHostNsu', 'teftransAuthorizationCode', 'teftransInstallmentQuantity', 'teftransFirstIntallmentDate'
|
||
]
|
||
|
||
for field in tef_fields:
|
||
values.append(tef_data.get(field) if tef_data else None)
|
||
|
||
return values
|
||
|
||
def _insert_receipt_data(self, cursor, receipt: Dict, skip_duplicates: bool = True) -> Dict:
|
||
"""
|
||
Insere dados de um recibo específico - UMA LINHA POR ITEM
|
||
Cada item gera uma linha separada com os dados do cupom repetidos
|
||
|
||
Args:
|
||
cursor: Cursor do banco de dados
|
||
receipt: Dados de um recibo específico
|
||
skip_duplicates: Se True, pula registros duplicados
|
||
|
||
Returns:
|
||
Dict com estatísticas: {'inserted': int, 'duplicates': int}
|
||
"""
|
||
records_inserted = 0
|
||
duplicates_skipped = 0
|
||
|
||
# Dados do cupom (receipt) - serão repetidos para cada item
|
||
cupom_data = {
|
||
'cupomId': receipt.get('id'),
|
||
'cupomReceiptSequence': receipt.get('receiptSequence'),
|
||
'cupomCashRegisterNumber': receipt.get('cashRegisterNumber'),
|
||
'cupomStoreId': receipt.get('storeId'),
|
||
'cupomCoo': receipt.get('coo'),
|
||
'cupomEmployeeId': receipt.get('employeeId'),
|
||
'cupomEmployeeName': receipt.get('employeeName'),
|
||
'cupomValue': receipt.get('value'),
|
||
'cupomAdditionalValue': receipt.get('additionalValue'),
|
||
'cupomDiscountValue': receipt.get('discountValue'),
|
||
'cupomItemsQuantity': receipt.get('itemsQuantity'),
|
||
'cupomUnitsQuantity': receipt.get('unitsQuantity'),
|
||
'cupomCancelledItemsQuantity': receipt.get('cancelledItemsQuantity'),
|
||
'cupomCancelledItemsValue': receipt.get('cancelledItemsValue'),
|
||
'cupomSaleType': receipt.get('saleType'),
|
||
'cupomCancelledUnitsQuantity': receipt.get('cancelledUnitsQuantity'),
|
||
'cupomSaleDate': receipt.get('saleDate'),
|
||
'cupomInvoiceXMLStatus': receipt.get('invoiceXMLStatus'),
|
||
'cupomReceiptOpeningDateTime': receipt.get('receiptOpeningDateTime'),
|
||
'cupomReceiptClosingDateTime': receipt.get('receiptClosingDateTime'),
|
||
'cupomEletronicKey': receipt.get('eletronicKey'),
|
||
'cupomSaleOrderId': receipt.get('saleOrderId'),
|
||
'cupomExternalId': receipt.get('externalId'),
|
||
'cupomDiscountReason': receipt.get('discountReason'),
|
||
'cupomLoyaltyDiscountValue': receipt.get('loyaltyDiscountValue'),
|
||
'cupomCancellingReason': receipt.get('cancellingReason'),
|
||
'cupomCancelledReceiptSequence': receipt.get('cancelledReceiptSequence'),
|
||
'cupomChannel': receipt.get('channel'),
|
||
'cupomChannelDescription': receipt.get('channelDescription')
|
||
}
|
||
|
||
# Preparar dados de payments e tef (serão repetidos para cada item)
|
||
payments = receipt.get('payments', [])
|
||
tef_transactions = receipt.get('tefTransactionItems', [])
|
||
|
||
# Preparar dados consolidados de payments (todos os payments em uma estrutura)
|
||
consolidated_payment_data = self._consolidate_payments(payments)
|
||
consolidated_tef_data = self._consolidate_tef_transactions(tef_transactions)
|
||
|
||
# Processar items - CADA ITEM = UMA LINHA
|
||
items = receipt.get('items', [])
|
||
|
||
if not items:
|
||
# Se não há items, inserir apenas dados do cupom (cupom vazio)
|
||
result = self._insert_single_record(
|
||
cursor, cupom_data, None, consolidated_payment_data, consolidated_tef_data, skip_duplicates
|
||
)
|
||
if result == 1:
|
||
records_inserted += 1
|
||
elif result == -1:
|
||
duplicates_skipped += 1
|
||
else:
|
||
# Para cada item, criar UMA LINHA com cupom + item + todos os payments + todos os tef
|
||
for item in items:
|
||
item_data = {
|
||
'itemId': item.get('id'),
|
||
'itemCancelled': item.get('cancelled'),
|
||
'itemProductId': item.get('productId'),
|
||
'itemSellerId': item.get('sellerId'),
|
||
'itemSellerName': item.get('sellerName'),
|
||
'itemQuantity': item.get('quantity'),
|
||
'itemUnitValue': item.get('unitValue'),
|
||
'itemGrossValue': item.get('grossValue'),
|
||
'itemAdditionalValue': item.get('additionalValue'),
|
||
'itemDiscountValue': item.get('discountValue'),
|
||
'itemTotalValue': item.get('totalValue'),
|
||
'itemTabelaA': item.get('tabelaA'),
|
||
'itemNcm': item.get('ncm'),
|
||
'itemNcmExcecao': item.get('ncmExcecao'),
|
||
'itemNatureza': item.get('natureza'),
|
||
'itemCfop': item.get('cfop'),
|
||
'itemCsosn': item.get('csosn'),
|
||
'itemCstICMS': item.get('cstICMS'),
|
||
'itemAliquotaICMS': item.get('aliquotaICMS'),
|
||
'itemValorReducaoAliquotaICMS': item.get('valorReducaoAliquotaICMS'),
|
||
'itemValorICMSDesonerado': item.get('valorICMSDesonerado'),
|
||
'itemValorFecop': item.get('valorFecop'),
|
||
'itemAliquotaFecop': item.get('aliquotaFecop'),
|
||
'itemCstPIS': item.get('cstPIS'),
|
||
'itemAliquotaPIS': item.get('aliquotaPIS'),
|
||
'itemCstCOFINS': item.get('cstCOFINS'),
|
||
'itemAliquotaCOFINS': item.get('aliquotaCOFINS')
|
||
}
|
||
|
||
# Inserir UMA linha por item (com dados do cupom, payments e tef repetidos)
|
||
result = self._insert_single_record(
|
||
cursor, cupom_data, item_data, consolidated_payment_data, consolidated_tef_data, skip_duplicates
|
||
)
|
||
if result == 1:
|
||
records_inserted += 1
|
||
elif result == -1:
|
||
duplicates_skipped += 1
|
||
|
||
return {
|
||
'inserted': records_inserted,
|
||
'duplicates': duplicates_skipped
|
||
}
|
||
|
||
def _consolidate_payments(self, payments: List[Dict]) -> Dict:
|
||
"""
|
||
Consolida múltiplos payments em uma estrutura única
|
||
Se houver múltiplos payments, pega o primeiro ou consolida conforme necessário
|
||
"""
|
||
if not payments:
|
||
return None
|
||
|
||
# Por enquanto, vamos pegar apenas o primeiro payment
|
||
# Você pode ajustar esta lógica conforme necessário
|
||
first_payment = payments[0]
|
||
|
||
return {
|
||
'paymentId': first_payment.get('id'),
|
||
'paymentMethodId': first_payment.get('paymentMethodId'),
|
||
'paymentMethodDescription': first_payment.get('paymentMethodDescription'),
|
||
'paymentValue': first_payment.get('value'),
|
||
'paymentChange': first_payment.get('change'),
|
||
'paymentInstallmentQuantity': first_payment.get('installmentQuantity'),
|
||
'paymentCheckIssuer': first_payment.get('checkIssuer'),
|
||
'paymentCardAuthorization': first_payment.get('cardAuthorization'),
|
||
'paymentCardFlag': first_payment.get('cardFlag'),
|
||
'paymentCardFlagDescription': first_payment.get('cardFlagDescription'),
|
||
'paymentCardModality': first_payment.get('cardModality'),
|
||
'paymentRedeAdquirente': first_payment.get('redeAdquirente'),
|
||
'paymentNsu': first_payment.get('nsu'),
|
||
'paymentAuthorizationNsu': first_payment.get('authorizationNsu'),
|
||
'paymentNsuCancelling': first_payment.get('nsuCancelling'),
|
||
'paymentCardBinNumber': first_payment.get('cardBinNumber')
|
||
}
|
||
|
||
def _consolidate_tef_transactions(self, tef_transactions: List[Dict]) -> Dict:
|
||
"""
|
||
Consolida múltiplas transações TEF em uma estrutura única
|
||
Se houver múltiplas transações, pega a primeira ou consolida conforme necessário
|
||
"""
|
||
if not tef_transactions:
|
||
return None
|
||
|
||
# Por enquanto, vamos pegar apenas a primeira transação TEF
|
||
# Você pode ajustar esta lógica conforme necessário
|
||
first_tef = tef_transactions[0]
|
||
|
||
return {
|
||
'teftransId': first_tef.get('id'),
|
||
'teftransSequential': first_tef.get('sequential'),
|
||
'teftransPaymentMethodDescription': first_tef.get('paymentMethodDescription'),
|
||
'teftransValue': first_tef.get('value'),
|
||
'teftransCardModality': first_tef.get('cardModality'),
|
||
'teftransCancellingModality': first_tef.get('cancellingModality'),
|
||
'teftransCardType': first_tef.get('cardType'),
|
||
'teftranSitefNsu': first_tef.get('sitefNsu'),
|
||
'teftransAuthorizerHostNsu': first_tef.get('authorizerHostNsu'),
|
||
'teftransAuthorizationCode': first_tef.get('authorizationCode'),
|
||
'teftransInstallmentQuantity': first_tef.get('installmentQuantity'),
|
||
'teftransFirstIntallmentDate': first_tef.get('firstIntallmentDate')
|
||
}
|
||
|
||
def _insert_single_record(self, cursor, cupom_data: Dict, item_data: Dict = None,
|
||
payment_data: Dict = None, tef_data: Dict = None, skip_duplicates: bool = True) -> int:
|
||
"""
|
||
Insere um único registro na tabela rgb_sale_receipts
|
||
Verifica se já existe antes de inserir para evitar duplicatas
|
||
|
||
Args:
|
||
cursor: Cursor do banco de dados
|
||
cupom_data: Dados do cupom
|
||
item_data: Dados do item (opcional)
|
||
payment_data: Dados do pagamento (opcional)
|
||
tef_data: Dados da transação TEF (opcional)
|
||
skip_duplicates: Se True, pula duplicatas; se False, tenta inserir
|
||
|
||
Returns:
|
||
1 se inserção foi bem-sucedida
|
||
-1 se registro já existe e foi pulado
|
||
0 se houve erro na inserção
|
||
"""
|
||
try:
|
||
# Verificar se o registro já existe (baseado na restrição UNIQUE) apenas se skip_duplicates=True
|
||
if skip_duplicates:
|
||
cupom_id = cupom_data.get('cupomId') if cupom_data else None
|
||
item_id = item_data.get('itemId') if item_data else None
|
||
|
||
if cupom_id and item_id:
|
||
# Verificar se já existe a combinação cupomId + itemId
|
||
check_sql = """
|
||
SELECT COUNT(*) FROM [GINSENG].[dbo].[rgb_sale_receipts]
|
||
WHERE [cupomId] = ? AND [itemId] = ?
|
||
"""
|
||
cursor.execute(check_sql, (cupom_id, item_id))
|
||
exists = cursor.fetchone()[0] > 0
|
||
|
||
if exists:
|
||
print(f"⚠️ Registro já existe: cupomId={cupom_id}, itemId={item_id} - Pulando...")
|
||
return -1 # Código para "duplicata pulada"
|
||
elif cupom_id:
|
||
# Se não há itemId, verificar apenas por cupomId (para cupons sem items)
|
||
check_sql = """
|
||
SELECT COUNT(*) FROM [GINSENG].[dbo].[rgb_sale_receipts]
|
||
WHERE [cupomId] = ? AND [itemId] IS NULL
|
||
"""
|
||
cursor.execute(check_sql, (cupom_id,))
|
||
exists = cursor.fetchone()[0] > 0
|
||
|
||
if exists:
|
||
print(f"⚠️ Cupom sem item já existe: cupomId={cupom_id} - Pulando...")
|
||
return -1 # Código para "duplicata pulada"
|
||
# Definir campos da tabela (baseado na estrutura fornecida)
|
||
fields = [
|
||
# Campos do cupom (29 campos)
|
||
'cupomId', 'cupomReceiptSequence', 'cupomCashRegisterNumber', 'cupomStoreId', 'cupomCoo',
|
||
'cupomEmployeeId', 'cupomEmployeeName', 'cupomValue', 'cupomAdditionalValue', 'cupomDiscountValue',
|
||
'cupomItemsQuantity', 'cupomUnitsQuantity', 'cupomCancelledItemsQuantity', 'cupomCancelledItemsValue',
|
||
'cupomSaleType', 'cupomCancelledUnitsQuantity', 'cupomSaleDate', 'cupomInvoiceXMLStatus',
|
||
'cupomReceiptOpeningDateTime', 'cupomReceiptClosingDateTime', 'cupomEletronicKey', 'cupomSaleOrderId',
|
||
'cupomExternalId', 'cupomDiscountReason', 'cupomLoyaltyDiscountValue', 'cupomCancellingReason',
|
||
'cupomCancelledReceiptSequence', 'cupomChannel', 'cupomChannelDescription',
|
||
# Campos do item (27 campos)
|
||
'itemId', 'itemCancelled', 'itemProductId', 'itemSellerId', 'itemSellerName', 'itemQuantity',
|
||
'itemUnitValue', 'itemGrossValue', 'itemAdditionalValue', 'itemDiscountValue', 'itemTotalValue',
|
||
'itemTabelaA', 'itemNcm', 'itemNcmExcecao', 'itemNatureza', 'itemCfop', 'itemCsosn',
|
||
'itemCstICMS', 'itemAliquotaICMS', 'itemValorReducaoAliquotaICMS', 'itemValorICMSDesonerado',
|
||
'itemValorFecop', 'itemAliquotaFecop', 'itemCstPIS', 'itemAliquotaPIS', 'itemCstCOFINS', 'itemAliquotaCOFINS',
|
||
# Campos do payment (16 campos)
|
||
'paymentId', 'paymentMethodId', 'paymentMethodDescription', 'paymentValue', 'paymentChange',
|
||
'paymentInstallmentQuantity', 'paymentCheckIssuer', 'paymentCardAuthorization', 'paymentCardFlag',
|
||
'paymentCardFlagDescription', 'paymentCardModality', 'paymentRedeAdquirente', 'paymentNsu',
|
||
'paymentAuthorizationNsu', 'paymentNsuCancelling', 'paymentCardBinNumber',
|
||
# Campos do tef (12 campos)
|
||
'teftransId', 'teftransSequential', 'teftransPaymentMethodDescription', 'teftransValue',
|
||
'teftransCardModality', 'teftransCancellingModality', 'teftransCardType', 'teftranSitefNsu',
|
||
'teftransAuthorizerHostNsu', 'teftransAuthorizationCode', 'teftransInstallmentQuantity', 'teftransFirstIntallmentDate'
|
||
]
|
||
|
||
# Total: 29 + 27 + 16 + 12 = 84 campos
|
||
# Nota: Não incluímos o campo 'id' pois pode ser auto-incremento
|
||
total_fields = len(fields)
|
||
|
||
# Montar o SQL dinamicamente
|
||
fields_str = ', '.join([f'[{field}]' for field in fields])
|
||
placeholders = ', '.join(['?'] * total_fields)
|
||
|
||
sql = f"""
|
||
INSERT INTO [GINSENG].[dbo].[rgb_sale_receipts] (
|
||
{fields_str}
|
||
) VALUES (
|
||
{placeholders}
|
||
)
|
||
"""
|
||
|
||
print(f"🔧 SQL preparado com {total_fields} campos e {total_fields} placeholders")
|
||
|
||
# Preparar valores para inserção na ordem exata dos campos
|
||
values = []
|
||
|
||
# Dados do cupom (29 valores)
|
||
cupom_fields = [
|
||
'cupomId', 'cupomReceiptSequence', 'cupomCashRegisterNumber', 'cupomStoreId', 'cupomCoo',
|
||
'cupomEmployeeId', 'cupomEmployeeName', 'cupomValue', 'cupomAdditionalValue', 'cupomDiscountValue',
|
||
'cupomItemsQuantity', 'cupomUnitsQuantity', 'cupomCancelledItemsQuantity', 'cupomCancelledItemsValue',
|
||
'cupomSaleType', 'cupomCancelledUnitsQuantity', 'cupomSaleDate', 'cupomInvoiceXMLStatus',
|
||
'cupomReceiptOpeningDateTime', 'cupomReceiptClosingDateTime', 'cupomEletronicKey', 'cupomSaleOrderId',
|
||
'cupomExternalId', 'cupomDiscountReason', 'cupomLoyaltyDiscountValue', 'cupomCancellingReason',
|
||
'cupomCancelledReceiptSequence', 'cupomChannel', 'cupomChannelDescription'
|
||
]
|
||
|
||
for field in cupom_fields:
|
||
values.append(cupom_data.get(field) if cupom_data else None)
|
||
|
||
# Dados do item (27 valores)
|
||
item_fields = [
|
||
'itemId', 'itemCancelled', 'itemProductId', 'itemSellerId', 'itemSellerName', 'itemQuantity',
|
||
'itemUnitValue', 'itemGrossValue', 'itemAdditionalValue', 'itemDiscountValue', 'itemTotalValue',
|
||
'itemTabelaA', 'itemNcm', 'itemNcmExcecao', 'itemNatureza', 'itemCfop', 'itemCsosn',
|
||
'itemCstICMS', 'itemAliquotaICMS', 'itemValorReducaoAliquotaICMS', 'itemValorICMSDesonerado',
|
||
'itemValorFecop', 'itemAliquotaFecop', 'itemCstPIS', 'itemAliquotaPIS', 'itemCstCOFINS', 'itemAliquotaCOFINS'
|
||
]
|
||
|
||
for field in item_fields:
|
||
values.append(item_data.get(field) if item_data else None)
|
||
|
||
# Dados do pagamento (16 valores)
|
||
payment_fields = [
|
||
'paymentId', 'paymentMethodId', 'paymentMethodDescription', 'paymentValue', 'paymentChange',
|
||
'paymentInstallmentQuantity', 'paymentCheckIssuer', 'paymentCardAuthorization', 'paymentCardFlag',
|
||
'paymentCardFlagDescription', 'paymentCardModality', 'paymentRedeAdquirente', 'paymentNsu',
|
||
'paymentAuthorizationNsu', 'paymentNsuCancelling', 'paymentCardBinNumber'
|
||
]
|
||
|
||
for field in payment_fields:
|
||
values.append(payment_data.get(field) if payment_data else None)
|
||
|
||
# Dados do TEF (12 valores)
|
||
tef_fields = [
|
||
'teftransId', 'teftransSequential', 'teftransPaymentMethodDescription', 'teftransValue',
|
||
'teftransCardModality', 'teftransCancellingModality', 'teftransCardType', 'teftranSitefNsu',
|
||
'teftransAuthorizerHostNsu', 'teftransAuthorizationCode', 'teftransInstallmentQuantity', 'teftransFirstIntallmentDate'
|
||
]
|
||
|
||
for field in tef_fields:
|
||
values.append(tef_data.get(field) if tef_data else None)
|
||
|
||
# Verificar se temos exatamente 84 valores
|
||
if len(values) != total_fields:
|
||
print(f"❌ ERRO: Esperado {total_fields} valores, mas temos {len(values)}")
|
||
return 0
|
||
|
||
print(f"✅ Preparados {len(values)} valores para inserção")
|
||
|
||
# Executar inserção
|
||
cursor.execute(sql, values)
|
||
return 1
|
||
|
||
except Exception as e:
|
||
print(f"❌ Erro ao inserir registro: {e}")
|
||
return 0
|
||
|
||
|
||
def get_bearer_token_from_database():
|
||
"""
|
||
Busca o token de autenticação do banco de dados
|
||
|
||
Returns:
|
||
str: Token de autenticação ou None em caso de erro
|
||
"""
|
||
# Lista de drivers para tentar em ordem de preferência
|
||
drivers = [
|
||
'ODBC Driver 18 for SQL Server',
|
||
'ODBC Driver 17 for SQL Server',
|
||
'ODBC Driver 13 for SQL Server',
|
||
'ODBC Driver 11 for SQL Server',
|
||
'SQL Server Native Client 11.0',
|
||
'SQL Server Native Client 10.0',
|
||
'SQL Server'
|
||
]
|
||
|
||
print("🔍 Buscando token de autenticação no banco de dados...")
|
||
available_drivers = pyodbc.drivers()
|
||
|
||
for driver in drivers:
|
||
if driver in available_drivers:
|
||
try:
|
||
connection_string = (
|
||
f'DRIVER={{{driver}}};'
|
||
'SERVER=10.77.77.10;'
|
||
'DATABASE=GINSENG;'
|
||
'UID=supginseng;'
|
||
'PWD=Ginseng@;'
|
||
'PORT=1433;'
|
||
'TrustServerCertificate=yes;'
|
||
'Encrypt=yes'
|
||
)
|
||
|
||
conn = pyodbc.connect(connection_string)
|
||
cursor = conn.cursor()
|
||
|
||
# Buscar o token da tabela dbo.rgb_token com id = 1
|
||
query = "SELECT token FROM dbo.rgb_token WHERE id = 1"
|
||
cursor.execute(query)
|
||
result = cursor.fetchone()
|
||
|
||
conn.close()
|
||
|
||
if result:
|
||
print("✅ Token obtido do banco de dados com sucesso!")
|
||
return result[0]
|
||
else:
|
||
print("❌ Token não encontrado na tabela dbo.rgb_token com id = 1")
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"❌ Erro ao buscar token com {driver}: {e}")
|
||
continue
|
||
|
||
# Se nenhum driver funcionou, tentar sem SSL
|
||
print("🔄 Tentando buscar token sem SSL...")
|
||
for driver in drivers:
|
||
if driver in available_drivers:
|
||
try:
|
||
connection_string = (
|
||
f'DRIVER={{{driver}}};'
|
||
'SERVER=10.77.77.10;'
|
||
'DATABASE=GINSENG;'
|
||
'UID=supginseng;'
|
||
'PWD=Ginseng@;'
|
||
'PORT=1433'
|
||
)
|
||
|
||
conn = pyodbc.connect(connection_string)
|
||
cursor = conn.cursor()
|
||
|
||
# Buscar o token da tabela dbo.rgb_token com id = 1
|
||
query = "SELECT token FROM dbo.rgb_token WHERE id = 1"
|
||
cursor.execute(query)
|
||
result = cursor.fetchone()
|
||
|
||
conn.close()
|
||
|
||
if result:
|
||
print("✅ Token obtido do banco de dados com sucesso!")
|
||
return result[0]
|
||
else:
|
||
print("❌ Token não encontrado na tabela dbo.rgb_token com id = 1")
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"❌ Erro ao buscar token sem SSL com {driver}: {e}")
|
||
continue
|
||
|
||
print("❌ Não foi possível conectar ao banco para buscar o token")
|
||
return None
|
||
|
||
|
||
def main():
|
||
"""Função principal do script"""
|
||
|
||
# Buscar token de autenticação do banco de dados
|
||
print("🔐 Obtendo token de autenticação...")
|
||
BEARER_TOKEN = get_bearer_token_from_database()
|
||
|
||
if not BEARER_TOKEN:
|
||
print("❌ Erro: Não foi possível obter o token de autenticação do banco de dados")
|
||
print("💡 Verifique se:")
|
||
print(" - A tabela dbo.rgb_token existe")
|
||
print(" - Existe um registro com id = 1")
|
||
print(" - A coluna 'token' contém um valor válido")
|
||
return
|
||
|
||
# ========================================
|
||
# CONFIGURAÇÃO DE DATA
|
||
# ========================================
|
||
# USE_DATE_RANGE:
|
||
# True = Processar intervalo de datas (START_DATE até END_DATE)
|
||
# False = Usar data única (ontem ou MANUAL_DATE)
|
||
USE_DATE_RANGE = False
|
||
|
||
# Intervalo de datas (usado quando USE_DATE_RANGE = True)
|
||
START_DATE = "2026-01-22"
|
||
END_DATE = "2026-01-22"
|
||
|
||
# USE_MANUAL_DATE:
|
||
# True = Usar MANUAL_DATE
|
||
# False = Usar data de ONTEM automaticamente
|
||
USE_MANUAL_DATE = False
|
||
MANUAL_DATE = "2025-09-25"
|
||
|
||
# Criar cliente da API
|
||
api_client = BoticarioAPI(BEARER_TOKEN)
|
||
|
||
# Verificar drivers ODBC antes de começar
|
||
has_drivers = api_client.check_and_install_odbc_driver()
|
||
if not has_drivers:
|
||
print("\n⚠️ Pulando inserção no banco de dados devido à falta de drivers ODBC")
|
||
print(" Execute as instruções acima para instalar o driver e execute o script novamente")
|
||
return
|
||
|
||
if USE_DATE_RANGE:
|
||
# ========================================
|
||
# MODO INTERVALO DE DATAS
|
||
# ========================================
|
||
from datetime import timedelta
|
||
|
||
start_dt = datetime.strptime(START_DATE, "%Y-%m-%d")
|
||
end_dt = datetime.strptime(END_DATE, "%Y-%m-%d")
|
||
total_days = (end_dt - start_dt).days + 1
|
||
|
||
print("\n" + "="*60)
|
||
print("🚀 SCRIPT DE ACESSO À API DO GRUPO BOTICÁRIO")
|
||
print("="*60)
|
||
print(f"📅 MODO: Intervalo de datas")
|
||
print(f"📅 De: {START_DATE} até {END_DATE}")
|
||
print(f"📅 Total de dias: {total_days}")
|
||
print("="*60)
|
||
|
||
# Estatísticas globais
|
||
total_stats = {
|
||
"dias_processados": 0,
|
||
"dias_com_erro": 0,
|
||
"total_recibos": 0,
|
||
"total_inseridos": 0
|
||
}
|
||
|
||
current_dt = start_dt
|
||
day_num = 0
|
||
|
||
while current_dt <= end_dt:
|
||
day_num += 1
|
||
SALE_DATE = current_dt.strftime("%Y-%m-%d")
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"📆 DIA {day_num}/{total_days}: {SALE_DATE}")
|
||
print(f"{'='*60}")
|
||
|
||
# Buscar TODOS os recibos (com paginação automática)
|
||
receipts_data = api_client.get_all_sale_receipts(SALE_DATE)
|
||
|
||
if receipts_data:
|
||
items = receipts_data.get('items', [])
|
||
total_stats["total_recibos"] += len(items)
|
||
|
||
print(f"✅ {len(items)} recibos obtidos")
|
||
|
||
if len(items) > 0:
|
||
# Inserir dados no banco de dados
|
||
success = api_client.insert_receipts_to_database(receipts_data, skip_duplicates=False)
|
||
|
||
if success:
|
||
total_stats["dias_processados"] += 1
|
||
print(f"✅ Dados inseridos com sucesso!")
|
||
else:
|
||
total_stats["dias_com_erro"] += 1
|
||
print(f"❌ Erro ao inserir dados")
|
||
else:
|
||
total_stats["dias_processados"] += 1
|
||
print(f"ℹ️ Nenhum recibo para esta data")
|
||
else:
|
||
total_stats["dias_com_erro"] += 1
|
||
print(f"❌ Erro ao buscar dados da API")
|
||
|
||
# Próximo dia
|
||
current_dt += timedelta(days=1)
|
||
|
||
# Resumo final
|
||
print(f"\n{'='*60}")
|
||
print("🎉 PROCESSAMENTO DO INTERVALO CONCLUÍDO!")
|
||
print(f"{'='*60}")
|
||
print(f"📅 Período: {START_DATE} até {END_DATE}")
|
||
print(f"📊 Dias processados: {total_stats['dias_processados']}/{total_days}")
|
||
print(f"❌ Dias com erro: {total_stats['dias_com_erro']}")
|
||
print(f"📦 Total de recibos: {total_stats['total_recibos']}")
|
||
print(f"{'='*60}")
|
||
|
||
else:
|
||
# ========================================
|
||
# MODO DATA ÚNICA (comportamento original)
|
||
# ========================================
|
||
from datetime import timedelta
|
||
|
||
if USE_MANUAL_DATE:
|
||
SALE_DATE = MANUAL_DATE
|
||
print("📅 CONFIGURAÇÃO: Data manual ativada")
|
||
print(f"📅 Data selecionada: {SALE_DATE}")
|
||
else:
|
||
# Usar data de ONTEM
|
||
SALE_DATE = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||
print("📅 CONFIGURAÇÃO: Data de ontem automática")
|
||
print(f"📅 Data selecionada: {SALE_DATE} (ontem)")
|
||
|
||
print("\n" + "="*60)
|
||
print("🚀 SCRIPT DE ACESSO À API DO GRUPO BOTICÁRIO")
|
||
print("="*60)
|
||
print(f"🎯 Buscando recibos de vendas para a data: {SALE_DATE}")
|
||
print("-" * 60)
|
||
|
||
# Buscar TODOS os recibos (com paginação automática)
|
||
receipts_data = api_client.get_all_sale_receipts(SALE_DATE)
|
||
|
||
if receipts_data:
|
||
print("\n✅ Dados obtidos com sucesso!")
|
||
|
||
if isinstance(receipts_data, dict):
|
||
print(f"\n📊 Resumo dos dados obtidos da API:")
|
||
print(f" - Start: {receipts_data.get('start', 'N/A')}")
|
||
print(f" - Count: {receipts_data.get('count', 'N/A')}")
|
||
print(f" - Total: {receipts_data.get('total', 'N/A')}")
|
||
|
||
items = receipts_data.get('items', [])
|
||
print(f" - Items obtidos: {len(items)}")
|
||
|
||
if receipts_data.get('total', 0) == len(items):
|
||
print(" - Status: ✅ Todos os registros obtidos da API")
|
||
else:
|
||
print(" - Status: ⚠️ Alguns registros podem estar faltando")
|
||
|
||
print(" - Destino: 🗄️ Direto para o banco de dados (sem arquivo local)")
|
||
|
||
# Inserir dados no banco de dados
|
||
print("\n" + "="*60)
|
||
print("🗄️ PROCESSAMENTO DO BANCO DE DADOS")
|
||
print("="*60)
|
||
print("🔄 Estratégia: Limpar dados existentes + Inserir novos dados")
|
||
print("📦 Inserção otimizada em lotes de 1000 registros")
|
||
|
||
success = api_client.insert_receipts_to_database(receipts_data, skip_duplicates=False)
|
||
|
||
if success:
|
||
print("\n🎉 PROCESSO COMPLETO!")
|
||
print("✅ Dados antigos removidos e novos dados inseridos com sucesso!")
|
||
else:
|
||
print("\n❌ PROCESSO FALHOU!")
|
||
print("❌ Falha durante o processamento do banco de dados")
|
||
|
||
else:
|
||
print("\n❌ Não foi possível obter os dados da API")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |