G-Scripts/Pedidos_mar.py
daniel.rodrigues 0ac2401d76 att
2026-02-11 12:08:26 -03:00

712 lines
27 KiB
Python

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)