import requests import pyodbc import time # =============================== # CONFIGURAÇÕES # =============================== TOKEN_API_URL = "https://api.grupoginseng.com.br/api/tokens" PDVS_API_URL = "https://api.grupoginseng.com.br/pdvs" ORDERS_URL = ( "https://mar-api-gateway-front.demanda-abastecimento.grupoboticario.digital/" "orders-bff/api/tracking/v5/orders" ) SUMMARY_URL = ( "https://mar-api-gateway-front.demanda-abastecimento.grupoboticario.digital/" "orders-bff/api/customer-orders/summary/" ) ITEMS_URL = ( "https://mar-api-gateway-front.demanda-abastecimento.grupoboticario.digital/" "orders-bff/api/customer-orders/v2/items/" ) PARAMS_BASE = { "sort": "orderid,desc" } # =============================== # CONFIGURAÇÕES DO BANCO DE DADOS # =============================== DB_DRIVER = "ODBC Driver 17 for SQL Server" DB_CONNECTION_STRING = ( f'DRIVER={{{DB_DRIVER}}};' 'SERVER=10.77.77.10;' 'DATABASE=GINSENG;' 'UID=supginseng;' 'PWD=Ginseng@;' 'PORT=1433;' 'TrustServerCertificate=yes;' 'Encrypt=yes' ) # =============================== # CONFIGURAÇÕES DE EXECUÇÃO # =============================== REQUEST_DELAY = 0.5 # Delay em segundos entre requisições para evitar rate limiting TOKEN_REFRESH_INTERVAL = 50 # Renovar token a cada N lojas processadas FAILED_STORES_MAX_RETRIES = 3 # Número máximo de tentativas para lojas que falharam FAILED_STORES_RETRY_DELAY = 180 # Tempo de espera (em segundos) antes de tentar novamente lojas que falharam # =============================== # FUNÇÕES AUXILIARES # =============================== def get_new_token(): """ Obtém um novo token da API Retorna o token ou None em caso de erro """ try: print("\n[TOKEN] Obtendo novo token...") token_response = requests.get(TOKEN_API_URL, timeout=30) token_response.raise_for_status() token = token_response.json()["data"][0]["token"] print("[TOKEN] Token renovado com sucesso!") return token except Exception as e: print(f"[TOKEN] Erro ao obter token: {e}") return None def wrap(text, width=30): """Mantém o texto sem quebras para Excel""" if not text: return "" return str(text) def format_money(value): """ Formata valor monetário evitando notação científica """ try: return f"{float(value):,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") except Exception: return "" def to_decimal(value): """ Converte valor para decimal (float) para inserção no banco """ if not value or value == "": return None try: # Remove formatação de moeda brasileira se houver if isinstance(value, str): value = value.replace(".", "").replace(",", ".") return float(value) except Exception: return None def to_int(value): """ Converte valor para inteiro para inserção no banco """ if not value or value == "": return None try: return int(value) except Exception: return None def to_date(value): """ Converte string de data para formato aceito pelo SQL Server Formato esperado: dd/mm/yyyy """ if not value or value == "" or value == "-": return None try: # Se já vier no formato dd/mm/yyyy, converte para datetime from datetime import datetime if isinstance(value, str) and "/" in value: parts = value.split("/") if len(parts) == 3: # Converte dd/mm/yyyy para yyyy-mm-dd return f"{parts[2]}-{parts[1]}-{parts[0]}" return value except Exception: return None def expand_sell_orders(sell_orders_list): """ Retorna uma única tupla com os sellOrders concatenados por marca. Se houver múltiplos sellOrders para uma marca, junta com vírgula (sem duplicatas). """ sell_map = { "BOT": set(), "EUD": set(), "QDB": set() } for item in sell_orders_list: bu = item.get("businessUnit") orders = item.get("sellOrders", []) if bu in sell_map and orders: sell_map[bu].update(orders) # Concatena múltiplos sellOrders com vírgula (ordenados), ou string vazia se não houver sell_bot = ", ".join(sorted(sell_map["BOT"])) if sell_map["BOT"] else "" sell_eud = ", ".join(sorted(sell_map["EUD"])) if sell_map["EUD"] else "" sell_qdb = ", ".join(sorted(sell_map["QDB"])) if sell_map["QDB"] else "" # Retorna sempre uma única combinação return [(sell_bot, sell_eud, sell_qdb)] def process_store_with_retry(store, headers, max_retries=3, retry_delay=5, token_refresh_callback=None): """ Processa uma loja com tentativas de retry em caso de erro Retorna tupla (lista de dados da loja, needs_token_refresh) needs_token_refresh indica se o token precisa ser renovado """ needs_token_refresh = False for attempt in range(max_retries): # IMPORTANTE: Limpar dados a cada tentativa para evitar duplicação # caso uma tentativa anterior tenha processado pedidos parcialmente antes de falhar store_data = [] try: # Primeira chamada para obter totalElements e calcular total de páginas params = PARAMS_BASE.copy() params["page"] = 0 initial_response = requests.get( ORDERS_URL, headers=headers, params=params, timeout=30 ) if initial_response.status_code != 200: print(f" [ERRO] Status code: {initial_response.status_code}") # Se for erro de autenticação, sinaliza para renovar token if initial_response.status_code in [401, 403]: print(f" [ERRO] Token expirado ou inválido!") needs_token_refresh = True # Se tiver callback para renovar token, usa if token_refresh_callback and attempt < max_retries - 1: new_token = token_refresh_callback() if new_token: headers["authorization"] = new_token headers["x-authorization"] = new_token needs_token_refresh = False print(f" [TOKEN] Tentando novamente com novo token...") time.sleep(retry_delay) continue if attempt < max_retries - 1: print(f" Tentativa {attempt + 1}/{max_retries} falhou. Aguardando {retry_delay}s antes de tentar novamente...") time.sleep(retry_delay) continue else: print(f" Erro ao buscar informações da loja {store} após {max_retries} tentativas") return [], needs_token_refresh initial_data = initial_response.json() total_elements = initial_data.get("totalElements", 0) total_pages = initial_data.get("totalPages", 1) page_size = initial_data.get("size", 25) print(f" Total de pedidos: {total_elements}") print(f" Total de páginas disponíveis: {total_pages}") print(f" Tamanho da página: {page_size}") # Buscar apenas página 0 pages_to_fetch = [0] print(f" Buscando páginas: {pages_to_fetch}") for page in pages_to_fetch: print(f" Buscando página {page}...") # Delay entre requisições para evitar rate limiting time.sleep(REQUEST_DELAY) params = PARAMS_BASE.copy() params["page"] = page response = requests.get( ORDERS_URL, headers=headers, params=params, timeout=30 ) if response.status_code != 200: print(f" [ERRO] Erro ao buscar página {page} da loja {store} - Status: {response.status_code}") # Se for erro de autenticação na página if response.status_code in [401, 403]: print(f" [ERRO] Token expirado durante busca de página!") needs_token_refresh = True continue orders = response.json().get("content", []) for order in orders: order_id = order.get("orderId") print(f" Processando pedido {order_id}...") # Delay entre requisições time.sleep(REQUEST_DELAY) summary_response = requests.get( f"{SUMMARY_URL}{order_id}", headers=headers, timeout=30 ) total_items = "" total_skus = "" total_value = "" percent_txt = [] sell_combinations = [("", "", "")] if summary_response.status_code == 200: summary = summary_response.json() order_summary = summary.get("orderSummary", {}) totals = summary.get("orderTotals", {}) sell_combinations = expand_sell_orders( order_summary.get("order", {}).get("sellOrdersList", []) ) total_items = totals.get("totalItems", "") total_skus = totals.get("totalSKUs", "") total_value = format_money(totals.get("totalOrderValue", "")) for p in totals.get("totalPerBrand", []): percent_txt.append(f"{p['businessUnit']}: {p['percentage']}%") # Buscar itens do pedido time.sleep(REQUEST_DELAY) items_response = requests.get( f"{ITEMS_URL}{order_id}", headers=headers, params={"page": 0, "sort": "business_unit,asc"}, timeout=30 ) items_list = [] if items_response.status_code == 200: items_data = items_response.json() items_list = items_data.get("content", []) # Se não houver itens, adiciona uma linha com os dados do pedido if not items_list: for sell_bot, sell_eud, sell_qdb in sell_combinations: store_data.append({ "Loja": store, "Pedido": order_id, "Data": order.get("orderDate"), "Tipo": order.get("orderType"), "Status": order.get("status"), "Valor (R$)": format_money(order.get("orderValue", 0)), "PDV": wrap(order.get("pdv")), "Observação": wrap(order.get("observation")), "Sell BOT": sell_bot, "Sell EUD": sell_eud, "Sell QDB": sell_qdb, "Itens": total_items, "SKUs": total_skus, "Valor Total": total_value, "% Atendido": wrap("\n".join(percent_txt)), # Dados do item "Item BU": "", "SKU": "", "Descrição Item": "", "Status Item": "", "Preço Unit.": "", "Qtd Solicitada": "", "Qtd Aceita": "", "Qtd Atendida": "", "Qtd Faturada": "", "% Item": "", "NF Número": "", "NF Data MAR": "", "Data Entrega": "", "Código Atendimento": "", "Motivo Recusa": "" }) else: # Adiciona uma linha para cada item do pedido for item in items_list: qty = item.get("quantity", {}) for sell_bot, sell_eud, sell_qdb in sell_combinations: store_data.append({ "Loja": store, "Pedido": order_id, "Data": order.get("orderDate"), "Tipo": order.get("orderType"), "Status": order.get("status"), "Valor (R$)": format_money(order.get("orderValue", 0)), "PDV": wrap(order.get("pdv")), "Observação": wrap(order.get("observation")), "Sell BOT": sell_bot, "Sell EUD": sell_eud, "Sell QDB": sell_qdb, "Itens": total_items, "SKUs": total_skus, "Valor Total": total_value, "% Atendido": wrap("\n".join(percent_txt)), # Dados do item "Item BU": item.get("bussinessUnit", ""), "SKU": item.get("sku", ""), "Descrição Item": wrap(item.get("description", "")), "Status Item": item.get("status", ""), "Preço Unit.": format_money(item.get("price", "")), "Qtd Solicitada": qty.get("requested", ""), "Qtd Aceita": qty.get("accepted", ""), "Qtd Atendida": qty.get("attended", ""), "Qtd Faturada": qty.get("invoiced", ""), "% Item": qty.get("percentage", ""), "NF Número": item.get("nfNumber", ""), "NF Data MAR": item.get("nfNumberMarDate", ""), "Data Entrega": item.get("deliveryDate", ""), "Código Atendimento": item.get("fullfilmentCode", ""), "Motivo Recusa": wrap(item.get("refusalReason", "")) }) # Se chegou aqui, processou com sucesso return store_data, needs_token_refresh except Exception as e: if attempt < max_retries - 1: print(f" [ERRO] Erro na tentativa {attempt + 1}/{max_retries}: {e}") print(f" Aguardando {retry_delay}s antes de tentar novamente...") time.sleep(retry_delay) else: print(f" [ERRO] Erro ao processar loja {store} após {max_retries} tentativas: {e}") return [], needs_token_refresh return [], needs_token_refresh # =============================== # 1) OBTER TOKEN # =============================== token_response = requests.get(TOKEN_API_URL, timeout=30) token_response.raise_for_status() TOKEN = token_response.json()["data"][0]["token"] print("Token obtido com sucesso") # =============================== # 2) OBTER LISTA DE LOJAS (PDVs) # =============================== print("Buscando lista de lojas...") pdvs_response = requests.get(PDVS_API_URL, timeout=30) pdvs_response.raise_for_status() pdvs_data = pdvs_response.json() STORES_FULL = [str(pdv) for pdv in pdvs_data["data"]["pdvs"]] STORES = STORES_FULL print(f"Total de lojas encontradas: {len(STORES)} (de {len(STORES_FULL)} disponíveis)") print(f"Lojas: {', '.join(STORES[:10])}{'...' if len(STORES) > 10 else ''}") # =============================== # 3) HEADERS BASE # =============================== BASE_HEADERS = { "accept": "*/*", "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", "authorization": TOKEN, "x-authorization": TOKEN, "content-type": "application/json", "origin": "https://extranet.grupoboticario.com.br", "referer": "https://extranet.grupoboticario.com.br/", "user-agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/143.0.0.0 Safari/537.36" ), "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "cross-site", "sec-ch-ua": "\"Google Chrome\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "x-user-id": "163165", "x-username": "daniel.rodrigue" } # =============================== # 4) LOOP POR LOJA # =============================== def save_store_data_to_db(store_data, stats): """ Salva os dados de uma loja no banco de dados imediatamente. Deleta os pedidos existentes e insere os novos. Retorna True se salvou com sucesso, False caso contrário. """ if not store_data: return True # Nada para salvar, considera sucesso try: conn = pyodbc.connect(DB_CONNECTION_STRING) cursor = conn.cursor() # Obter lista única de pedidos para deletar unique_orders = list(set([record["Pedido"] for record in store_data])) stats["pedidos_unicos"] += len(unique_orders) # Deletar registros existentes dos pedidos if unique_orders: # Converter para int para garantir compatibilidade com coluna numérica do banco unique_orders_int = [int(p) for p in unique_orders] placeholders = ','.join(['?' for _ in unique_orders_int]) delete_query = f"DELETE FROM [GINSENG].[dbo].[extrato_pedidos_mar] WHERE [Pedido] IN ({placeholders})" cursor.execute(delete_query, unique_orders_int) stats["registros_deletados"] += cursor.rowcount conn.commit() # Query de inserção insert_query = """ INSERT INTO [GINSENG].[dbo].[extrato_pedidos_mar] ([Loja], [Pedido], [Data], [Tipo], [Status], [Valor], [PDV], [Observacao], [SellBOT], [SellEUD], [SellQDB], [Itens], [SKUs], [ValorTotal], [PercentualAtendido], [ItemBU], [SKU], [DescricaoItem], [StatusItem], [PrecoUnitario], [QtdSolicitada], [QtdAceita], [QtdAtendida], [QtdFaturada], [PercentualItem], [NFNumero], [NFDataMAR], [DataEntrega], [CodigoAtendimento], [MotivoRecusa]) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ # Inserir dados for record in store_data: cursor.execute(insert_query, record["Loja"], record["Pedido"], to_date(record["Data"]), record["Tipo"], record["Status"], to_decimal(record["Valor (R$)"]), record["PDV"] if record["PDV"] else None, record["Observação"] if record["Observação"] else None, record["Sell BOT"] if record["Sell BOT"] else None, record["Sell EUD"] if record["Sell EUD"] else None, record["Sell QDB"] if record["Sell QDB"] else None, to_int(record["Itens"]), str(record["SKUs"]) if record["SKUs"] else None, to_decimal(record["Valor Total"]), record["% Atendido"] if record["% Atendido"] else None, record["Item BU"] if record["Item BU"] else None, record["SKU"] if record["SKU"] else None, record["Descrição Item"] if record["Descrição Item"] else None, record["Status Item"] if record["Status Item"] else None, to_decimal(record["Preço Unit."]), to_int(record["Qtd Solicitada"]), to_int(record["Qtd Aceita"]), to_int(record["Qtd Atendida"]), to_int(record["Qtd Faturada"]), to_int(record["% Item"]), record["NF Número"] if record["NF Número"] else None, to_date(record["NF Data MAR"]), to_date(record["Data Entrega"]), record["Código Atendimento"] if record["Código Atendimento"] else None, record["Motivo Recusa"] if record["Motivo Recusa"] else None ) stats["registros_inseridos"] += 1 conn.commit() cursor.close() conn.close() return True except Exception as e: print(f" [ERRO BD] Erro ao salvar no banco: {e}") return False def process_stores(stores_to_process, base_headers, token, stats): """ Processa uma lista de lojas e salva no banco imediatamente após cada loja. Retorna as lojas que falharam e o token atualizado. """ failed = [] processed_count = 0 for store in stores_to_process: print(f"\nProcessando loja {store}... ({processed_count + 1}/{len(stores_to_process)})") # Renovar token periodicamente a cada N lojas if processed_count > 0 and processed_count % TOKEN_REFRESH_INTERVAL == 0: print(f"\n[TOKEN] Renovação preventiva após {processed_count} lojas...") new_token = get_new_token() if new_token: token = new_token base_headers["authorization"] = token base_headers["x-authorization"] = token headers = base_headers.copy() headers["storecode"] = store # Processar loja com retry e callback para renovar token store_data, needs_refresh = process_store_with_retry( store, headers, max_retries=3, retry_delay=5, token_refresh_callback=get_new_token ) # Se precisou renovar token, atualiza os headers base if needs_refresh: new_token = get_new_token() if new_token: token = new_token base_headers["authorization"] = token base_headers["x-authorization"] = token processed_count += 1 if len(store_data) == 0: failed.append(store) print(f"Loja {store}: 0 registros (falha)") else: # Salvar no banco imediatamente print(f" Salvando {len(store_data)} registros no banco...") if save_store_data_to_db(store_data, stats): print(f" ✓ Loja {store}: {len(store_data)} registros salvos") stats["lojas_salvas"] += 1 else: failed.append(store) print(f" ✗ Loja {store}: erro ao salvar no banco") return failed, token # Estatísticas para relatório final stats = { "total_lojas": len(STORES), "lojas_sucesso_primeira": 0, "lojas_falha_inicial": [], "lojas_recuperadas_retry": [], "lojas_falha_final": [], "lojas_salvas": 0, "registros_inseridos": 0, "registros_deletados": 0, "pedidos_unicos": 0 } failed_stores = [] # Primeira passagem: processar todas as lojas failed_stores, TOKEN = process_stores(STORES, BASE_HEADERS, TOKEN, stats) # Guardar lojas que falharam na primeira tentativa stats["lojas_falha_inicial"] = failed_stores.copy() stats["lojas_sucesso_primeira"] = len(STORES) - len(failed_stores) # Retry das lojas que falharam retry_round = 0 while failed_stores and retry_round < FAILED_STORES_MAX_RETRIES: retry_round += 1 print(f"\n{'=' * 60}") print(f"[RETRY {retry_round}/{FAILED_STORES_MAX_RETRIES}] {len(failed_stores)} lojas falharam: {', '.join(failed_stores[:20])}{'...' if len(failed_stores) > 20 else ''}") print(f"Aguardando {FAILED_STORES_RETRY_DELAY} segundos antes de tentar novamente...") print(f"{'=' * 60}") time.sleep(FAILED_STORES_RETRY_DELAY) # Renovar token antes de tentar novamente print(f"\n[TOKEN] Renovando token antes do retry...") new_token = get_new_token() if new_token: TOKEN = new_token BASE_HEADERS["authorization"] = TOKEN BASE_HEADERS["x-authorization"] = TOKEN # Guardar lojas antes do retry para comparar depois lojas_antes_retry = set(failed_stores) # Tentar processar as lojas que falharam stores_to_retry = failed_stores.copy() failed_stores, TOKEN = process_stores(stores_to_retry, BASE_HEADERS, TOKEN, stats) # Identificar lojas que foram recuperadas neste retry lojas_recuperadas = lojas_antes_retry - set(failed_stores) stats["lojas_recuperadas_retry"].extend(lojas_recuperadas) if not failed_stores: print(f"\n[SUCESSO] Todas as lojas foram processadas com sucesso no retry {retry_round}!") # Guardar lojas que falharam definitivamente stats["lojas_falha_final"] = failed_stores.copy() # Resumo final das lojas que ainda falharam if failed_stores: print(f"\n{'=' * 60}") print(f"[AVISO FINAL] {len(failed_stores)} lojas falharam após {FAILED_STORES_MAX_RETRIES} tentativas de retry:") print(f" {', '.join(failed_stores)}") print(f"{'=' * 60}") # =============================== # 6) RELATÓRIO FINAL # =============================== print("\n") print("=" * 70) print(" RELATÓRIO FINAL DE EXECUÇÃO") print("=" * 70) print("\n📊 ESTATÍSTICAS DE LOJAS:") print(f" Total de lojas processadas: {stats['total_lojas']}") print(f" Lojas com sucesso na 1ª tentativa: {stats['lojas_sucesso_primeira']}") print(f" Lojas que falharam inicialmente: {len(stats['lojas_falha_inicial'])}") print(f" Lojas recuperadas após retry: {len(stats['lojas_recuperadas_retry'])}") print(f" Lojas que falharam definitivamente: {len(stats['lojas_falha_final'])}") if stats['lojas_recuperadas_retry']: print(f"\n Lojas recuperadas no retry:") for loja in stats['lojas_recuperadas_retry']: print(f" - {loja}") if stats['lojas_falha_final']: print(f"\n Lojas com falha definitiva:") for loja in stats['lojas_falha_final']: print(f" - {loja}") print("\n📦 ESTATÍSTICAS DE DADOS:") print(f" Pedidos únicos processados: {stats['pedidos_unicos']}") print(f" Registros deletados do banco: {stats['registros_deletados']}") print(f" Registros inseridos no banco: {stats['registros_inseridos']}") # Calcular taxa de sucesso taxa_sucesso = ((stats['total_lojas'] - len(stats['lojas_falha_final'])) / stats['total_lojas'] * 100) if stats['total_lojas'] > 0 else 0 print("\n📈 RESUMO:") print(f" Taxa de sucesso: {taxa_sucesso:.1f}%") if len(stats['lojas_falha_final']) == 0: print(f" Status: ✓ SUCESSO TOTAL") elif len(stats['lojas_falha_final']) < stats['total_lojas']: print(f" Status: ⚠ SUCESSO PARCIAL") else: print(f" Status: ✗ FALHA TOTAL") print("\n" + "=" * 70) print(" FIM DO RELATÓRIO") print("=" * 70)