import requests import json import time import os import pandas as pd import pyodbc import warnings from datetime import datetime, timezone, timedelta # Suprimir avisos do openpyxl warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl') # Configurações DIRETORIO_TEMP = "/tmp/download" # Lista de colunas na ordem exata do banco COLUNAS_BANCO = [ 'SKU', 'SKU_PARA', 'DESCRICAO', 'CATEGORIA', 'CLASSE', 'FASES PRODUTO', 'LANCAMENTO', 'DESATIVACAO', 'PDV', 'ESTOQUE ATUAL', 'ESTOQUE EM TRANSITO', 'PEDIDO PENDENTE', 'COBERTURA ALVO', 'ESTOQUE DE SEGURANCA', 'DDV PREVISTO', 'COBERTURA ATUAL', 'COBERTURA ATUAL + TRANSITO', 'COBERTURA PROJETADA', 'ORIGEM' ] # Headers comuns para as requisições HEADERS_API = { "accept": "*/*", "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", "authorization": "Basic b2NVc2VySW50ZXJuYWw6Nk5RV0BOU2M1anpEUy1oeg==", "content-type": "application/json", "origin": "https://extranet.grupoboticario.com.br", "priority": "u=1, i", "referer": "https://extranet.grupoboticario.com.br/", "sec-ch-ua": '"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "cross-site", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "x-authorization": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InV6YkJManZabTJxVDRsSERBZXdBX3Ewd2ZscTQtVGJnZmhVUzBBUE5HVzQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhNmNkNGZlNi0zZDcxLTQ1NWEtYjk5ZC1mNDU4YTA3Y2MwZDEiLCJpc3MiOiJodHRwczovL2xvZ2luLmV4dHJhbmV0LmdydXBvYm90aWNhcmlvLmNvbS5ici8xZTYzOTJiZC01Mzc3LTQ4ZjAtOWE4ZS00NjdmNWIzODFiMTgvdjIuMC8iLCJleHAiOjE3NTAyNjg5NjAsIm5iZiI6MTc1MDI2NTM2MCwic3ViIjoiNTI0MDliZmYtODNlNC00MjliLThmNGEtYzdjMDc2MGZiMmNlIiwiZW1haWwiOiJkYW5pZWwucm9kcmlndWVAZS1ib3RpY2FyaW8uY29tLmJyIiwibmFtZSI6IkRhbmllbCBKb3NlIE1lZGVpcm9zIFJvZHJpZ3VlcyIsImdpdmVuX25hbWUiOiJEYW5pZWwiLCJmYW1pbHlfbmFtZSI6IlJvZHJpZ3VlcyIsImV4dGVuc2lvbl9DUEYiOiIxMTExMzE3NDQ1NSIsInN0b3JlcyI6WyI0NDk0Il0sInJvbGVzIjpbIkNSRURfQURNIiwiR0NfTEVJVFVSQSIsIkxJVkVTX0ZEViIsIk1BUl9GUkFOUVVFQURPX0FETUlOIiwiUEJfQURNX1BBR0FET1IiLCJQR0lfUkVTVUxUQURPX1BEViIsIlZESV9TVUYiXSwiY3AiOiIxMDI2OSIsInJlZ2lzdHJhdGlvbiI6Ijg2MDk4NDAwIiwibGlueG9tcyI6ImNvbGFib3JhZG9yIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwic2NwIjoiZXh0cmFuZXQuYXBpIiwiYXpwIjoiYjMwMDFlNjAtYThlMC00ZGE4LTgyYmEtYzNhNzAxNDA1ZjA4IiwidmVyIjoiMS4wIiwiaWF0IjoxNzUwMjY1MzYwfQ.klcrMK2sx7GEJlMx_dbwgopc1RFjJwBLh0kLqjLlk__POtHNpJKti42r6xSuAM5AnieVMx0koK2oyg3eGoQJEchttsr4LyVoqSpcKzrqTR69gEHbTMo-EWWh_UglM6tr7ge6dzMF4yg-R_2XHwlrNEoYthVEVnSF1cqBxCHdTlRJcmso0q3ObGUU4heA-55OkzBjZ2Nz1mL7MmujZmNHlQXsoQ2vtOnnM3Ui7SAy08jGsIAIHdH8UKy0Xg-GrzjVrUwqAmyadSpXnvLc1sAE--bbtxP-3ADmnvHdxffkfQFtbPC0lws0MgESaqrIn0I9X6_OkAnUuMA_cCD9QJoyTA", "x-correlation-id": "15508cc3-fa35-47bf-ab19-8361f870e197", "x-user-id": "163165", "x-username": "daniel.rodrigue" } # Configurações dos dois grupos de lojas LOJAS_GRUPO_1 = [ "24268", "24258", "24454", "23702", "24455", "24450", "23665", "24448", "24447", "23713", "24449", "23156", "24254", "24253", "23813", "20056", "23475", "3546", "21647", "12824", "14617", "4560", "21068", "21277", "21296", "21381", "13427", "21624", "19103", "14668", "20006", "20057", "20005", "20009", "5699", "12522", "12817", "12820", "12829", "12818", "12823", "12826", "12828", "12830", "12838", "20441", "20858", "21007", "910173", "910291", "24455" ] LOJAS_GRUPO_2 = [ "20992", "21383", "24458", "23703", "20986", "24293", "24451", "20994", "23711", "24269", "21000", "21001", "21375", "20970", "20989", "22541", "20988", "20993", "20999", "24255", "24257", "20991", "20969", "20998", "20996", "20997", "20995", "21495", "20968", "21278", "24458" ] def criar_diretorio_temp(): """Cria o diretório temporário se não existir.""" os.makedirs(DIRETORIO_TEMP, exist_ok=True) print(f"✓ Diretório temporário criado/verificado: {DIRETORIO_TEMP}") def fazer_requisicao_stock(store_codes, nome_grupo): """ Faz uma requisição para a API de exportação de stock. Args: store_codes: Lista de códigos de lojas nome_grupo: Nome do grupo para identificação Returns: dict: Response da API ou None se erro """ url = "https://mar-orders-bff-api.demanda-abastecimento.grupoboticario.digital/api/export/STOCK" payload = { "storeCodes": store_codes, "cpId": 10269, "fileType": "XLSX", "userId": "163165", "metadata": { "storeCodes": store_codes, "userName": "Daniel Jose Medeiros Rodrigues", "fileFormattedName": f"Consulta de estoque {nome_grupo}.XLSX", "create_at": datetime.now(timezone.utc).isoformat(), "exportType": "STOCK" }, "validateOnRequest": True } try: print(f" Fazendo requisição para {nome_grupo}...") response = requests.post(url, headers=HEADERS_API, json=payload) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f" ✗ Erro na requisição para {nome_grupo}: {e}") return None except json.JSONDecodeError as e: print(f" ✗ Erro ao decodificar JSON para {nome_grupo}: {e}") return None def tentar_download(request_id): """ Tenta fazer o download da exportação usando o requestId. Args: request_id: ID da requisição Returns: dict: Resultado da API de download ou None se erro """ url = f"https://mar-orders-bff-api.demanda-abastecimento.grupoboticario.digital/api/export/{request_id}/download" params = {"redirect": "false"} try: response = requests.get(url, headers=HEADERS_API, params=params) response.raise_for_status() return response.json() except: return None def aguardar_download_disponivel(request_id, nome_grupo, timeout_minutes=90): """ Aguarda até que o download esteja disponível. Args: request_id: ID da requisição para aguardar nome_grupo: Nome do grupo para identificação timeout_minutes: Tempo limite em minutos (padrão: 90 min) Returns: dict: Resultado final com URL de download ou None se erro/timeout """ print(f" Aguardando download de {nome_grupo} (ID: {request_id})...") timeout_seconds = timeout_minutes * 60 start_time = time.time() tentativas = 0 while True: elapsed_time = time.time() - start_time if elapsed_time > timeout_seconds: print(f" ✗ Timeout atingido para {nome_grupo} ({timeout_minutes} minutos)") return None tentativas += 1 resultado_download = tentar_download(request_id) if resultado_download and 'fileUrl' in resultado_download: print(f" ✓ Download de {nome_grupo} disponível após {tentativas} tentativas!") return resultado_download if tentativas % 6 == 0: # Log a cada minuto (6 tentativas de 10s) print(f" ... Aguardando {nome_grupo} ({tentativas} tentativas, {int(elapsed_time)}s)") time.sleep(10) def baixar_e_salvar_arquivo(file_url, nome_arquivo): """ Baixa o arquivo da URL fornecida e salva no diretório temporário. Args: file_url: URL do arquivo para download nome_arquivo: Nome do arquivo a ser salvo Returns: str: Caminho completo do arquivo salvo ou None se erro """ try: caminho_completo = os.path.join(DIRETORIO_TEMP, nome_arquivo) print(f" Baixando {nome_arquivo}...") response = requests.get(file_url, stream=True) response.raise_for_status() with open(caminho_completo, 'wb') as arquivo: for chunk in response.iter_content(chunk_size=8192): if chunk: arquivo.write(chunk) if os.path.exists(caminho_completo) and os.path.getsize(caminho_completo) > 0: tamanho_mb = os.path.getsize(caminho_completo) / (1024 * 1024) print(f" ✓ Arquivo baixado: {nome_arquivo} ({tamanho_mb:.2f} MB)") return caminho_completo else: print(f" ✗ Erro: Arquivo não foi salvo corretamente") return None except Exception as e: print(f" ✗ Erro ao baixar arquivo: {e}") return None def enviar_status_api(status_id, status="OK"): """ Envia o status da execução para a API. Args: status_id: ID do status (2 para grupo 1, 5 para grupo 2) status: Status a ser enviado, "OK" ou "FAIL" """ url = f"https://api.grupoginseng.com.br/api/status/{status_id}" sao_paulo_offset = timedelta(hours=-3) current_datetime = datetime.now(timezone(sao_paulo_offset)).strftime("%Y-%m-%d %H:%M:%S") payload = {"STATUS": status, "DATA": current_datetime} headers = {"Content-Type": "application/json"} try: response = requests.put(url, json=payload, headers=headers) print(f" Status enviado para API (ID {status_id}): {status} - {response.status_code}") except Exception as e: print(f" ✗ Erro ao enviar status: {e}") def processar_download_grupo(store_codes, nome_grupo, nome_arquivo, status_id): """ Processa o download de um grupo de lojas. Args: store_codes: Lista de códigos de lojas nome_grupo: Nome do grupo para identificação nome_arquivo: Nome do arquivo a ser salvo status_id: ID do status para enviar à API Returns: str: Caminho do arquivo baixado ou None se erro """ print(f"\n{'='*60}") print(f"PROCESSANDO {nome_grupo}") print(f"{'='*60}") # 1. Fazer requisição inicial resultado_inicial = fazer_requisicao_stock(store_codes, nome_grupo) if not resultado_inicial: enviar_status_api(status_id, "FAIL") return None request_id = resultado_inicial.get('id') or resultado_inicial.get('requestId') if not request_id: print(f" ✗ ID da requisição não encontrado para {nome_grupo}") enviar_status_api(status_id, "FAIL") return None print(f" ✓ Requisição iniciada - ID: {request_id}") # 2. Aguardar download ficar disponível resultado_download = aguardar_download_disponivel(request_id, nome_grupo) if not resultado_download: enviar_status_api(status_id, "FAIL") return None file_url = resultado_download.get('fileUrl', '') if not file_url: print(f" ✗ URL do arquivo não encontrada para {nome_grupo}") enviar_status_api(status_id, "FAIL") return None # 3. Baixar arquivo arquivo_salvo = baixar_e_salvar_arquivo(file_url, nome_arquivo) if arquivo_salvo: enviar_status_api(status_id, "OK") return arquivo_salvo else: enviar_status_api(status_id, "FAIL") return None # ============================================================================ # FUNÇÕES DE UPLOAD PARA O BANCO DE DADOS # ============================================================================ def conectar_banco(): """Estabelece conexão com o banco de dados.""" try: conn = pyodbc.connect( 'DRIVER={ODBC Driver 18 for SQL Server};' 'SERVER=10.77.77.10;' 'DATABASE=GINSENG;' 'UID=supginseng;' 'PWD=Ginseng@;' 'PORT=1433;' 'TrustServerCertificate=yes' ) return conn except Exception as e: print(f"✗ Erro ao conectar ao banco de dados: {e}") raise def limpar_dados_data_atual(conn): """Remove dados da tabela estoque_mar_historico para a data do estoque (dia atual).""" try: cursor = conn.cursor() # Data do estoque é o dia atual data_estoque = datetime.now().strftime("%Y-%m-%d") # Verificar se existem dados para a data do estoque cursor.execute( "SELECT COUNT(*) FROM [GINSENG].[dbo].[estoque_mar_historico] WHERE CAST([data_estoque] AS DATE) = ?", (data_estoque,) ) count = cursor.fetchone()[0] if count > 0: print(f" Encontrados {count} registros para a data {data_estoque}") print(f" Removendo dados existentes da data {data_estoque}...") cursor.execute( "DELETE FROM [GINSENG].[dbo].[estoque_mar_historico] WHERE CAST([data_estoque] AS DATE) = ?", (data_estoque,) ) conn.commit() print(f" ✓ {count} registros removidos com sucesso!") else: print(f" Nenhum registro encontrado para a data {data_estoque}") return data_estoque except Exception as e: print(f" ✗ Erro ao limpar dados da data do estoque: {e}") raise def unificar_arquivo(caminho_arquivo): """Unifica as três páginas (BOT, EUD, QDB) de um arquivo Excel.""" try: # Lê cada página do arquivo df_bot = pd.read_excel(caminho_arquivo, sheet_name='BOT') df_eud = pd.read_excel(caminho_arquivo, sheet_name='EUD') df_qdb = pd.read_excel(caminho_arquivo, sheet_name='QDB') # Adiciona coluna de origem df_bot['ORIGEM'] = 'BOT' df_eud['ORIGEM'] = 'EUD' df_qdb['ORIGEM'] = 'QDB' # Concatena os DataFrames df_unificado = pd.concat([df_bot, df_eud, df_qdb], ignore_index=True) # Garante que todas as colunas necessárias existem for coluna in COLUNAS_BANCO: if coluna not in df_unificado.columns: df_unificado[coluna] = None # Reordena as colunas na ordem correta do banco df_unificado = df_unificado[COLUNAS_BANCO] return df_unificado except Exception as e: print(f" ✗ Erro ao unificar arquivo {caminho_arquivo}: {e}") return None def formatar_data(valor): """Formata uma data no formato YYYYMM.""" try: if pd.isna(valor) or valor == '-': return None data_str = str(int(float(valor))) if len(data_str) >= 6: return data_str[:6] return None except: return None def formatar_numero(valor, max_length=50): """Formata um número como string, usando vírgula como separador decimal.""" try: if pd.isna(valor) or valor == '-': return None num = float(str(valor).replace(',', '.')) if num.is_integer(): return str(int(num))[:max_length] return f"{num:.2f}".replace('.', ',')[:max_length] except: return None def enviar_para_banco(conn, df, data_estoque): """ Envia os dados do DataFrame para o banco na tabela estoque_mar_historico. Args: conn: Conexão com o banco de dados df: DataFrame com os dados a serem inseridos data_estoque: Data do estoque no formato YYYY-MM-DD Returns: bool: True se sucesso, False se erro """ try: cursor = conn.cursor() total_linhas = len(df) linhas_processadas = 0 erros = 0 for _, row in df.iterrows(): try: valores = [] for coluna in COLUNAS_BANCO: valor = row[coluna] if pd.isna(valor) or valor == '-': valores.append(None) elif coluna in ['SKU', 'PDV']: valores.append(str(int(float(valor)))[:50]) elif coluna in ['LANCAMENTO', 'DESATIVACAO']: valores.append(formatar_data(valor)) elif coluna in ['ESTOQUE ATUAL', 'ESTOQUE EM TRANSITO', 'PEDIDO PENDENTE', 'COBERTURA ALVO', 'ESTOQUE DE SEGURANCA', 'DDV PREVISTO', 'COBERTURA ATUAL', 'COBERTURA ATUAL + TRANSITO', 'COBERTURA PROJETADA']: valores.append(formatar_numero(valor)) elif coluna == 'DESCRICAO': valores.append(str(valor)[:255] if pd.notna(valor) else None) elif coluna in ['CATEGORIA', 'CLASSE', 'FASES PRODUTO', 'ORIGEM']: valores.append(str(valor)[:100] if pd.notna(valor) else None) else: if isinstance(valor, (int, float)): valor = str(int(valor)) valores.append(str(valor)[:50] if pd.notna(valor) else None) # Adiciona a data_estoque ao final dos valores valores.append(data_estoque) cursor.execute(""" INSERT INTO [GINSENG].[dbo].[estoque_mar_historico] ( [SKU], [SKU_PARA], [DESCRICAO], [CATEGORIA], [CLASSE], [FASES PRODUTO], [LANCAMENTO], [DESATIVACAO], [PDV], [ESTOQUE ATUAL], [ESTOQUE EM TRANSITO], [PEDIDO PENDENTE], [COBERTURA ALVO], [ESTOQUE DE SEGURANCA], [DDV PREVISTO], [COBERTURA ATUAL], [COBERTURA ATUAL + TRANSITO], [COBERTURA PROJETADA], [ORIGEM], [data_estoque] ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, valores) linhas_processadas += 1 if linhas_processadas % 5000 == 0: conn.commit() print(f" Progresso: {linhas_processadas}/{total_linhas} linhas inseridas") except Exception as e: erros += 1 if erros <= 3: print(f" ✗ Erro ao inserir linha: {e}") continue conn.commit() print(f" ✓ Total de linhas processadas: {linhas_processadas}") if erros > 0: print(f" ⚠ Total de erros: {erros}") return linhas_processadas > 0 except Exception as e: print(f" ✗ Erro ao enviar dados para o banco: {e}") conn.rollback() return False def processar_upload_banco(arquivos): """ Processa o upload dos arquivos para o banco de dados na tabela estoque_mar_historico. Args: arquivos: Lista de caminhos dos arquivos a processar Returns: bool: True se sucesso, False se erro """ print(f"\n{'='*60}") print("PROCESSANDO UPLOAD PARA O BANCO DE DADOS") print(f"{'='*60}") try: # Conecta ao banco print(" Conectando ao banco de dados...") conn = conectar_banco() print(" ✓ Conectado ao banco de dados") # Limpa dados da data atual e obtém a data data_estoque = limpar_dados_data_atual(conn) print(f" Data do estoque: {data_estoque}") # Processa cada arquivo arquivos_processados = 0 for arquivo in arquivos: if not arquivo or not os.path.exists(arquivo): print(f" ⚠ Arquivo não encontrado: {arquivo}") continue nome_arquivo = os.path.basename(arquivo) print(f"\n Processando arquivo: {nome_arquivo}") # Unifica as páginas do arquivo df_unificado = unificar_arquivo(arquivo) if df_unificado is None: print(f" ✗ Falha ao unificar arquivo: {nome_arquivo}") continue print(f" Total de linhas: {len(df_unificado)}") # Envia para o banco com a data_estoque if enviar_para_banco(conn, df_unificado, data_estoque): arquivos_processados += 1 print(f" ✓ Arquivo enviado ao banco com sucesso: {nome_arquivo}") else: print(f" ✗ Falha ao enviar arquivo: {nome_arquivo}") # Fecha a conexão conn.close() print(f"\n{'='*60}") print(f"✓ Upload finalizado: {arquivos_processados}/{len(arquivos)} arquivos processados") print(f"✓ Dados inseridos na tabela estoque_mar_historico com data_estoque = {data_estoque}") print(f"{'='*60}") return arquivos_processados > 0 except Exception as e: print(f"✗ Erro durante o upload: {e}") return False def limpar_arquivos_temporarios(): """Remove os arquivos temporários baixados.""" try: if os.path.exists(DIRETORIO_TEMP): for arquivo in os.listdir(DIRETORIO_TEMP): caminho = os.path.join(DIRETORIO_TEMP, arquivo) try: os.remove(caminho) print(f" ✓ Arquivo removido: {arquivo}") except Exception as e: print(f" ⚠ Erro ao remover {arquivo}: {e}") # Remove o diretório se estiver vazio try: os.rmdir(DIRETORIO_TEMP) print(f" ✓ Diretório temporário removido") except: pass except Exception as e: print(f" ⚠ Erro ao limpar arquivos temporários: {e}") def main(): """Função principal que executa todo o processo.""" print("\n" + "="*60) print("ESTOQUE MAR - PROCESSO UNIFICADO") print("="*60) print(f"Início: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print("="*60) # Criar diretório temporário criar_diretorio_temp() # Lista para armazenar os arquivos baixados arquivos_baixados = [] # 1. Baixar arquivo do Grupo 1 (Lojas) arquivo_loja = processar_download_grupo( LOJAS_GRUPO_1, "GRUPO 1 - LOJAS", "Estoque_mar_loja.xlsx", 2 # status_id ) if arquivo_loja: arquivos_baixados.append(arquivo_loja) # 2. Baixar arquivo do Grupo 2 (VD) arquivo_vd = processar_download_grupo( LOJAS_GRUPO_2, "GRUPO 2 - VD", "Estoque_mar_VD.xlsx", 5 # status_id ) if arquivo_vd: arquivos_baixados.append(arquivo_vd) # 3. Upload para o banco de dados if arquivos_baixados: sucesso_upload = processar_upload_banco(arquivos_baixados) else: print("\n✗ Nenhum arquivo foi baixado. Abortando upload.") sucesso_upload = False # 4. Limpar arquivos temporários print(f"\n{'='*60}") print("LIMPANDO ARQUIVOS TEMPORÁRIOS") print(f"{'='*60}") limpar_arquivos_temporarios() # Resumo final print("\n" + "="*60) print("RESUMO FINAL") print("="*60) print(f"Arquivos baixados: {len(arquivos_baixados)}/2") print(f"Upload para banco: {'✓ SUCESSO' if sucesso_upload else '✗ FALHA'}") print(f"Fim: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print("="*60 + "\n") return sucesso_upload if __name__ == "__main__": main()