G-Scripts/estoque_mar.py
daniel.rodrigues 9beacb963c att
2026-02-19 17:37:19 -03:00

621 lines
22 KiB
Python

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()