diff --git a/estoque_mar.py b/estoque_mar.py new file mode 100644 index 0000000..4922b87 --- /dev/null +++ b/estoque_mar.py @@ -0,0 +1,589 @@ +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', 'loja_id' +] + +# 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", "23701", "23702", "23705", "23706", "23665", "23709", "23708", + "23713", "23707", "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" +] + +LOJAS_GRUPO_2 = [ + "20992", "21383", "23704", "23703", "20986", "24293", "23712", "20994", "23711", + "24269", "21000", "21001", "21375", "20970", "20989", "22541", "20988", "20993", + "20999", "24255", "24257", "20991", "20969", "20998", "20996", "20997", "20995", + "21495", "20968", "21278" +] + + +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=Iphone2513@;' + 'PORT=1433;' + 'TrustServerCertificate=yes' + ) + return conn + except Exception as e: + print(f"✗ Erro ao conectar ao banco de dados: {e}") + raise + + +def limpar_tabela(conn): + """Limpa a tabela estoque_mar.""" + try: + cursor = conn.cursor() + print(" Limpando tabela estoque_mar...") + cursor.execute("DELETE FROM [GINSENG].[dbo].[estoque_mar]") + conn.commit() + print(" ✓ Tabela limpa com sucesso!") + except Exception as e: + print(f" ✗ Erro ao limpar tabela: {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 + + # Define loja_id como igual ao PDV + df_unificado['loja_id'] = df_unificado['PDV'] + + # 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): + """Envia os dados do DataFrame para o banco.""" + 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', 'loja_id']: + 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) + + cursor.execute(""" + INSERT INTO [GINSENG].[dbo].[estoque_mar] ( + [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], [loja_id] + ) 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. + + 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 a tabela + limpar_tabela(conn) + + # 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 + if enviar_para_banco(conn, df_unificado): + 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"{'='*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() +