first commit

This commit is contained in:
daniel.rodrigues 2026-02-20 09:00:12 -03:00
commit e250eb3cf6
13 changed files with 8123 additions and 0 deletions

602
Grgb_sale_receipts.py Normal file
View File

@ -0,0 +1,602 @@
import requests
import pyodbc
from datetime import datetime, timedelta
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from queue import Queue
from threading import Thread, Event, Lock
# =====================================================
# CONFIGURAÇÕES DO BANCO DE DADOS SQL SERVER
# =====================================================
CONNECTION_STRING = (
"DRIVER={ODBC Driver 18 for SQL Server};"
"SERVER=10.77.77.10;"
"DATABASE=GINSENG;"
"UID=supginseng;"
"PWD=Ginseng@;"
"PORT=1433;"
"TrustServerCertificate=yes"
)
# =====================================================
# CONFIGURAÇÕES DAS APIs
# =====================================================
API_TOKEN_URL = "https://api.grupoginseng.com.br/api/rgb_token"
API_VENDAS_URL = "https://api.grupoboticario.com.br/global/v1/franchising/gb-stores-data/sale/receipts"
# =====================================================
# CONFIGURAÇÕES DE PARALELISMO
# =====================================================
MAX_WORKERS = 3 # Número de requisições simultâneas (reduzido para evitar 429)
PAGE_SIZE = 50 # Itens por página
MAX_RETRIES = 5 # Tentativas em caso de falha
RETRY_DELAY = 3 # Segundos entre tentativas
RATE_LIMIT_DELAY = 5 # Segundos extras para erro 429 (Too Many Requests)
REQUEST_DELAY = 0.5 # Delay entre cada requisição (respeitar rate limit)
# =====================================================
# GERENCIADOR DE TOKEN (com renovação automática)
# =====================================================
class TokenManager:
"""Gerencia o token com renovação automática quando expira"""
def __init__(self):
self._token = None
self._lock = Lock()
self._ultimo_refresh = None
def _buscar_novo_token(self):
"""Busca um novo token da API"""
try:
response = requests.get(API_TOKEN_URL, timeout=30)
response.raise_for_status()
data = response.json()
if data.get('success') and data.get('data'):
return data['data'][0]['token']
except Exception as e:
print(f" ✗ Erro ao buscar token: {e}")
return None
def obter_token(self, forcar_refresh=False):
"""Obtém o token atual ou busca um novo se necessário"""
with self._lock:
if self._token is None or forcar_refresh:
print(" 🔑 Obtendo novo token..." if forcar_refresh else "", end="")
novo_token = self._buscar_novo_token()
if novo_token:
self._token = novo_token
self._ultimo_refresh = datetime.now()
if forcar_refresh:
print("")
return self._token
else:
return None
return self._token
def renovar_token(self):
"""Força a renovação do token"""
return self.obter_token(forcar_refresh=True)
# Instância global do gerenciador de token
token_manager = TokenManager()
def obter_token():
"""Obtém o token de autenticação da API"""
token = token_manager.obter_token()
if token:
print(f"✓ Token obtido com sucesso")
else:
print("✗ Erro: Não foi possível obter o token")
return token
def buscar_pagina(data_venda, start, count, tentativa=1):
"""Busca uma página específica da API com retry automático e renovação de token"""
token = token_manager.obter_token()
# Pequeno delay antes de cada requisição para respeitar rate limit
time.sleep(REQUEST_DELAY)
try:
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
params = {
'receipt.saleDate': data_venda,
'start': start,
'count': count
}
response = requests.get(API_VENDAS_URL, headers=headers, params=params, timeout=30)
# Verifica se o token expirou (401 Unauthorized)
if response.status_code == 401:
print(f" 🔄 Token expirado, renovando...")
token_manager.renovar_token()
if tentativa < MAX_RETRIES:
return buscar_pagina(data_venda, start, count, tentativa + 1)
# Erro 429 - Too Many Requests (rate limit)
if response.status_code == 429:
if tentativa < MAX_RETRIES:
wait_time = RATE_LIMIT_DELAY * tentativa # Aumenta espera progressivamente
print(f" ⏳ Rate limit (429), aguardando {wait_time}s...")
time.sleep(wait_time)
return buscar_pagina(data_venda, start, count, tentativa + 1)
response.raise_for_status()
data = response.json()
# Log de sucesso após retry
if tentativa > 1:
print(f" ✓ Página {start} OK (após {tentativa-1} retry)")
return {
'start': start,
'items': data.get('items', []),
'total': data.get('total', 0),
'success': True
}
except requests.exceptions.HTTPError as e:
# Se for erro de autenticação, tenta renovar o token
if hasattr(e, 'response') and e.response is not None:
status = e.response.status_code
if status in [401, 403]:
print(f" 🔄 Erro de autenticação, renovando token...")
token_manager.renovar_token()
if tentativa < MAX_RETRIES:
time.sleep(RETRY_DELAY)
return buscar_pagina(data_venda, start, count, tentativa + 1)
# Erro 429 - Too Many Requests
if status == 429:
if tentativa < MAX_RETRIES:
wait_time = RATE_LIMIT_DELAY * tentativa
print(f" ⏳ Rate limit (429), aguardando {wait_time}s...")
time.sleep(wait_time)
return buscar_pagina(data_venda, start, count, tentativa + 1)
if tentativa < MAX_RETRIES:
print(f" ⚠ Retry página {start}, tentativa {tentativa}/{MAX_RETRIES}...")
time.sleep(RETRY_DELAY)
return buscar_pagina(data_venda, start, count, tentativa + 1)
else:
print(f" ✗ FALHOU página {start} após {MAX_RETRIES} tentativas: {e}")
return {
'start': start,
'items': [],
'total': 0,
'success': False,
'error': str(e)
}
except Exception as e:
if tentativa < MAX_RETRIES:
print(f" ⚠ Retry página {start}, tentativa {tentativa}/{MAX_RETRIES}...")
time.sleep(RETRY_DELAY)
return buscar_pagina(data_venda, start, count, tentativa + 1)
else:
print(f" ✗ FALHOU página {start} após {MAX_RETRIES} tentativas: {e}")
return {
'start': start,
'items': [],
'total': 0,
'success': False,
'error': str(e)
}
def obter_total_registros(data_venda):
"""Faz uma requisição inicial para descobrir o total de registros"""
resultado = buscar_pagina(data_venda, 0, 1)
if resultado['success']:
return resultado['total']
return 0
def parse_datetime(dt_string):
"""Converte string de datetime para formato SQL Server"""
if not dt_string:
return None
try:
dt_string = dt_string.replace('-03:00', '').replace('-02:00', '')
dt = datetime.fromisoformat(dt_string)
return dt.strftime('%Y-%m-%d %H:%M:%S')
except:
return None
def inserir_venda(cursor, venda):
"""Insere uma venda na tabela Grgb_sales_receipts usando MERGE (upsert)"""
sql = """
MERGE INTO Grgb_sales_receipts AS target
USING (SELECT ? AS id) AS source
ON target.id = source.id
WHEN MATCHED THEN
UPDATE SET
value = ?,
discount_value = ?,
invoice_xml_status = ?,
updated_at = GETDATE()
WHEN NOT MATCHED THEN
INSERT (
id, receipt_sequence, cash_register_number, store_id, coo,
employee_id, employee_name, value, additional_value, discount_value,
items_quantity, units_quantity, cancelled_items_quantity, cancelled_items_value,
sale_type, cancelled_units_quantity, sale_date, invoice_xml_status,
receipt_opening_datetime, receipt_closing_datetime, eletronic_key,
sale_order_id, external_id, discount_reason, loyalty_discount_value,
cancelling_reason, cancelled_receipt_sequence, channel, channel_description
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"""
sale_id = venda.get('id')
valores = (
sale_id,
venda.get('value'),
venda.get('discountValue'),
venda.get('invoiceXMLStatus'),
sale_id,
venda.get('receiptSequence'),
venda.get('cashRegisterNumber'),
venda.get('storeId'),
venda.get('coo'),
venda.get('employeeId'),
venda.get('employeeName'),
venda.get('value'),
venda.get('additionalValue'),
venda.get('discountValue'),
venda.get('itemsQuantity'),
venda.get('unitsQuantity'),
venda.get('cancelledItemsQuantity'),
venda.get('cancelledItemsValue'),
venda.get('saleType'),
venda.get('cancelledUnitsQuantity'),
venda.get('saleDate'),
venda.get('invoiceXMLStatus'),
parse_datetime(venda.get('receiptOpeningDateTime')),
parse_datetime(venda.get('receiptClosingDateTime')),
venda.get('eletronicKey'),
venda.get('saleOrderId'),
venda.get('externalId'),
venda.get('discountReason'),
venda.get('loyaltyDiscountValue'),
venda.get('cancellingReason'),
venda.get('cancelledReceiptSequence'),
venda.get('channel'),
venda.get('channelDescription')
)
cursor.execute(sql, valores)
return sale_id
def inserir_itens(cursor, sale_id, itens):
"""Insere os itens de uma venda na tabela Grgb_sales_receipts_itemsvenda usando MERGE"""
sql = """
MERGE INTO Grgb_sales_receipts_itemsvenda AS target
USING (SELECT ? AS id) AS source
ON target.id = source.id
WHEN MATCHED THEN
UPDATE SET
quantity = ?,
total_value = ?,
cancelled = ?
WHEN NOT MATCHED THEN
INSERT (
id, sale_id, cancelled, product_id, seller_id, seller_name,
quantity, unit_value, gross_value, additional_value, discount_value,
total_value, tabela_a, ncm, ncm_excecao, natureza, cfop, csosn,
cst_icms, aliquota_icms, valor_reducao_aliquota_icms, valor_icms_desonerado,
valor_fecop, aliquota_fecop, cst_pis, aliquota_pis, cst_cofins, aliquota_cofins
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"""
for item in itens:
item_id = item.get('id')
valores = (
item_id,
item.get('quantity'),
item.get('totalValue'),
item.get('cancelled'),
item_id,
sale_id,
item.get('cancelled'),
item.get('productId'),
item.get('sellerId'),
item.get('sellerName'),
item.get('quantity'),
item.get('unitValue'),
item.get('grossValue'),
item.get('additionalValue'),
item.get('discountValue'),
item.get('totalValue'),
item.get('tabelaA'),
item.get('ncm'),
item.get('ncmExcecao'),
item.get('natureza'),
item.get('cfop'),
item.get('csosn'),
item.get('cstICMS'),
item.get('aliquotaICMS'),
item.get('valorReducaoAliquotaICMS'),
item.get('valorICMSDesonerado'),
item.get('valorFecop'),
item.get('aliquotaFecop'),
item.get('cstPIS'),
item.get('aliquotaPIS'),
item.get('cstCOFINS'),
item.get('aliquotaCOFINS')
)
cursor.execute(sql, valores)
def inserir_pagamentos(cursor, sale_id, pagamentos):
"""Insere os pagamentos de uma venda na tabela Grgb_sales_receipts_pagamentosvenda usando MERGE"""
sql = """
MERGE INTO Grgb_sales_receipts_pagamentosvenda AS target
USING (SELECT ? AS id) AS source
ON target.id = source.id
WHEN MATCHED THEN
UPDATE SET
value = ?,
payment_method_description = ?
WHEN NOT MATCHED THEN
INSERT (
id, sale_id, payment_method_id, payment_method_description, value,
change_value, installment_quantity, check_issuer, card_authorization,
card_flag, card_flag_description, card_modality, rede_adquirente,
nsu, authorization_nsu, nsu_cancelling, card_bin_number
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"""
for pagamento in pagamentos:
pag_id = pagamento.get('id')
valores = (
pag_id,
pagamento.get('value'),
pagamento.get('paymentMethodDescription'),
pag_id,
sale_id,
pagamento.get('paymentMethodId'),
pagamento.get('paymentMethodDescription'),
pagamento.get('value'),
pagamento.get('change'),
pagamento.get('installmentQuantity'),
pagamento.get('checkIssuer'),
pagamento.get('cardAuthorization'),
pagamento.get('cardFlag'),
pagamento.get('cardFlagDescription'),
pagamento.get('cardModality'),
pagamento.get('redeAdquirente'),
pagamento.get('nsu'),
pagamento.get('authorizationNsu'),
pagamento.get('nsuCancelling'),
pagamento.get('cardBinNumber')
)
cursor.execute(sql, valores)
def worker_inserir_banco(fila, conn_string, stats, stop_event):
"""Worker que consome a fila e insere no banco"""
conn = pyodbc.connect(conn_string)
cursor = conn.cursor()
while not stop_event.is_set() or not fila.empty():
try:
vendas = fila.get(timeout=1)
except:
continue
for venda in vendas:
try:
sale_id = inserir_venda(cursor, venda)
stats['vendas'] += 1
itens = venda.get('items', [])
if itens:
inserir_itens(cursor, sale_id, itens)
stats['itens'] += len(itens)
pagamentos = venda.get('payments', [])
if pagamentos:
inserir_pagamentos(cursor, sale_id, pagamentos)
stats['pagamentos'] += len(pagamentos)
except Exception:
stats['erros'] += 1
# Commit a cada lote
conn.commit()
fila.task_done()
cursor.close()
conn.close()
def deletar_dados_existentes(data_venda):
"""Deleta dados existentes para a data especificada antes de inserir novos"""
try:
conn = pyodbc.connect(CONNECTION_STRING)
cursor = conn.cursor()
# Deletar da tabela principal (itens e pagamentos são deletados automaticamente pelo CASCADE)
cursor.execute("DELETE FROM Grgb_sales_receipts WHERE sale_date = ?", data_venda)
deleted_count = cursor.rowcount
conn.commit()
if deleted_count > 0:
print(f" 🗑️ Deletados: {deleted_count} registros existentes")
cursor.close()
conn.close()
return deleted_count
except Exception as e:
print(f" ⚠ Erro ao deletar dados existentes: {e}")
return 0
def processar_dia(data_venda, fila, stats):
"""Processa um dia específico - busca da API e coloca na fila"""
# Descobrir total de registros para este dia
total = obter_total_registros(data_venda)
if total == 0:
print(f" {data_venda}: Nenhum registro")
return True # Sucesso, apenas não tem dados
print(f" {data_venda}: {total} registros encontrados na API")
# Deletar dados existentes para esta data antes de inserir os novos
deletar_dados_existentes(data_venda)
print(f" {data_venda}: Buscando dados da API...")
# Calcular páginas
paginas = list(range(0, total, PAGE_SIZE))
obtidas = 0
falhas = 0
# Buscar páginas em paralelo
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = {
executor.submit(buscar_pagina, data_venda, start, PAGE_SIZE): start
for start in paginas
}
for future in as_completed(futures):
resultado = future.result()
if resultado['success'] and resultado['items']:
# Coloca na fila para o worker do banco processar
fila.put(resultado['items'])
obtidas += len(resultado['items'])
stats['api_obtidas'] += len(resultado['items'])
elif not resultado['success']:
falhas += 1
if falhas > 0:
print(f" {data_venda}: ⚠ {obtidas} vendas OK, {falhas} páginas falharam")
else:
print(f" {data_venda}: ✓ {obtidas} vendas enviadas para o banco")
return falhas == 0
def gerar_datas(data_inicio, data_fim):
"""Gera lista de datas entre início e fim"""
datas = []
atual = data_inicio
while atual <= data_fim:
datas.append(atual.strftime('%Y-%m-%d'))
atual += timedelta(days=1)
return datas
def processar_periodo(data_inicio_str, data_fim_str):
"""Processo principal - processa um período de datas"""
data_inicio = datetime.strptime(data_inicio_str, '%Y-%m-%d')
data_fim = datetime.strptime(data_fim_str, '%Y-%m-%d')
datas = gerar_datas(data_inicio, data_fim)
print("=" * 60)
print(f"IMPORTAÇÃO DE VENDAS")
print(f"Período: {data_inicio_str} até {data_fim_str} ({len(datas)} dias)")
print("=" * 60)
# 1. Obter token inicial
print("\n[1/3] Obtendo token de autenticação...")
token = obter_token()
if not token:
return False
# 2. Configurar pipeline
print("\n[2/3] Iniciando pipeline (API → Banco)...")
fila = Queue(maxsize=20) # Buffer de 20 lotes
stop_event = Event()
stats = {'vendas': 0, 'itens': 0, 'pagamentos': 0, 'erros': 0, 'api_obtidas': 0}
# Iniciar worker do banco
worker = Thread(target=worker_inserir_banco, args=(fila, CONNECTION_STRING, stats, stop_event))
worker.start()
# 3. Processar cada dia
print("\n[3/3] Processando dias...")
inicio = time.time()
dias_com_falha = []
try:
for i, data_venda in enumerate(datas, 1):
print(f"\n[{i}/{len(datas)}] ", end="")
sucesso = processar_dia(data_venda, fila, stats)
if not sucesso:
dias_com_falha.append(data_venda)
# Mostrar stats parciais a cada 5 dias
if i % 5 == 0:
print(f" 📊 Parcial: {stats['vendas']} vendas, {stats['itens']} itens, {stats['pagamentos']} pagamentos no banco")
except KeyboardInterrupt:
print("\n\n⚠ Interrompido pelo usuário!")
# Sinalizar fim e aguardar worker
print("\n\nFinalizando inserções pendentes...")
stop_event.set()
fila.join()
worker.join()
tempo_total = time.time() - inicio
# Resultado final
print("\n" + "=" * 60)
print("IMPORTAÇÃO CONCLUÍDA!")
print("=" * 60)
print(f" Período: {data_inicio_str} até {data_fim_str}")
print(f" Dias processados: {len(datas)}")
print(f" Vendas inseridas: {stats['vendas']}")
print(f" Itens inseridos: {stats['itens']}")
print(f" Pagamentos inseridos: {stats['pagamentos']}")
print(f" Erros de inserção: {stats['erros']}")
print(f" Tempo total: {tempo_total:.1f}s ({tempo_total/60:.1f} min)")
if dias_com_falha:
print(f"\n ⚠ Dias com falhas parciais: {', '.join(dias_com_falha)}")
print("=" * 60)
return True
if __name__ == "__main__":
# Configuração do período - sempre pega o dia anterior (ontem)
ONTEM = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
DATA_INICIO = ONTEM
DATA_FIM = ONTEM
# Permite passar datas como argumentos: python script.py 2025-09-24 2025-12-31
if len(sys.argv) >= 3:
DATA_INICIO = sys.argv[1]
DATA_FIM = sys.argv[2]
elif len(sys.argv) == 2:
# Se passar só uma data, processa só aquele dia
DATA_INICIO = sys.argv[1]
DATA_FIM = sys.argv[1]
processar_periodo(DATA_INICIO, DATA_FIM)

780
Notas-calamo.py Normal file
View File

@ -0,0 +1,780 @@
import requests
import pandas as pd
import json
import os
import time
import pyodbc
import base64
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# =============================================================================
# CONFIGURAÇÕES
# =============================================================================
# Configurações da API
API_URL = "https://api.arquivei.com.br/v1/nfe/received"
API_HEADERS = {
"X-API-ID": "3e51eeaeb4c678bb648801cbc545da9cc75682cf",
"X-API-KEY": "73d6941b0c948ac010b35c4f57506072dac44a4f",
"Content-Type": "application/json"
}
# CNPJs permitidos para filtragem
CNPJS_PERMITIDOS = {'06147451000990', '06147451000809'}
# Quantidade de dias para buscar (hoje + X dias para trás)
DIAS_PARA_BUSCAR = 1 # Altere aqui para mudar o período
# =============================================================================
# FUNÇÕES AUXILIARES
# =============================================================================
def create_session_with_retry():
"""Cria uma sessão de requests com configuração de retry e timeout"""
session = requests.Session()
try:
retry_strategy = Retry(
total=3,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS"],
backoff_factor=1
)
except TypeError:
try:
retry_strategy = Retry(
total=3,
status_forcelist=[429, 500, 502, 503, 504],
method_whitelist=["HEAD", "GET", "OPTIONS"],
backoff_factor=1
)
except TypeError:
retry_strategy = Retry(
total=3,
status_forcelist=[429, 500, 502, 503, 504],
backoff_factor=1
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
def get_db_connection():
"""Configuração da conexão SQL Server"""
return pyodbc.connect(
'DRIVER={ODBC Driver 18 for SQL Server};'
'SERVER=10.77.77.10;'
'DATABASE=GINSENG;'
'UID=supginseng;'
'PWD=Ginseng@;'
'PORT=1433;'
'TrustServerCertificate=yes'
)
def fazer_requisicao_robusta(session, url, headers, params, max_tentativas=3):
"""Faz uma requisição HTTP com tratamento robusto de timeout e erros"""
for tentativa in range(max_tentativas):
try:
print(f" Tentativa {tentativa + 1}/{max_tentativas}...", end=" ", flush=True)
response = session.get(url, headers=headers, params=params, timeout=(30, 60))
if response.status_code == 200:
print("Sucesso!")
return response
else:
print(f"Erro {response.status_code}")
if tentativa < max_tentativas - 1:
wait_time = (tentativa + 1) * 2
print(f" Aguardando {wait_time} segundos...")
time.sleep(wait_time)
except requests.exceptions.ConnectTimeout:
print("Timeout de conexão!")
if tentativa < max_tentativas - 1:
time.sleep((tentativa + 1) * 5)
except requests.exceptions.ReadTimeout:
print("Timeout de leitura!")
if tentativa < max_tentativas - 1:
time.sleep((tentativa + 1) * 3)
except requests.exceptions.ConnectionError as e:
print(f"Erro de conexão: {str(e)[:100]}...")
if tentativa < max_tentativas - 1:
time.sleep((tentativa + 1) * 5)
except Exception as e:
print(f"Erro inesperado: {str(e)[:100]}...")
if tentativa < max_tentativas - 1:
time.sleep((tentativa + 1) * 2)
print(" Todas as tentativas falharam")
return None
# =============================================================================
# PARTE 1: BUSCAR NOTAS E IDENTIFICAR NOVAS CHAVES
# =============================================================================
def consultar_chaves_existentes(chaves_list):
"""Consulta quais chaves já existem no banco de dados"""
if not chaves_list:
return set()
try:
conn = get_db_connection()
cursor = conn.cursor()
chaves_str = "', '".join(chaves_list)
query = f"""
SELECT DISTINCT [chave]
FROM [GINSENG].[dbo].[fato_notas_entrada]
WHERE [chave] IN ('{chaves_str}')
"""
cursor.execute(query)
chaves_existentes = {row[0] for row in cursor.fetchall()}
cursor.close()
conn.close()
print(f" Consulta no banco: {len(chaves_existentes):,} chaves já existem de {len(chaves_list):,} consultadas")
return chaves_existentes
except Exception as e:
print(f" Erro ao consultar banco de dados: {e}")
return set()
def inserir_nfes_banco(registros_novos):
"""Insere os registros novos de NFe no banco de dados"""
if not registros_novos:
return 0
try:
conn = get_db_connection()
cursor = conn.cursor()
insert_query = """
INSERT INTO [GINSENG].[dbo].[fato_notas_entrada] (
[chave], [cnf], [serie], [data_emissao], [hora_emissao], [cnpj_emissor],
[nome_emissor], [cnpj_destinatario], [valor_total_produtos], [valor_icmsst],
[valor_fcpst], [valor_frete], [valor_seguro], [valor_outras_despesas],
[valor_ii], [valor_ipi], [valor_ipi_devol], [valor_servicos], [valor_desconto],
[valor_icms_desonerado], [valor_liquido], [tipo_pagamento_json], [numero_fatura],
[qtd_parcelas], [duplicatas_json], [valor_icms], [situacao], [TRIAL119]
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
registros_inseridos = 0
for registro in registros_novos:
try:
cursor.execute(insert_query, (
registro.get('chave', ''),
registro.get('cnf', ''),
registro.get('serie', ''),
registro.get('data_emissao', None),
registro.get('hora_emissao', None),
registro.get('cnpj_emissor', ''),
registro.get('nome_emissor', ''),
registro.get('cnpj_destinatario', ''),
float(registro.get('valor_total_produtos', 0)) if registro.get('valor_total_produtos') else None,
float(registro.get('valor_icmsst', 0)) if registro.get('valor_icmsst') else None,
float(registro.get('valor_fcpst', 0)) if registro.get('valor_fcpst') else None,
float(registro.get('valor_frete', 0)) if registro.get('valor_frete') else None,
float(registro.get('valor_seguro', 0)) if registro.get('valor_seguro') else None,
float(registro.get('valor_outras_despesas', 0)) if registro.get('valor_outras_despesas') else None,
float(registro.get('valor_ii', 0)) if registro.get('valor_ii') else None,
float(registro.get('valor_ipi', 0)) if registro.get('valor_ipi') else None,
float(registro.get('valor_ipi_devol', 0)) if registro.get('valor_ipi_devol') else None,
float(registro.get('valor_servicos', 0)) if registro.get('valor_servicos') else None,
float(registro.get('valor_desconto', 0)) if registro.get('valor_desconto') else None,
float(registro.get('valor_icms_desonerado', 0)) if registro.get('valor_icms_desonerado') else None,
float(registro.get('valor_liquido', 0)) if registro.get('valor_liquido') else None,
registro.get('tipo_pagamento_json', ''),
registro.get('numero_fatura', ''),
int(registro.get('qtd_parcelas', 0)) if registro.get('qtd_parcelas') else None,
registro.get('duplicatas_json', ''),
float(registro.get('valor_icms', 0)) if registro.get('valor_icms') else None,
registro.get('situacao', ''),
registro.get('TRIAL119', '')
))
registros_inseridos += 1
except Exception as e:
print(f" Erro ao inserir registro {registro.get('chave', '')}: {e}")
conn.commit()
cursor.close()
conn.close()
return registros_inseridos
except Exception as e:
print(f" Erro ao conectar com banco de dados: {e}")
return 0
def obter_total_registros_dia(session, data_str):
"""Consulta quantos registros existem em um dia específico"""
params = {
"created_at[from]": data_str,
"created_at[to]": data_str,
"format_type": "JSON",
"limit": 1
}
response = fazer_requisicao_robusta(session, API_URL, API_HEADERS, params, max_tentativas=2)
if response and response.status_code == 200:
data = response.json()
return data.get('count', 0)
return 0
def processar_dia_chaves(session, data_str, all_extracted_data):
"""Processa todos os registros de um dia específico para extrair chaves"""
params = {
"created_at[from]": data_str,
"created_at[to]": data_str,
"format_type": "JSON",
"limit": 50
}
registros_dia = 0
registros_filtrados = 0
page_count = 0
while True:
page_count += 1
print(f" Página {page_count}...", end=" ")
response = fazer_requisicao_robusta(session, API_URL, API_HEADERS, params, max_tentativas=3)
if not response or response.status_code != 200:
print(f"Falha")
break
data = response.json()
nfe_list = data.get('data', [])
if not nfe_list:
print(f"Fim dos dados")
break
print(f"{len(nfe_list)} registros")
for nfe_data in nfe_list:
registros_dia += 1
nfe = nfe_data.get('xml', {}).get('NFe', {})
inf_nfe = nfe.get('infNFe', {})
ide = inf_nfe.get('ide', {})
emit = inf_nfe.get('emit', {})
dest = inf_nfe.get('dest', {})
total = inf_nfe.get('total', {}).get('ICMSTot', {})
cobr = inf_nfe.get('cobr', {})
cnpj_emissor = str(emit.get('CNPJ', '')).strip()
if cnpj_emissor not in CNPJS_PERMITIDOS:
continue
registros_filtrados += 1
registro = {
'chave': str(nfe_data.get('access_key', '')).strip(),
'cnf': str(ide.get('cNF', '')).strip(),
'serie': str(ide.get('serie', '')).strip(),
'data_emissao': ide.get('dhEmi', '').split('T')[0] if ide.get('dhEmi') else '',
'hora_emissao': ide.get('dhEmi', '').split('T')[1].split('-')[0] if ide.get('dhEmi') and 'T' in ide.get('dhEmi', '') else '',
'cnpj_emissor': cnpj_emissor,
'nome_emissor': str(emit.get('xNome', '')).strip(),
'cnpj_destinatario': str(dest.get('CNPJ', '')).strip(),
'valor_total_produtos': str(total.get('vProd', '')).strip(),
'valor_icmsst': str(total.get('vST', '')).strip(),
'valor_fcpst': str(total.get('vFCPST', '')).strip(),
'valor_frete': str(total.get('vFrete', '')).strip(),
'valor_seguro': str(total.get('vSeg', '')).strip(),
'valor_outras_despesas': str(total.get('vOutro', '')).strip(),
'valor_ii': str(total.get('vII', '')).strip(),
'valor_ipi': str(total.get('vIPI', '')).strip(),
'valor_ipi_devol': str(total.get('vIPIDevol', '')).strip(),
'valor_servicos': '',
'valor_desconto': str(total.get('vDesc', '')).strip(),
'valor_icms_desonerado': str(total.get('vICMSDeson', '')).strip(),
'valor_liquido': str(total.get('vNF', '')).strip(),
'tipo_pagamento_json': json.dumps(inf_nfe.get('pag', {}), ensure_ascii=False) if inf_nfe.get('pag') else '',
'numero_fatura': str(cobr.get('fat', {}).get('nFat', '')).strip() if cobr.get('fat') else '',
'qtd_parcelas': len(cobr.get('dup', [])) if cobr.get('dup') else 0,
'duplicatas_json': json.dumps(cobr.get('dup', []), ensure_ascii=False) if cobr.get('dup') else '',
'valor_icms': str(total.get('vICMS', '')).strip(),
'situacao': str(inf_nfe.get('protNFe', {}).get('infProt', {}).get('cStat', '')).strip() if inf_nfe.get('protNFe') else ''
}
all_extracted_data.append(registro)
page_info = data.get('page', {})
next_url = page_info.get('next')
if not next_url:
break
if 'cursor=' in next_url:
cursor_value = next_url.split('cursor=')[1].split('&')[0]
params['cursor'] = cursor_value
else:
break
time.sleep(0.3)
return registros_filtrados
def buscar_notas_e_identificar_novas():
"""
PARTE 1: Busca notas no período e retorna lista de chaves novas
Retorna: lista de chaves que não existem no banco
"""
print("\n" + "=" * 70)
print(" PARTE 1: BUSCANDO NOTAS E IDENTIFICANDO CHAVES NOVAS")
print("=" * 70)
data_atual = datetime.now()
data_inicio = data_atual - timedelta(days=DIAS_PARA_BUSCAR)
print(f" Período: {data_inicio.strftime('%Y-%m-%d')} até {data_atual.strftime('%Y-%m-%d')}")
print(f" CNPJs permitidos: {', '.join(CNPJS_PERMITIDOS)}")
session = create_session_with_retry()
# Analisar dias com dados
print(f"\n Analisando período por dia...")
detalhes_dias = []
current_date = data_inicio
while current_date <= data_atual:
data_str = current_date.strftime("%Y-%m-%d")
print(f" Consultando {data_str}...", end=" ")
registros = obter_total_registros_dia(session, data_str)
if registros > 0:
detalhes_dias.append((data_str, registros))
print(f"{registros:,} registros")
else:
print("0 registros")
current_date += timedelta(days=1)
time.sleep(0.3)
if not detalhes_dias:
print(" Nenhum registro encontrado no período.")
return []
# Extrair dados
print(f"\n Extraindo dados dos dias com registros...")
all_extracted_data = []
for dia, qtd in detalhes_dias:
print(f"\n Processando {dia} ({qtd:,} registros esperados)")
processar_dia_chaves(session, dia, all_extracted_data)
print(f"\n Total extraído (após filtro CNPJ): {len(all_extracted_data):,} registros")
if not all_extracted_data:
print(" Nenhum dado extraído após filtro de CNPJ.")
return []
# Verificar duplicatas
print(f"\n Verificando duplicatas no banco...")
chaves_extraidas = [r['chave'] for r in all_extracted_data if r['chave']]
chaves_existentes = consultar_chaves_existentes(chaves_extraidas)
# Filtrar registros novos
registros_novos = [r for r in all_extracted_data if r['chave'] not in chaves_existentes]
print(f" Registros duplicados: {len(all_extracted_data) - len(registros_novos):,}")
print(f" Registros novos: {len(registros_novos):,}")
if not registros_novos:
print(" Todos os registros já existem no banco.")
return []
# Inserir no banco fato_notas_entrada
print(f"\n Inserindo {len(registros_novos):,} notas no banco...")
inseridos = inserir_nfes_banco(registros_novos)
print(f" {inseridos:,} notas inseridas com sucesso!")
# Retornar lista de chaves novas
chaves_novas = [r['chave'] for r in registros_novos if r['chave']]
return chaves_novas
# =============================================================================
# PARTE 2: BUSCAR ITENS DAS NOTAS NOVAS
# =============================================================================
def buscar_valor_xml(item_element, campo):
"""Busca um valor específico dentro de um elemento XML do item da NFe"""
try:
prod = item_element.find('prod')
imposto = item_element.find('imposto')
if campo in ['cProd', 'cEAN', 'xProd', 'NCM', 'CEST', 'CFOP', 'uCom', 'qCom', 'vUnCom', 'vProd', 'vFrete', 'vSeg', 'vDesc', 'vOutro', 'xPed']:
if prod is not None:
elem = prod.find(campo)
if elem is not None and elem.text:
valor = elem.text.strip()
if campo == 'qCom':
try:
valor_float = float(valor)
if valor_float == int(valor_float):
valor = str(int(valor_float))
else:
valor = str(valor_float).rstrip('0').rstrip('.')
except:
pass
return valor
return ''
elif campo in ['orig', 'CST', 'modBC', 'vBC', 'pICMS', 'vICMS', 'vBCFCP', 'pFCP', 'vFCP', 'modBCST', 'pMVAST', 'vBCST', 'pICMSST', 'vICMSST', 'vBCFCPST', 'pFCPST', 'vFCPST', 'vICMSDeson']:
if imposto is not None:
icms = imposto.find('ICMS')
if icms is not None:
for icms_child in icms:
elem = icms_child.find(campo)
if elem is not None and elem.text:
valor = elem.text.strip()
if campo in ['pICMS', 'pICMSST', 'pFCP', 'pFCPST', 'pMVAST']:
try:
valor_float = float(valor)
valor = str(round(valor_float / 100, 4))
except:
pass
return valor
return ''
elif campo == 'vII':
if imposto is not None:
ii = imposto.find('II')
if ii is not None:
elem = ii.find('vII')
if elem is not None and elem.text:
return elem.text.strip()
return ''
elif campo == 'vIPI':
if imposto is not None:
ipi = imposto.find('IPI')
if ipi is not None:
for ipi_child in ipi:
elem = ipi_child.find('vIPI')
if elem is not None and elem.text:
return elem.text.strip()
return ''
return ''
except:
return ''
def inserir_itens_banco(lista_itens):
"""Insere os itens da NFe no banco de dados"""
if not lista_itens:
return 0
try:
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute("SELECT TOP 0 * FROM [GINSENG].[dbo].[fato_notas_entrada_itens]")
colunas_tabela = [desc[0].lower() for desc in cursor.description]
colunas_tabela = [col for col in colunas_tabela if col not in ['id']]
except Exception as e:
print(f" Erro ao verificar estrutura da tabela: {e}")
colunas_tabela = ['chave', 'n_item', 'cod_produto', 'produto', 'quantidade', 'valor_unitario', 'valor_total_produtos']
campos_mapeados = {
'chave': 'chave',
'n_item': 'n_item',
'data_emissao': 'data_emissao',
'cod_produto': 'cod_produto',
'produto': 'produto',
'cEAN': 'cean',
'NCM': 'ncm',
'CEST': 'cest',
'CFOP': 'cfop',
'unidade_medida': 'unidade_medida',
'quantidade': 'quantidade',
'valor_unitario': 'valor_unitario',
'valor_total_produtos': 'valor_total_produtos',
'valor_frete': 'valor_frete',
'valor_seguro': 'valor_seguro',
'valor_desconto': 'valor_desconto',
'valor_outras_despesas': 'valor_outras_despesas',
'codigo_pedido': 'codigo_pedido',
'cod_origem': 'cod_origem',
'CST': 'cst',
'modalidade_BC_ICMS': 'modalidade_bc_icms',
'valor_BC_ICMS': 'valor_bc_icms',
'aliquota_ICMS': 'aliquota_icms',
'valor_ICMS': 'valor_icms',
'valor_BC_FCP': 'valor_bc_fcp',
'aliquota_FCP': 'aliquota_fcp',
'valor_FCP': 'valor_fcp',
'modalidade_BC_ST': 'modalidade_bc_st',
'aliquota_MVA_ST': 'aliquota_mva_st',
'valor_BC_ST': 'valor_bc_st',
'aliquota_ICMS_ST': 'aliquota_icms_st',
'valor_ICMSST': 'valor_icmsst',
'valor_BC_FCPST': 'valor_bc_fcpst',
'aliquota_FCPST': 'aliquota_fcpst',
'valor_FCPST': 'valor_fcpst',
'valor_II': 'valor_ii',
'valor_IPI': 'valor_ipi',
'valor_ICMS_desonerado': 'valor_icms_desonerado'
}
campos_validos = []
valores_placeholders = []
for campo_item, campo_tabela in campos_mapeados.items():
if campo_tabela in colunas_tabela:
campos_validos.append(f"[{campo_tabela}]")
valores_placeholders.append("?")
if not campos_validos:
print(" Nenhum campo válido encontrado para inserção!")
return 0
insert_query = f"""
INSERT INTO [GINSENG].[dbo].[fato_notas_entrada_itens] ({', '.join(campos_validos)})
VALUES ({', '.join(valores_placeholders)})
"""
registros_inseridos = 0
for item in lista_itens:
try:
valores = []
for campo_item, campo_tabela in campos_mapeados.items():
if campo_tabela in colunas_tabela:
valor = item.get(campo_item, '')
if valor == '':
valor = None
valores.append(valor)
cursor.execute(insert_query, valores)
registros_inseridos += 1
except Exception as e:
print(f" Erro ao inserir item {item.get('n_item', '')}: {e}")
conn.commit()
cursor.close()
conn.close()
return registros_inseridos
except Exception as e:
print(f" Erro ao conectar com banco de dados: {e}")
return 0
def buscar_itens_das_chaves(chaves_novas):
"""
PARTE 2: Busca os itens das notas novas via API
"""
print("\n" + "=" * 70)
print(" PARTE 2: BUSCANDO ITENS DAS NOTAS NOVAS")
print("=" * 70)
if not chaves_novas:
print(" Nenhuma chave nova para processar.")
return 0
print(f" Total de chaves para buscar itens: {len(chaves_novas):,}")
session = create_session_with_retry()
lista_itens_notas = []
chaves_processadas = 0
chaves_com_erro = 0
for i, chave in enumerate(chaves_novas, 1):
print(f"\n [{i}/{len(chaves_novas)}] Processando: {chave[:20]}...")
url = f"{API_URL}?access_key[]={chave}"
try:
response = fazer_requisicao_robusta(session, url, API_HEADERS, None, max_tentativas=3)
if response and response.status_code == 200:
data = response.json()
nfe_list = data.get('data', [])
if not nfe_list:
print(f" Nenhum dado retornado")
continue
for nfe_data in nfe_list:
chave_retornada = nfe_data.get('access_key', '')
xml_base64 = nfe_data.get('xml', '')
if not xml_base64 or not isinstance(xml_base64, str):
print(f" XML não encontrado")
continue
try:
xml_decoded = base64.b64decode(xml_base64).decode('utf-8')
xml_decoded = xml_decoded.replace('xmlns="http://www.portalfiscal.inf.br/nfe"', '')
root = ET.fromstring(xml_decoded)
except Exception as e:
print(f" Erro ao decodificar XML: {e}")
continue
# Buscar data de emissão
data_emissao = ''
dhEmi_elem = root.find('.//ide/dhEmi')
if dhEmi_elem is not None and dhEmi_elem.text:
data_emissao = dhEmi_elem.text[:10]
# Buscar todos os itens
itens = root.findall('.//det')
print(f" {len(itens)} itens encontrados (Emissão: {data_emissao})")
for j, item in enumerate(itens, 1):
n_item = item.get('nItem', str(j))
item_data = {
'chave': chave_retornada,
'n_item': n_item,
'data_emissao': data_emissao,
'cod_produto': buscar_valor_xml(item, 'cProd'),
'produto': buscar_valor_xml(item, 'xProd'),
'cEAN': buscar_valor_xml(item, 'cEAN'),
'NCM': buscar_valor_xml(item, 'NCM'),
'CEST': buscar_valor_xml(item, 'CEST'),
'CFOP': buscar_valor_xml(item, 'CFOP'),
'unidade_medida': buscar_valor_xml(item, 'uCom'),
'quantidade': buscar_valor_xml(item, 'qCom'),
'valor_unitario': buscar_valor_xml(item, 'vUnCom'),
'valor_total_produtos': buscar_valor_xml(item, 'vProd'),
'valor_frete': buscar_valor_xml(item, 'vFrete'),
'valor_seguro': buscar_valor_xml(item, 'vSeg'),
'valor_desconto': buscar_valor_xml(item, 'vDesc'),
'valor_outras_despesas': buscar_valor_xml(item, 'vOutro'),
'codigo_pedido': buscar_valor_xml(item, 'xPed'),
'cod_origem': buscar_valor_xml(item, 'orig'),
'CST': buscar_valor_xml(item, 'CST'),
'modalidade_BC_ICMS': buscar_valor_xml(item, 'modBC'),
'valor_BC_ICMS': buscar_valor_xml(item, 'vBC'),
'aliquota_ICMS': buscar_valor_xml(item, 'pICMS'),
'valor_ICMS': buscar_valor_xml(item, 'vICMS'),
'valor_BC_FCP': buscar_valor_xml(item, 'vBCFCP'),
'aliquota_FCP': buscar_valor_xml(item, 'pFCP'),
'valor_FCP': buscar_valor_xml(item, 'vFCP'),
'modalidade_BC_ST': buscar_valor_xml(item, 'modBCST'),
'aliquota_MVA_ST': buscar_valor_xml(item, 'pMVAST'),
'valor_BC_ST': buscar_valor_xml(item, 'vBCST'),
'aliquota_ICMS_ST': buscar_valor_xml(item, 'pICMSST'),
'valor_ICMSST': buscar_valor_xml(item, 'vICMSST'),
'valor_BC_FCPST': buscar_valor_xml(item, 'vBCFCPST'),
'aliquota_FCPST': buscar_valor_xml(item, 'pFCPST'),
'valor_FCPST': buscar_valor_xml(item, 'vFCPST'),
'valor_II': buscar_valor_xml(item, 'vII'),
'valor_IPI': buscar_valor_xml(item, 'vIPI'),
'valor_ICMS_desonerado': buscar_valor_xml(item, 'vICMSDeson')
}
lista_itens_notas.append(item_data)
chaves_processadas += 1
else:
print(f" Erro na requisição")
chaves_com_erro += 1
except Exception as e:
print(f" Erro: {e}")
chaves_com_erro += 1
print(f"\n Chaves processadas: {chaves_processadas:,}")
print(f" Chaves com erro: {chaves_com_erro:,}")
print(f" Total de itens extraídos: {len(lista_itens_notas):,}")
# Inserir itens no banco
if lista_itens_notas:
print(f"\n Inserindo {len(lista_itens_notas):,} itens no banco...")
inseridos = inserir_itens_banco(lista_itens_notas)
print(f" {inseridos:,} itens inseridos com sucesso!")
return inseridos
return 0
# =============================================================================
# ENVIAR STATUS PARA API
# =============================================================================
def enviar_status_api(sucesso):
"""Envia status para a API de monitoramento"""
try:
print("\n" + "=" * 70)
print(" ENVIANDO STATUS PARA API DE MONITORAMENTO")
print("=" * 70)
url = "https://api.grupoginseng.com.br/api/status/4"
sao_paulo_offset = timedelta(hours=-3)
current_datetime = datetime.now(timezone(sao_paulo_offset)).strftime("%Y-%m-%d %H:%M:%S")
status_code = "OK" if sucesso else "FAIL"
payload = {
"STATUS": status_code,
"DATA": current_datetime
}
headers = {"Content-Type": "application/json"}
response = requests.put(url, json=payload, headers=headers)
print(f" Status: {status_code}")
print(f" Hora: {current_datetime}")
print(f" Response: {response.status_code}")
except Exception as e:
print(f" Erro ao enviar status: {e}")
# =============================================================================
# EXECUÇÃO PRINCIPAL
# =============================================================================
def main():
print("\n" + "=" * 70)
print(" SCRIPT COMPLETO - NOTAS E ITENS")
print(" Data de execução:", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print("=" * 70)
sucesso = True
try:
# PARTE 1: Buscar notas e identificar chaves novas
chaves_novas = buscar_notas_e_identificar_novas()
# PARTE 2: Buscar itens das notas novas
if chaves_novas:
itens_inseridos = buscar_itens_das_chaves(chaves_novas)
else:
print("\n Nenhuma chave nova encontrada. Pulando busca de itens.")
itens_inseridos = 0
# Resumo final
print("\n" + "=" * 70)
print(" RESUMO FINAL")
print("=" * 70)
print(f" Notas novas encontradas: {len(chaves_novas):,}")
print(f" Itens inseridos: {itens_inseridos:,}")
except Exception as e:
print(f"\n ERRO GERAL: {e}")
sucesso = False
# Enviar status
enviar_status_api(sucesso)
print("\n" + "=" * 70)
print(" PROCESSAMENTO CONCLUÍDO!")
print("=" * 70)
if __name__ == "__main__":
main()

711
Pedidos_mar.py Normal file
View File

@ -0,0 +1,711 @@
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)

105
ciclos.py Normal file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Script para consultar ciclos da API do Grupo Boticário
"""
import requests
def get_token():
"""Busca o token da API Ginseng"""
url = "https://api.grupoginseng.com.br/api/tokens"
print("Buscando token...")
try:
response = requests.get(url, timeout=30)
if response.status_code == 200:
data = response.json()
if data.get("success") and data.get("data"):
token = data["data"][0].get("token")
if token:
print("✓ Token obtido com sucesso!")
return token
print(f"✗ Erro ao buscar token: {response.status_code}")
return None
except Exception as e:
print(f"✗ Erro na requisição: {e}")
return None
def get_cycles(token):
"""Consulta os ciclos da API do Grupo Boticário"""
url = "https://api-extranet.grupoboticario.digital/api/v2/cycles"
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
"authorization": token,
"cache-control": "no-cache",
"origin": "https://extranet.grupoboticario.com.br",
"pragma": "no-cache",
"referer": "https://extranet.grupoboticario.com.br/",
"sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"',
"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/144.0.0.0 Safari/537.36"
}
print("\nConsultando ciclos...")
try:
response = requests.get(url, headers=headers, timeout=30)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
data = response.json()
print("✓ Ciclos obtidos com sucesso!")
return data
else:
print(f"✗ Erro na requisição: {response.status_code}")
print(f"Resposta: {response.text[:500]}")
return None
except Exception as e:
print(f"✗ Erro na requisição: {e}")
return None
if __name__ == "__main__":
print("="*60)
print("CICLOS - Consulta de Ciclos do Grupo Boticário")
print("="*60)
# 1. Buscar token
print("\n[1/2] Obtendo token...")
token = get_token()
if not token:
print("\n✗ Não foi possível obter o token.")
exit(1)
print(f" Token: {token[:60]}...")
# 2. Consultar ciclos
print("\n[2/2] Consultando ciclos...")
cycles = get_cycles(token)
if cycles:
print(f"\n{'='*60}")
print("RESULTADO")
print(f"{'='*60}")
import json
print(json.dumps(cycles, indent=2, ensure_ascii=False))
else:
print("\n✗ Não foi possível obter os ciclos.")

836
draft_mar.py Normal file
View File

@ -0,0 +1,836 @@
# -*- coding: utf-8 -*-
import requests
import json
import socket
import pyodbc
import time
from requests.exceptions import RequestException
from datetime import datetime, timezone, timedelta
# URL da API
url = "https://mar-orders-bff-api.demanda-abastecimento.grupoboticario.digital/api/orderdraft/order-building-data?draftType=SEM"
# Cabeçalhos da requisição
headers = {
"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": '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
"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/127.0.0.0 Safari/537.36"
}
# Lista de códigos das lojas
store_codes = [
"12522", "12817", "12818", "12820", "12823", "12824", "12826", "12828", "12829",
"12830", "12838", "13427", "14617", "19103", "20005", "20006", "20009", "20056",
"20057", "20441", "20858", "20968", "20969", "20970", "20986", "20988", "20989",
"20991", "20992", "20993", "20994", "20995", "20996", "20997", "20998", "20999",
"21000", "21001", "21068", "21277", "21278", "21296", "21375", "21381", "21383",
"21495", "21624", "21647", "22541", "3546", "4560", "5699", "910173", "910291",
"21007", "23665", "23712", "23711", "23702", "23703",
"23713", "23708", "23701", "23709","23475","23156","14668", "24253",
"24254", "24255", "24258", "24257", "24268", "24269", "24293", "23813", "24449", "24450", "24455", "24458", "24454", "24447", "24448", "24451", "24456", "24457", "24453", "24452"
]
# Mapeamento de códigos para nomes de categorias
category_map = {
"10040": "CUIDADOS COM A PELE",
"10090": "HOME CARE",
"10060": "DESODORANTES",
"10110": "MAQUIAGEM",
"10080": "GIFTS",
"10120": "OLEOS",
"10050": "CUIDADOS FACIAIS",
"10170": "UNHAS",
"10150": "SOLAR",
"10190": "CUIDADOS PETS",
"10160": "SUPORTE A VENDA",
"10030": "CUIDADOS COM A BARBA",
"10130": "PERFUMARIA",
"10100": "INFANTIL",
"10020": "CABELOS",
"10070": "EMBALAGENS",
"10140": "SABONETE CORPO",
"10010": "ACESSORIOS"
}
# Mapeamento de códigos para nomes de subcategorias
subcategory_map = {
"1004020125": "CUIDADOS COM O CORPO",
"1006020150": "DESODORANTE FEMININO",
"1013020285": "PERFUMARIA FEMININA",
"1006020155": "DESODORANTE MASCULINO",
"1013020290": "PERFUMARIA MASCULINA",
"1001020055": "ORGANIZADOR COSTURAVEL",
"1014020315": "MULTIFUNCIONAL",
"1019020430": "PETS",
"1016020385": "MATERIAL DE APOIO",
"1001020030": "ELETRONICOS",
"1001020045": "FUNCIONAIS MAQUIAGEM",
"1010020225": "PERFUMARIA",
"1010020200": "CABES",
"1010020205": "CUIDADOS COM O CORPO",
"1010020235": "SABONETES",
"1016020395": "PRM",
"1009020195": "AROMATIZACAO",
"1001020025": "CORPO E BANHO",
"1015020335": "POS-SOL",
"1015020360": "PROTETOR FACIAL",
"1015020355": "PROTETOR CORPO",
"1014020300": "CORPO",
"1005020135": "HIDRATANTES E TRATAMENTOS",
"1005020140": "LIMPEZA",
"1005020145": "MASCARA FACIAL",
"1001020015": "CAPINHA",
"1009020140": "LIMPEZA",
"1004020120": "CUIDADOS COM AS MAOS",
"1001020060": "ORGANIZADOR NAO COSTURAVEL",
"1002020085": "CONDICIONADOR",
"1004020130": "CUIDADOS COM OS PES",
"1002020090": "FINALIZADOR",
"1006020160": "DESODORANTE UNISSEX",
"1002020105": "TRATAMENTO CABELOS",
"1012020275": "CUIDADOS COM O CORPO",
"1014020305": "CUIDADOS INTIMOS",
"1002020095": "SHAMPOO/PRE-SHAMPOO",
"1003020110": "POS-BARBEAR",
"1003020115": "PRE-BARBEAR",
"1001020005": "BRINQUEDO",
"1008020190": "ESTOJO REGULAR",
"1011020255": "MAQUIAGEM MULTIFUNCIONAL",
"1011020260": "MAQUIAGEM OLHOS",
"1011020245": "MAQUIAGEM BOCA",
"1001020070": "PINCEIS",
"1007020170": "EMBALAGENS VENDAVEIS",
"1001020035": "EMBALAGEM PRESENTEAVEL",
"1011020265": "MAQUIAGEM ROSTO",
"1001020040": "EMBALAGEM PRODUTO",
"1011020270": "MAQUIAGEM SOBRANCELHAS",
"1013020295": "PERFUMARIA UNISSEX",
"1014020310": "MAOS",
"1002020100": "STYLING",
"1001020010": "CABES",
"1002020075": "2 EM 1",
"1015020365": "PROTETOR FACIAL COM COR",
"1012020280": "MULTIFUNCIONAL",
"1010020220": "MAQUIAGEM",
"1010020210": "DESODORANTE"
}
# Configurações de conexão com o banco de dados
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'
)
def check_internet_connection():
"""Verifica se há conexão com a internet"""
try:
socket.gethostbyname("google.com")
return True
except socket.error:
return False
def create_draft_temp_table(cursor):
"""Cria a tabela draft_temp se ela não existir"""
try:
# Verifica se a tabela já existe
cursor.execute("""
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'draft_temp' AND schema_id = SCHEMA_ID('dbo'))
BEGIN
CREATE TABLE dbo.draft_temp (
id INT IDENTITY(1,1) PRIMARY KEY,
date DATE DEFAULT CAST(GETDATE() AS DATE),
loja_id VARCHAR(50),
code VARCHAR(50),
description VARCHAR(255),
launch VARCHAR(50),
deactivation VARCHAR(50),
thirdToLastCycleSales INT,
secondToLastCycleSales INT,
lastCycleSales INT,
currentCycleSales INT,
nextCycleProjection INT,
secondToNextCycleProjection INT,
stock_actual INT,
stock_inTransit INT,
purchaseSuggestion INT,
smartPurchase_purchaseSuggestionCycle INT,
smartPurchase_nextCyclePurchaseSuggestion INT,
pendingOrder INT,
salesCurve VARCHAR(50),
promotions_description NVARCHAR(MAX),
promotions_discountPercent NVARCHAR(MAX),
priceSellin DECIMAL(18, 2),
businessUnit VARCHAR(50),
codCategory VARCHAR(255),
criticalItem_dtProvidedRegularization VARCHAR(50),
criticalItem_blockedWallet NVARCHAR(MAX),
criticalItem_isCritical NVARCHAR(MAX),
codSubCategory VARCHAR(255),
isProductDeactivated NVARCHAR(MAX),
brandGroupCode VARCHAR(50),
daysWithoutSales INT,
coverageDays INT,
hasCoverage VARCHAR(50),
thirdToLastCycle VARCHAR(5),
secondToLastCycle VARCHAR(5),
lastCycle VARCHAR(5),
currentCycle VARCHAR(5),
nextCycle VARCHAR(5),
secondToNextCycle VARCHAR(5),
secondToNextCycleEud VARCHAR(5),
nextCycleEud VARCHAR(5),
thirdToLastCycleEud VARCHAR(5),
secondToLastCycleEud VARCHAR(5),
lastCycleEud VARCHAR(5),
currentCycleEud VARCHAR(5)
)
END
""")
conn.commit()
print("Tabela draft_temp verificada/criada com sucesso.")
except Exception as e:
print(f"Erro ao criar tabela draft_temp: {e}")
conn.rollback()
raise
def create_promo_temp_table(cursor):
"""Cria a tabela promo_temp se ela não existir"""
try:
# Verifica se a tabela já existe e exclui para recriar
cursor.execute("""
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'promo_temp' AND schema_id = SCHEMA_ID('dbo'))
BEGIN
DROP TABLE dbo.promo_temp
END
""")
conn.commit()
# Cria a tabela promo_temp
cursor.execute("""
CREATE TABLE dbo.promo_temp (
id INT IDENTITY(1,1) PRIMARY KEY,
loja_id NVARCHAR(MAX) NULL,
code NVARCHAR(MAX) NULL,
type NVARCHAR(MAX) NULL,
target NVARCHAR(MAX) NULL,
thirdToLastCycle_discountPercent NVARCHAR(MAX) NULL,
thirdToLastCycle_description NVARCHAR(MAX) NULL,
secondToLastCycle_discountPercent NVARCHAR(MAX) NULL,
secondToLastCycle_description NVARCHAR(MAX) NULL,
lastCycle_discountPercent NVARCHAR(MAX) NULL,
lastCycle_description NVARCHAR(MAX) NULL,
currentCycle_discountPercent NVARCHAR(MAX) NULL,
currentCycle_description NVARCHAR(MAX) NULL,
nextCycle_discountPercent NVARCHAR(MAX) NULL,
nextCycle_description NVARCHAR(MAX) NULL,
secondToNextCycle_discountPercent NVARCHAR(MAX) NULL,
secondToNextCycle_description NVARCHAR(MAX) NULL,
thirdToLastCycleEud_discountPercent NVARCHAR(MAX) NULL,
thirdToLastCycleEud_description NVARCHAR(MAX) NULL,
secondToLastCycleEud_discountPercent NVARCHAR(MAX) NULL,
secondToLastCycleEud_description NVARCHAR(MAX) NULL,
lastCycleEud_discountPercent NVARCHAR(MAX) NULL,
lastCycleEud_description NVARCHAR(MAX) NULL,
currentCycleEud_discountPercent NVARCHAR(MAX) NULL,
currentCycleEud_description NVARCHAR(MAX) NULL,
nextCycleEud_discountPercent NVARCHAR(MAX) NULL,
nextCycleEud_description NVARCHAR(MAX) NULL,
secondToNextCycleEud_discountPercent NVARCHAR(MAX) NULL,
secondToNextCycleEud_description NVARCHAR(MAX) NULL,
dt_atualizacao DATETIME DEFAULT GETDATE()
)
""")
conn.commit()
print("Tabela promo_temp verificada/criada com sucesso.")
except Exception as e:
print(f"Erro ao criar tabela promo_temp: {e}")
conn.rollback()
raise
def process_and_insert_promo_data(store_code, response_data, cursor):
"""Processa os dados da API e insere na tabela promo_temp"""
try:
base = response_data.get('data', {})
# Definir os ciclos normais e EUD com seus valores do JSON
cycle_values = {
'thirdToLastCycle': base.get('thirdToLastCycle', ''),
'secondToLastCycle': base.get('secondToLastCycle', ''),
'lastCycle': base.get('lastCycle', ''),
'currentCycle': base.get('currentCycle', ''),
'nextCycle': base.get('nextCycle', ''),
'secondToNextCycle': base.get('secondToNextCycle', ''),
'secondToNextCycleEud': base.get('secondToNextCycleEud', ''),
'nextCycleEud': base.get('nextCycleEud', ''),
'thirdToLastCycleEud': base.get('thirdToLastCycleEud', ''),
'secondToLastCycleEud': base.get('secondToLastCycleEud', ''),
'lastCycleEud': base.get('lastCycleEud', ''),
'currentCycleEud': base.get('currentCycleEud', ''),
}
products = base.get('products', [])
if not products:
return 0
promo_count = 0
for product in products:
code = product.get('code')
if not code:
continue
promotions = product.get('promotions', [])
if not promotions:
continue
# Agrupar todas as promoções do produto em um único registro
promo_data = {
'loja_id': store_code,
'code': code,
'type': '',
'target': '',
}
# Dicionários temporários para acumular valores por ciclo
cycle_discounts = {}
cycle_descriptions = {}
types_list = []
targets_list = []
for promotion in promotions:
promo_type = promotion.get('type', '')
promo_target = promotion.get('target', '')
if promo_type and promo_type not in types_list:
types_list.append(promo_type)
if promo_target and promo_target not in targets_list:
targets_list.append(promo_target)
cycle = promotion.get('cycle')
if not cycle:
continue
# Identificar qual ciclo corresponde a esta promoção
for cycle_key, cycle_value in cycle_values.items():
if cycle == cycle_value and cycle_value:
discount = str(promotion.get('discountPercent', ''))
description = promotion.get('description', '')
# Acumular valores com separador " | "
if cycle_key not in cycle_discounts:
cycle_discounts[cycle_key] = []
cycle_descriptions[cycle_key] = []
if discount:
cycle_discounts[cycle_key].append(discount)
if description:
cycle_descriptions[cycle_key].append(description)
break
# Montar o registro final com valores concatenados
promo_data['type'] = ' | '.join(types_list) if types_list else ''
promo_data['target'] = ' | '.join(targets_list) if targets_list else ''
for cycle_key in cycle_values.keys():
if cycle_key in cycle_discounts:
promo_data[f'{cycle_key}_discountPercent'] = ' | '.join(cycle_discounts[cycle_key])
if cycle_key in cycle_descriptions:
promo_data[f'{cycle_key}_description'] = ' | '.join(cycle_descriptions[cycle_key])
# Inserir o registro de promoção na tabela promo_temp
columns = ['loja_id', 'code', 'type', 'target',
'thirdToLastCycle_discountPercent', 'thirdToLastCycle_description',
'secondToLastCycle_discountPercent', 'secondToLastCycle_description',
'lastCycle_discountPercent', 'lastCycle_description',
'currentCycle_discountPercent', 'currentCycle_description',
'nextCycle_discountPercent', 'nextCycle_description',
'secondToNextCycle_discountPercent', 'secondToNextCycle_description',
'thirdToLastCycleEud_discountPercent', 'thirdToLastCycleEud_description',
'secondToLastCycleEud_discountPercent', 'secondToLastCycleEud_description',
'lastCycleEud_discountPercent', 'lastCycleEud_description',
'currentCycleEud_discountPercent', 'currentCycleEud_description',
'nextCycleEud_discountPercent', 'nextCycleEud_description',
'secondToNextCycleEud_discountPercent', 'secondToNextCycleEud_description']
values = [promo_data.get(col, None) for col in columns]
placeholders = ', '.join(['?' for _ in columns])
columns_str = ', '.join(columns)
query = f"""
INSERT INTO [GINSENG].[dbo].[promo_temp] ({columns_str})
VALUES ({placeholders})
"""
cursor.execute(query, values)
promo_count += 1
conn.commit()
return promo_count
except Exception as e:
print(f"Erro ao processar dados de promoção da loja {store_code}: {e}")
conn.rollback()
return 0
def process_and_insert_data(store_code, response_data, cursor):
"""Processa os dados da API e insere na tabela draft_temp"""
try:
# Limpar dados da loja no banco antes de inserir novos dados
delete_sql = "DELETE FROM draft_temp WHERE loja_id = ?"
cursor.execute(delete_sql, (store_code,))
base = response_data.get('data', {})
products = base.get('products', [])
current_cycle = base.get('currentCycle', '')
# Extrair valores de ciclo do JSON
cycle_data = {
'thirdToLastCycle': base.get('thirdToLastCycle', ''),
'secondToLastCycle': base.get('secondToLastCycle', ''),
'lastCycle': base.get('lastCycle', ''),
'currentCycle': base.get('currentCycle', ''),
'nextCycle': base.get('nextCycle', ''),
'secondToNextCycle': base.get('secondToNextCycle', ''),
'secondToNextCycleEud': base.get('secondToNextCycleEud', ''),
'nextCycleEud': base.get('nextCycleEud', ''),
'thirdToLastCycleEud': base.get('thirdToLastCycleEud', ''),
'secondToLastCycleEud': base.get('secondToLastCycleEud', ''),
'lastCycleEud': base.get('lastCycleEud', ''),
'currentCycleEud': base.get('currentCycleEud', ''),
}
registros = []
for product in products:
promotions = product.get('promotions', [])
filtered_promotions = [promo for promo in promotions if promo.get('cycle') == current_cycle]
if filtered_promotions:
promotion_description = " | ".join(promo.get('description', '') for promo in filtered_promotions)
promotion_discount = " | ".join(str(promo.get('discountPercent', '')) for promo in filtered_promotions)
else:
promotion_description = ''
promotion_discount = ''
# Busca o nome da categoria no dicionário
cod_category = product.get('codCategory', '')
category_name = category_map.get(str(cod_category), '')
# Busca o nome da subcategoria no dicionário
cod_subcategory = product.get('codSubCategory', '')
subcategory_name = subcategory_map.get(str(cod_subcategory), '')
# Corrigir o campo criticalItem_dtProvidedRegularization
dt_provided_regularization = product.get('criticalItem', {}).get('dtProvidedRegularization', '')
if isinstance(dt_provided_regularization, list):
if not dt_provided_regularization or dt_provided_regularization[0] is None:
dt_provided_regularization = ''
elif dt_provided_regularization is None:
dt_provided_regularization = ''
# Corrigindo o campo daysWithoutSales e coverageDays
days_without_sales = product.get('daysWithoutSales', 0)
coverage_days = product.get('coverageDays', 0)
if days_without_sales is None:
days_without_sales = 0
if coverage_days is None:
coverage_days = 0
registro = (
store_code,
product.get('code'),
product.get('description'),
product.get('launch'),
product.get('deactivation'),
product.get('sales', {}).get('thirdToLastCycleSales', 0),
product.get('sales', {}).get('secondToLastCycleSales', 0),
product.get('sales', {}).get('lastCycleSales', 0),
product.get('sales', {}).get('currentCycleSales', 0),
product.get('sales', {}).get('nextCycleProjection', 0),
product.get('sales', {}).get('secondToNextCycleProjection', 0),
product.get('stock', {}).get('actual', 0),
product.get('stock', {}).get('inTransit', 0),
product.get('purchaseSuggestion', 0),
product.get('smartPurchase', {}).get('purchaseSuggestionCycle', 0),
product.get('smartPurchase', {}).get('nextCyclePurchaseSuggestion', 0),
product.get('pendingOrder', 0),
product.get('salesCurve', ''),
promotion_description,
promotion_discount,
product.get('priceSellin', 0.0),
product.get('businessUnit', ''),
category_name,
dt_provided_regularization,
product.get('criticalItem', {}).get('blockedWallet', False),
product.get('criticalItem', {}).get('isCritical', False),
subcategory_name,
product.get('isProductDeactivated', False),
product.get('brandGroupCode', ''),
days_without_sales,
coverage_days,
product.get('hasCoverage', False),
cycle_data['thirdToLastCycle'],
cycle_data['secondToLastCycle'],
cycle_data['lastCycle'],
cycle_data['currentCycle'],
cycle_data['nextCycle'],
cycle_data['secondToNextCycle'],
cycle_data['secondToNextCycleEud'],
cycle_data['nextCycleEud'],
cycle_data['thirdToLastCycleEud'],
cycle_data['secondToLastCycleEud'],
cycle_data['lastCycleEud'],
cycle_data['currentCycleEud']
)
registros.append(registro)
if registros:
sql = """
INSERT INTO draft_temp (
loja_id, code, description, launch, deactivation,
thirdToLastCycleSales, secondToLastCycleSales, lastCycleSales, currentCycleSales, nextCycleProjection, secondToNextCycleProjection,
stock_actual, stock_inTransit, purchaseSuggestion,
smartPurchase_purchaseSuggestionCycle, smartPurchase_nextCyclePurchaseSuggestion,
pendingOrder, salesCurve,
promotions_description, promotions_discountPercent,
priceSellin, businessUnit, codCategory,
criticalItem_dtProvidedRegularization, criticalItem_blockedWallet, criticalItem_isCritical,
codSubCategory, isProductDeactivated, brandGroupCode, daysWithoutSales, coverageDays, hasCoverage,
thirdToLastCycle, secondToLastCycle, lastCycle, currentCycle, nextCycle, secondToNextCycle,
secondToNextCycleEud, nextCycleEud, thirdToLastCycleEud, secondToLastCycleEud, lastCycleEud, currentCycleEud
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
cursor.executemany(sql, registros)
conn.commit()
return len(registros)
return 0
except Exception as e:
print(f"Erro ao processar dados da loja {store_code}: {e}")
conn.rollback()
raise
def fetch_and_insert_data(store_code, index, total, cursor, retry_count=1):
"""Baixa os dados da API e insere diretamente no banco"""
attempt = 0
while attempt < retry_count:
try:
# Definindo storeType com base no código da loja
if store_code == "21007":
store_type = "BOT"
elif store_code in ["910173", "910291"]:
store_type = "QDB"
else:
store_type = "BOT"
# Dados da requisição com o código da loja
data = {
"storeCode": store_code,
"useId": 163165,
"storeType": store_type,
"generateNew": False
}
# Fazendo a requisição POST
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
# Requisição bem-sucedida
response_data = response.json()
# Processa e insere os dados na tabela draft_temp
num_products = process_and_insert_data(store_code, response_data, cursor)
# Processa e insere os dados na tabela promo
num_promos = process_and_insert_promo_data(store_code, response_data, cursor)
print(f"{index}/{total}: Loja {store_code} processada com sucesso ({num_products} produtos inseridos, {num_promos} promoções inseridas).")
return True
except RequestException as e:
print(f"{index}/{total}: Erro na requisição para a loja {store_code}: {e}")
except Exception as e:
print(f"{index}/{total}: Erro inesperado para a loja {store_code}: {e}")
attempt += 1
print(f"{index}/{total}: Tentativa {attempt} de {retry_count} para a loja {store_code}.")
time.sleep(5)
print(f"{index}/{total}: Falha ao processar a loja {store_code} após {retry_count} tentativas.")
return False
def finalize_tables(cursor):
"""
Finaliza o processo:
1. Atualiza dbo.draft_historico com os dados da dbo.draft_temp
2. Exclui a tabela dbo.draft_temp
3. Atualiza dbo.promo com os dados da dbo.promo_temp
4. Exclui a tabela dbo.promo_temp
"""
try:
print("\n=== Iniciando finalização das tabelas ===")
# Passo 1: Atualizar draft_historico
print("Passo 1: Atualizando dbo.draft_historico...")
# Obter a data de hoje
today = datetime.now().strftime("%Y-%m-%d")
print(f"Data de hoje: {today}")
# Verificar se já existem dados para hoje
cursor.execute("SELECT COUNT(*) FROM dbo.draft_historico WHERE CAST(data AS DATE) = ?", (today,))
count = cursor.fetchone()[0]
if count > 0:
print(f"Encontrados {count} registros para a data {today}. Excluindo...")
cursor.execute("DELETE FROM dbo.draft_historico WHERE CAST(data AS DATE) = ?", (today,))
conn.commit()
print(f"Registros da data {today} excluídos com sucesso.")
else:
print(f"Nenhum registro encontrado para a data {today}.")
# Inserir dados da tabela draft_temp na draft_historico
print("Inserindo dados da dbo.draft_temp na dbo.draft_historico...")
cursor.execute("""
INSERT INTO dbo.draft_historico (
loja_id, code, description, launch, deactivation,
thirdtolastcyclesales, secondtolastcyclesales, lastcyclesales,
currentcyclesales, nextcycleprojection, secondtonextcycleprojection,
stock_actual, stock_intransit, purchasesuggestion,
smartpurchase_purchasesuggestioncycle, smartpurchase_nextcyclepurchasesuggestion,
pendingorder, salescurve,
promotions_description, promotions_discountpercent,
pricesellin, businessunit, codcategory,
criticalitem_dtprovidedregularization, criticalitem_blockedwallet, criticalitem_iscritical,
codsubcategory, isproductdeactivated, brandgroupcode,
dayswithoutsales, coveragedays, hascoverage, data,
thirdToLastCycle, secondToLastCycle, lastCycle, currentCycle, nextCycle, secondToNextCycle,
secondToNextCycleEud, nextCycleEud, thirdToLastCycleEud, secondToLastCycleEud, lastCycleEud, currentCycleEud
)
SELECT
loja_id, code, description, launch, deactivation,
thirdToLastCycleSales, secondToLastCycleSales, lastCycleSales,
currentCycleSales, nextCycleProjection, secondToNextCycleProjection,
stock_actual, stock_inTransit, purchaseSuggestion,
smartPurchase_purchaseSuggestionCycle, smartPurchase_nextCyclePurchaseSuggestion,
pendingOrder, salesCurve,
promotions_description, promotions_discountPercent,
priceSellin, businessUnit, codCategory,
criticalItem_dtProvidedRegularization, criticalItem_blockedWallet, criticalItem_isCritical,
codSubCategory, isProductDeactivated, brandGroupCode,
daysWithoutSales, coverageDays, hasCoverage, date,
thirdToLastCycle, secondToLastCycle, lastCycle, currentCycle, nextCycle, secondToNextCycle,
secondToNextCycleEud, nextCycleEud, thirdToLastCycleEud, secondToLastCycleEud, lastCycleEud, currentCycleEud
FROM dbo.draft_temp
""")
conn.commit()
# Verificar quantos registros foram inseridos
cursor.execute("SELECT @@ROWCOUNT")
inserted_count = cursor.fetchone()[0]
print(f"{inserted_count} registros inseridos na dbo.draft_historico com sucesso.")
# Passo 2: Excluir a tabela draft_temp
print("Passo 2: Excluindo tabela dbo.draft_temp...")
cursor.execute("""
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'draft_temp' AND schema_id = SCHEMA_ID('dbo'))
BEGIN
DROP TABLE dbo.draft_temp
END
""")
conn.commit()
print("Tabela dbo.draft_temp excluída com sucesso.")
# Passo 3: Atualizar tabela promo
print("Passo 3: Atualizando dbo.promo...")
# Limpar a tabela promo e resetar o identity
cursor.execute("""
TRUNCATE TABLE [GINSENG].[dbo].[promo]
""")
conn.commit()
print("Tabela dbo.promo limpa com sucesso (TRUNCATE reseta o identity).")
# Inserir dados da tabela promo_temp na promo
print("Inserindo dados da dbo.promo_temp na dbo.promo...")
cursor.execute("""
INSERT INTO dbo.promo (
loja_id, code, type, target,
thirdToLastCycle_discountPercent, thirdToLastCycle_description,
secondToLastCycle_discountPercent, secondToLastCycle_description,
lastCycle_discountPercent, lastCycle_description,
currentCycle_discountPercent, currentCycle_description,
nextCycle_discountPercent, nextCycle_description,
secondToNextCycle_discountPercent, secondToNextCycle_description,
thirdToLastCycleEud_discountPercent, thirdToLastCycleEud_description,
secondToLastCycleEud_discountPercent, secondToLastCycleEud_description,
lastCycleEud_discountPercent, lastCycleEud_description,
currentCycleEud_discountPercent, currentCycleEud_description,
nextCycleEud_discountPercent, nextCycleEud_description,
secondToNextCycleEud_discountPercent, secondToNextCycleEud_description,
dt_atualizacao
)
SELECT
loja_id, code, type, target,
thirdToLastCycle_discountPercent, thirdToLastCycle_description,
secondToLastCycle_discountPercent, secondToLastCycle_description,
lastCycle_discountPercent, lastCycle_description,
currentCycle_discountPercent, currentCycle_description,
nextCycle_discountPercent, nextCycle_description,
secondToNextCycle_discountPercent, secondToNextCycle_description,
thirdToLastCycleEud_discountPercent, thirdToLastCycleEud_description,
secondToLastCycleEud_discountPercent, secondToLastCycleEud_description,
lastCycleEud_discountPercent, lastCycleEud_description,
currentCycleEud_discountPercent, currentCycleEud_description,
nextCycleEud_discountPercent, nextCycleEud_description,
secondToNextCycleEud_discountPercent, secondToNextCycleEud_description,
dt_atualizacao
FROM dbo.promo_temp
""")
conn.commit()
# Verificar quantos registros foram inseridos
cursor.execute("SELECT @@ROWCOUNT")
promo_inserted_count = cursor.fetchone()[0]
print(f"{promo_inserted_count} registros inseridos na dbo.promo com sucesso.")
# Passo 4: Excluir a tabela promo_temp
print("Passo 4: Excluindo tabela dbo.promo_temp...")
cursor.execute("""
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'promo_temp' AND schema_id = SCHEMA_ID('dbo'))
BEGIN
DROP TABLE dbo.promo_temp
END
""")
conn.commit()
print("Tabela dbo.promo_temp excluída com sucesso.")
print("=== Finalização das tabelas concluída com sucesso ===\n")
return True
except Exception as e:
print(f"Erro ao finalizar tabelas: {e}")
conn.rollback()
return False
def update_api_status(status):
"""Atualiza o status da API"""
try:
url_status = "https://api.grupoginseng.com.br/api/status/1"
# Definir fuso horário de São Paulo (UTC-3)
sao_paulo_offset = timedelta(hours=-3)
# Gerar data/hora atual no formato YYYY-MM-DD HH:MM:SS com UTC-3
current_datetime = datetime.now(timezone(sao_paulo_offset)).strftime("%Y-%m-%d %H:%M:%S")
# Montar o payload
payload = {
"STATUS": status,
"DATA": current_datetime
}
# Cabeçalhos
headers_status = {
"Content-Type": "application/json"
}
# Enviar PUT request
response = requests.put(url_status, json=payload, headers=headers_status)
print("Hora enviada:", current_datetime)
print("Status Code:", response.status_code)
print("Response Body:", response.text)
except Exception as e:
print(f"Erro ao atualizar status da API: {e}")
def main():
"""Função principal"""
if not check_internet_connection():
print("Sem conexão com a internet. Verifique sua conexão e tente novamente.")
update_api_status("FAIL")
return
cursor = conn.cursor()
try:
# Criar a tabela draft_temp se não existir
create_draft_temp_table(cursor)
# Criar a tabela promo_temp
create_promo_temp_table(cursor)
failed_stores = []
total = len(store_codes)
# Processa todas as lojas
for index, store_code in enumerate(store_codes, start=1):
success = fetch_and_insert_data(store_code, index, total, cursor)
if not success:
failed_stores.append(store_code)
# Tenta novamente as lojas que falharam
if failed_stores:
print("\nTentando novamente as lojas que falharam.")
retry_count = 1
for attempt in range(1, retry_count + 1):
retry_failed_stores = []
for store_code in failed_stores:
index = store_codes.index(store_code) + 1
success = fetch_and_insert_data(store_code, index, total, cursor)
if not success:
retry_failed_stores.append(store_code)
failed_stores = retry_failed_stores
if not failed_stores:
print("\nTodos os arquivos foram processados com sucesso.")
break
if failed_stores:
print(f"Tentativa {attempt} de {retry_count} para as lojas que falharam.")
time.sleep(10)
# Se ainda houver lojas com falha após todas as tentativas
if failed_stores:
print(f"\nNão foi possível processar algumas lojas: {failed_stores}")
else:
print("\nTodos os arquivos foram processados com sucesso.")
# Finalizar tabelas SEMPRE, independente de ter lojas com falha ou não
if finalize_tables(cursor):
# Se a finalização foi bem-sucedida, verifica se houve lojas com falha
if failed_stores:
update_api_status("FAIL")
else:
update_api_status("OK")
else:
# Se a finalização falhou, sempre marca como FAIL
update_api_status("FAIL")
except Exception as e:
print(f"Erro durante a execução: {e}")
update_api_status("FAIL")
finally:
cursor.close()
conn.close()
print("\nProcesso finalizado.")
if __name__ == "__main__":
main()

620
estoque_mar.py Normal file
View File

@ -0,0 +1,620 @@
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()

772
extracao_vendashora_rgb.py Normal file
View File

@ -0,0 +1,772 @@
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# from selenium.webdriver.chrome.service import Service # Usado apenas para Linux/Docker
from selenium.webdriver.chrome.options import Options
import time
import os
import csv
import unicodedata
import pyodbc
from datetime import datetime, timedelta
import json
# Configuração da data
USE_MANUAL_DATE = False # False = usar dia anterior automaticamente; True = usar MANUAL_DATE_STR
MANUAL_DATE_STR = "20112025" # Formato DDMMAAAA, usado quando USE_MANUAL_DATE=True
# Configuração de intervalo de datas (execução dia por dia)
USE_DATE_RANGE = False # True = usar intervalo de datas; False = usar USE_MANUAL_DATE
DATE_RANGE_START = "15/10/2025" # Formato DD/MM/YYYY
DATE_RANGE_END = "19/10/2025" # Formato DD/MM/YYYY
STATE_FILE = "date_range_state.json" # Arquivo para rastrear progresso
RANGE_COMPLETED = False # Flag para indicar que o intervalo foi completado
def _parse_date_ddmmyyyy(date_str: str) -> datetime:
"""Converte string DD/MM/YYYY para datetime."""
try:
return datetime.strptime(date_str.strip(), "%d/%m/%Y")
except Exception:
raise ValueError(f"Formato de data inválido: {date_str}. Use DD/MM/YYYY")
def _get_next_date_in_range() -> str:
"""Retorna a próxima data do intervalo no formato DDMMAAAA.
Rastreia o progresso em um arquivo JSON.
Quando chega ao fim, para de executar."""
global RANGE_COMPLETED
try:
start_dt = _parse_date_ddmmyyyy(DATE_RANGE_START)
end_dt = _parse_date_ddmmyyyy(DATE_RANGE_END)
except ValueError as e:
print(f"Erro ao parsear datas: {e}")
raise
# Carregar estado anterior
current_date = start_dt
if os.path.exists(STATE_FILE):
try:
with open(STATE_FILE, 'r') as f:
state = json.load(f)
# Verificar se o intervalo já foi completado
if state.get('completed', False):
print(f"Intervalo de datas já foi completado!")
print(f"Última data processada: {state.get('last_date')}")
print(f"Para reiniciar, delete o arquivo '{STATE_FILE}'")
RANGE_COMPLETED = True
return None # Sinal para parar
last_date_str = state.get('last_date')
if last_date_str:
last_date = datetime.strptime(last_date_str, "%d%m%Y")
# Se a última data foi antes do fim, incrementar um dia
if last_date < end_dt:
current_date = last_date + timedelta(days=1)
else:
# Chegou ao fim
RANGE_COMPLETED = True
return None # Sinal para parar
except Exception as e:
print(f"Erro ao ler arquivo de estado: {e}. Iniciando do início.")
current_date = start_dt
print(f"Data a processar: {current_date.strftime('%d/%m/%Y')}")
return current_date.strftime("%d%m%Y")
def _save_date_state(date_str: str) -> None:
"""Salva o estado da data processada com sucesso.
Deve ser chamado apenas após a conclusão bem-sucedida.
Se for a data final, marca como completo."""
try:
end_dt = _parse_date_ddmmyyyy(DATE_RANGE_END)
current_dt = datetime.strptime(date_str, "%d%m%Y")
state = {'last_date': date_str}
# Se chegou na data final, marcar como completo
if current_dt >= end_dt:
state['completed'] = True
print(f"Intervalo de datas COMPLETO! Última data: {date_str}")
with open(STATE_FILE, 'w') as f:
json.dump(state, f)
print(f"Estado salvo: {date_str}")
except Exception as e:
print(f"Aviso: não foi possível salvar estado: {e}")
def resolve_data(use_manual: bool, manual_str: str):
"""Retorna a data no formato DDMMAAAA.
Se USE_DATE_RANGE=True, usa o intervalo de datas (uma por execução).
Se use_manual=True, valida e usa manual_str; caso contrário, usa o dia anterior.
Retorna None se o intervalo de datas foi completado."""
if USE_DATE_RANGE:
return _get_next_date_in_range()
if use_manual:
s = ''.join(ch for ch in str(manual_str) if ch.isdigit())
if len(s) == 8:
return s
else:
print("Data manual inválida; usando dia anterior automaticamente.")
return (datetime.now() - timedelta(days=1)).strftime("%d%m%Y")
def choose_sql_driver() -> str:
try:
available = pyodbc.drivers()
except Exception:
available = []
# Prefer 18, depois 17, depois o primeiro da lista que contenha 'SQL Server'
for candidate in ['ODBC Driver 18 for SQL Server', 'ODBC Driver 17 for SQL Server', 'SQL Server']:
if candidate in available:
return candidate
for d in available:
if 'SQL Server' in d:
return d
# Fallback (pode falhar na conexão se realmente não houver driver)
return 'ODBC Driver 17 for SQL Server'
def main():
# Configurar opções do Chrome para ambiente Kubernetes/Docker
chrome_options = Options()
# CONFIGURAÇÃO: Defina como False para ver o navegador (útil para debug no Windows)
USE_HEADLESS = True # True = modo headless (sem interface), False = com interface
# Configurações essenciais para rodar em Docker/Kubernetes (sem interface gráfica)
if USE_HEADLESS:
chrome_options.add_argument('--headless=new') # Novo modo headless (mais estável)
chrome_options.add_argument('--no-sandbox') # Necessário para rodar como root
chrome_options.add_argument('--disable-dev-shm-usage') # Evita problemas de memória compartilhada
# Configurações adicionais recomendadas
chrome_options.add_argument('--disable-software-rasterizer')
chrome_options.add_argument('--disable-extensions')
chrome_options.add_argument('--disable-blink-features=AutomationControlled') # Evita detecção de automação
# Configurações de janela
chrome_options.add_argument('--window-size=1920,1080')
chrome_options.add_argument('--start-maximized')
# Desabilitar notificações e popups
chrome_options.add_argument('--disable-notifications')
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging', 'enable-automation'])
chrome_options.add_experimental_option('useAutomationExtension', False)
# Definir pasta de download
# Tenta usar Desktop, se não existir usa /tmp (para Kubernetes/Docker)
desktop_dir = os.path.join(os.path.expanduser("~"), "Desktop")
if os.path.exists(desktop_dir):
download_dir = desktop_dir
else:
# Em ambiente Kubernetes/Docker, usar /tmp ou criar diretório
download_dir = "/tmp/downloads"
os.makedirs(download_dir, exist_ok=True)
print(f"Usando diretório de download: {download_dir}")
prefs = {
"download.default_directory": download_dir,
"download.prompt_for_download": False,
"download.directory_upgrade": True,
"safebrowsing.enabled": True,
"profile.default_content_setting_values.notifications": 2 # Desabilitar notificações
}
chrome_options.add_experimental_option("prefs", prefs)
# Usar o binário do Chromium instalado no sistema (para Kubernetes/Docker)
# Comentar a linha abaixo se estiver rodando localmente no Windows
# chrome_options.binary_location = '/usr/bin/chromium'
# Inicializar o driver do Chrome
print("Iniciando o navegador...")
print(f"Modo headless: {USE_HEADLESS}")
try:
# Criar o service apontando para o chromedriver do sistema (Kubernetes/Docker)
# Comentar a linha abaixo se estiver rodando localmente no Windows
# from selenium.webdriver.chrome.service import Service
# service = Service('/usr/bin/chromedriver')
# driver = webdriver.Chrome(service=service, options=chrome_options)
# Para rodar localmente no Windows, use:
driver = webdriver.Chrome(options=chrome_options)
print("Navegador iniciado com sucesso!")
except Exception as e:
print(f"Erro ao iniciar o navegador: {e}")
print("\nPossíveis soluções:")
print("1. Verifique se o Chrome está instalado")
print("2. Verifique se o ChromeDriver está instalado e atualizado")
print("3. Execute: pip install --upgrade selenium")
print("4. Tente desabilitar o modo headless (USE_HEADLESS = False)")
raise
try:
# Acessar a URL
url = "https://cp10269.retaguarda.grupoboticario.com.br/app/#/"
print(f"Acessando {url}...")
driver.get(url)
# Aguardar o campo de input estar presente e visível
print("Aguardando o campo de login aparecer...")
wait = WebDriverWait(driver, 10)
input_field = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, '[data-cy="login-usuario-input-field"]'))
)
# Digitar "daniel.medeiros" no campo de login
print("Digitando 'daniel.medeiros' no campo de login...")
input_field.clear()
input_field.send_keys("daniel.medeiros")
# Aguardar o campo de senha aparecer
print("Aguardando o campo de senha aparecer...")
password_field = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, '[data-cy="login-senha-input-field"]'))
)
# Digitar a senha
print("Digitando a senha...")
password_field.clear()
password_field.send_keys("@ginseng")
# Clicar no botão de entrar com retentativa se houver erro do reCAPTCHA
print("Clicando no botão Entrar...")
max_login_attempts = 3
for attempt in range(1, max_login_attempts + 1):
try:
entrar_button = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-cy="login-entrar-button"]'))
)
# usar JS click para maior robustez
driver.execute_script("arguments[0].click();", entrar_button)
except Exception:
try:
entrar_button = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-cy="login-entrar-button"]'))
)
entrar_button.click()
except Exception:
pass
# Verificar se o menu "Venda" apareceu como sinal de login bem-sucedido
try:
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, '[data-cy="sidemenu-item-venda"]'))
)
print("Login confirmado.")
break
except Exception:
# Verificar mensagem de erro no corpo da página
try:
body_text = driver.execute_script("return dment.body ? document.body.innerText : ''") or ""
except Exception:
body_text = ""
if 'grecaptcha.execute is not a function' in body_text.lower() or 'this.grecaptcha.execute is not a function' in body_text.lower():
print("Aviso do reCAPTCHA detectado; tentando clicar Entrar novamente...")
else:
print(f"Login ainda não confirmado (tentativa {attempt}/{max_login_attempts}). Tentando novamente...")
time.sleep(2)
if attempt == max_login_attempts:
raise Exception("Falha ao efetuar login após múltiplas tentativas.")
# Clicar em "Venda" no menu lateral
print("Clicando em 'Venda'...")
venda_menu = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-cy="sidemenu-item-venda"]'))
)
venda_menu.click()
time.sleep(1)
# Clicar em "Relatórios"
print("Clicando em 'Relatórios'...")
relatorios_menu = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-cy="sidemenu-item-relatorios"]'))
)
relatorios_menu.click()
time.sleep(1)
# Clicar em "Vendas por Hora"
print("Clicando em 'Vendas por Hora'...")
vendas_hora_menu = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-cy="sidemenu-item-vendas-por-hora"]'))
)
vendas_hora_menu.click()
# Aguardar a página de relatório carregar
print("Aguardando a página de relatório carregar...")
time.sleep(3)
# Resolver a data a ser utilizada (dia anterior ou manual), formato DDMMAAAA
data_formatada = resolve_data(USE_MANUAL_DATE, MANUAL_DATE_STR)
# Se retornar None, significa que o intervalo de datas foi completado
if data_formatada is None:
print("Intervalo de datas completado. Encerrando...")
return
print(f"Data a ser preenchida: {data_formatada}")
# Clicar no canto inferior direito da página usando JavaScript
print("Clicando no canto inferior direito da página...")
driver.execute_script("""
var width = window.innerWidth;
var height = window.innerHeight;
var element = document.elementFromPoint(width - 50, height - 50);
if (element) {
element.click();
}
""")
time.sleep(0.5)
# Apertar CTRL + K
print("Apertando CTRL + K...")
from selenium.webdriver.common.action_chains import ActionChains
actions = ActionChains(driver)
actions.key_down(Keys.CONTROL).send_keys('k').key_up(Keys.CONTROL).perform()
# Aguardar a página carregar completamente
print("Aguardando 10 segundos para a página carregar...")
time.sleep(10)
# Apertar TAB 3 vezes
print("Apertando TAB 3 vezes...")
for i in range(3):
actions.send_keys(Keys.TAB).perform()
time.sleep(0.3)
# Digitar a data do dia anterior
print(f"Digitando a data inicial: {data_formatada}")
actions.send_keys(data_formatada).perform()
time.sleep(0.5)
# Digitar a mesma data novamente (data final)
print(f"Digitando a data final: {data_formatada}")
actions.send_keys(data_formatada).perform()
time.sleep(2)
# Pressionar ENTER ou ESC para fechar possíveis popups/calendários
print("Pressionando ESC para fechar calendários...")
actions.send_keys(Keys.ESCAPE).perform()
time.sleep(1)
# Rolar a página para baixo para carregar mais elementos
print("Rolando a página para baixo para carregar elementos...")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
# Rolar para cima
driver.execute_script("window.scrollTo(0, 0);")
time.sleep(1)
# Rolar para o meio
driver.execute_script("window.scrollTo(0, document.body.scrollHeight / 2);")
time.sleep(2)
# Trocar para o iframe que contém o formulário
print("\nMudando para o iframe do formulário...")
wait.until(EC.frame_to_be_available_and_switch_to_it((By.TAG_NAME, 'iframe')))
# Confirmar que estamos no iframe correto procurando um campo conhecido
wait.until(EC.presence_of_element_located((By.ID, 'dataInicial')))
print("Dentro do iframe do formulário.")
# Tentar clicar no radio 'Produto' por várias estratégias
print("Procurando o radio 'Produto'...")
radio_xpath = "//input[@type='radio' and @name='filtro.formato' and (@id='filtro_produto' or translate(@value,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='produto')]"
try:
radio_produto = wait.until(EC.element_to_be_clickable((By.XPATH, radio_xpath)))
except Exception:
# Fallback: procurar label com texto 'Produto' e um input dentro
radio_produto = wait.until(EC.element_to_be_clickable((By.XPATH, "//label[normalize-space()[contains(., 'Produto')]]//input[@type='radio']")))
# Garantir visibilidade e clicar
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", radio_produto)
time.sleep(0.5)
driver.execute_script("arguments[0].click();", radio_produto)
print("Radio 'Produto' selecionado.")
time.sleep(0.5)
# Selecionar formato CSV (label dentro de #divArquivo)
print("Selecionando formato CSV...")
formato_csv_label = wait.until(EC.element_to_be_clickable((By.XPATH, "//*[@id='divArquivo']/div[2]/label")))
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", formato_csv_label)
time.sleep(0.3)
driver.execute_script("arguments[0].click();", formato_csv_label)
time.sleep(0.5)
# Antes de gerar, capturar arquivos existentes na pasta de download
before_files = set(f for f in os.listdir(download_dir) if not f.endswith('.crdownload'))
# Clicar no botão Gerar
print("Clicando no botão Gerar...")
botao_gerar = wait.until(EC.element_to_be_clickable((By.ID, 'gerar')))
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", botao_gerar)
time.sleep(0.3)
driver.execute_script("arguments[0].click();", botao_gerar)
# Voltar para o conteúdo principal
driver.switch_to.default_content()
# Aguardar download finalizar e tratar/renomear com a data usada
print("Aguardando download finalizar para tratar e renomear...")
end_time = time.time() + 180 # até 3 minutos
downloaded_file = None
while time.time() < end_time:
# Esperar terminar downloads parciais (.crdownload ou arquivos temporários do Chrome)
current_dir_files = os.listdir(download_dir)
if any(name.endswith('.crdownload') for name in current_dir_files):
time.sleep(1)
continue
# Filtrar arquivos válidos:
# - Não termina com .crdownload
# - Não começa com . (arquivos temporários/ocultos do Chrome)
# - Não contém .com.google.Chrome (arquivos temporários específicos)
def is_valid_file(filename):
if filename.endswith('.crdownload'):
return False
if filename.startswith('.'):
return False
if '.com.google.Chrome' in filename:
return False
return True
current_files = set(f for f in current_dir_files if is_valid_file(f))
new_files = [f for f in current_files - before_files]
if new_files:
# Escolher o mais recente
candidate = max((os.path.join(download_dir, f) for f in new_files), key=os.path.getmtime)
# Verificar se o arquivo realmente existe e tem tamanho > 0
if os.path.exists(candidate) and os.path.getsize(candidate) > 0:
downloaded_file = candidate
break
time.sleep(1)
def _normalize_label(s: str) -> str:
if s is None:
return ""
s = str(s)
s = unicodedata.normalize('NFKD', s)
s = ''.join(c for c in s if not unicodedata.combining(c))
s = s.lower()
# remover caracteres comuns
for ch in ['%', '(', ')', '/', '-', '_', '.', ':']:
s = s.replace(ch, ' ')
s = ' '.join(s.split())
return s
if downloaded_file:
print(f"Arquivo baixado detectado: {downloaded_file}")
# Preparar nome final (garantindo nome nico)
final_name = f"vendas_por_hora_{data_formatada}.csv"
final_path = os.path.join(download_dir, final_name)
if os.path.exists(final_path):
base_no_ext, ext = os.path.splitext(final_name)
counter = 1
while os.path.exists(os.path.join(download_dir, f"{base_no_ext}({counter}){ext}")):
counter += 1
final_path = os.path.join(download_dir, f"{base_no_ext}({counter}){ext}")
# Processar CSV removendo colunas e transformando campos
cols_para_remover = {
_normalize_label(n) for n in [
'Receita líquida', 'Receita liquida',
'Participação Receita (%)', 'Participacao Receita (%)',
'Maior receita', 'Maior Receita',
'Participação Boletos (%)', 'Participacao Boletos (%)'
]
}
try:
print("Abrindo arquivo CSV para leitura...")
# Detectar delimitador e ler
with open(downloaded_file, 'r', encoding='utf-8-sig', errors='ignore') as f:
print("Detectando delimitador do CSV...")
sample = f.read(4096)
f.seek(0)
try:
dialect = csv.Sniffer().sniff(sample, delimiters=';,\t|')
print(f"Delimitador detectado: '{dialect.delimiter}'")
except Exception as e:
print(f"Erro ao detectar delimitador, usando ';' por padrão: {e}")
class Simple(csv.Dialect):
delimiter = ';'
quotechar = '"'
escapechar = None
doublequote = True
skipinitialspace = False
lineterminator = '\n'
quoting = csv.QUOTE_MINIMAL
dialect = Simple
print("Lendo cabeçalhos do CSV...")
reader = csv.DictReader(f, dialect=dialect)
original_headers = reader.fieldnames or []
print(f"Cabeçalhos encontrados: {original_headers}")
# Construir novos headers, removendo colunas e substituindo Produto
new_headers = []
produto_idx = None
for h in original_headers:
nh = _normalize_label(h)
if nh in cols_para_remover:
continue
if nh == _normalize_label('Produto'):
produto_idx = len(new_headers)
new_headers.extend(['SKU', 'Descricao'])
elif nh == _normalize_label('Preço Médio') or nh == _normalize_label('Preco Medio'):
new_headers.append('PRECO_MEDIO')
else:
new_headers.append(h)
# Se Produto não existir, garante as colunas novas ao final
if 'SKU' not in new_headers:
new_headers.extend(['SKU', 'Descricao'])
print(f"Novos cabeçalhos processados: {new_headers}")
print("Processando linhas do CSV...")
rows_out = []
row_count = 0
for row in reader:
row_count += 1
if row_count % 100 == 0:
print(f"Processadas {row_count} linhas...")
new_row = {}
for h in original_headers:
nh = _normalize_label(h)
if nh in cols_para_remover:
continue
if nh == _normalize_label('Produto'):
val = row.get(h, '') or ''
parts = [p.strip() for p in val.split(' - ', 1)]
sku = parts[0] if parts else ''
desc = parts[1] if len(parts) > 1 else val
new_row['SKU'] = sku
new_row['Descricao'] = desc
elif nh == _normalize_label('Preço Médio') or nh == _normalize_label('Preco Medio'):
val = row.get(h, '')
new_row['PRECO_MEDIO'] = val
else:
val = row.get(h, '')
if nh == _normalize_label('Loja'):
val = (val or '').split(' - ', 1)[0].strip()
new_row[h] = val
# Garantir chaves SKU/Descricao existam
new_row.setdefault('SKU', '')
new_row.setdefault('Descricao', '')
new_row.setdefault('PRECO_MEDIO', '')
rows_out.append(new_row)
print(f"Total de linhas processadas: {row_count}")
print(f"Total de linhas válidas: {len(rows_out)}")
# Inserir os dados tratados no banco de dados
print("Preparando para inserir dados no banco...")
def _find_header(headers, targets):
targets_norm = { _normalize_label(t) for t in targets }
for hh in headers:
if _normalize_label(hh) in targets_norm:
return hh
return None
def _parse_number(val):
s = str(val or '').strip()
if s == '':
return 0
# converter formato pt-BR para ponto flutuante
s = s.replace('.', '').replace(',', '.')
try:
num = float(s)
# se for inteiro, retorna int
return int(num) if abs(num - int(num)) < 1e-9 else num
except Exception:
return 0
# Mapear colunas de interesse
pdv_header = _find_header(new_headers, ['Loja', 'PDV'])
vendas_header = _find_header(new_headers, ['Vendas', 'Quantidade', 'Qtd', 'Qtd. Vendida', 'Qtd Vendida', 'Qtde Vendida', 'Qtde. Vendida', 'Quantidade Vendida', 'Qtde'])
preco_medio_header = _find_header(new_headers, ['PRECO_MEDIO', 'Preço Médio', 'Preco Medio'])
if not pdv_header:
raise Exception("Não foi possível identificar a coluna de PDV/Loja no CSV tratado.")
if not vendas_header:
print("Aviso: coluna de Vendas não encontrada; valores serão inseridos como 0.")
if not preco_medio_header:
print("Aviso: coluna de Preço Médio não encontrada; valores serão inseridos como 0.00.")
# Conectar ao SQL Server
print("Escolhendo driver SQL Server...")
driver_name = choose_sql_driver()
print(f"Driver selecionado: {driver_name}")
connection_string = (
f'DRIVER={{{driver_name}}};'
f'SERVER=10.77.77.10;'
f'DATABASE=GINSENG;'
f'UID=supginseng;'
f'PWD=Ginseng@;'
f'PORT=1433;'
f'TrustServerCertificate=yes;'
f'Encrypt=yes'
)
# Converter data para formato aceito pelo banco (date)
data_db = datetime.strptime(data_formatada, '%d%m%Y').date()
print(f"Data para inserção no banco: {data_db}")
print("Conectando ao banco de dados...")
inserted = 0
with pyodbc.connect(connection_string) as conn:
print("Conexão estabelecida com sucesso!")
conn.autocommit = False
cur = conn.cursor()
# Apagar dados existentes para a data
print(f"Deletando dados existentes para a data {data_db}...")
cur.execute("DELETE FROM [GINSENG].[dbo].[rgb_sales_selenium] WHERE [Data] = ?", data_db)
print("Dados antigos deletados.")
# Inserir linhas
print("Preparando inserção de dados...")
insert_sql = (
"INSERT INTO [GINSENG].[dbo].[rgb_sales_selenium] ([Data],[PDV],[SKU],[DESCRICAO],[VENDAS],[PRECO_MEDIO]) "
"VALUES (?,?,?,?,?,?)"
)
batch = []
batch_count = 0
for r in rows_out:
pdv_val = r.get(pdv_header, '')
try:
pdv_val = int(str(pdv_val).strip().split()[0]) if str(pdv_val).strip() else None
except Exception:
pdv_val = None
sku_val = r.get('SKU', '')
desc_val = r.get('Descricao', '')
vendas_val = _parse_number(r.get(vendas_header, '')) if vendas_header else 0
preco_medio_val = _parse_number(r.get(preco_medio_header, '')) if preco_medio_header else 0.00
if pdv_val is None and r.get('Loja'):
try:
pdv_val = int(str(r.get('Loja')).strip().split()[0])
except Exception:
pdv_val = None
batch.append((data_db, pdv_val, sku_val, desc_val, vendas_val, preco_medio_val))
if len(batch) >= 1000:
batch_count += 1
print(f"Inserindo lote {batch_count} ({len(batch)} registros)...")
cur.executemany(insert_sql, batch)
inserted += len(batch)
batch = []
if batch:
batch_count += 1
print(f"Inserindo lote final {batch_count} ({len(batch)} registros)...")
cur.executemany(insert_sql, batch)
inserted += len(batch)
print("Fazendo commit das alterações...")
conn.commit()
print("Commit realizado com sucesso!")
print(f"Dados inseridos no banco: {inserted} registros para a data {data_db}.")
# Remover arquivo original
print(f"Removendo arquivo temporário: {downloaded_file}")
try:
os.remove(downloaded_file)
print("Arquivo removido com sucesso.")
except Exception as e:
print(f"Aviso: não foi possível remover o arquivo: {e}")
# Salvar estado apenas após sucesso completo
if USE_DATE_RANGE:
print("Salvando estado do processamento...")
_save_date_state(data_formatada)
print("Processamento concluído com sucesso!")
except Exception as e:
print(f"Falha ao tratar o CSV: {e}")
import traceback
traceback.print_exc()
else:
print("Não foi possível detectar o arquivo baixado dentro do tempo limite.")
# Pausa breve antes de encerrar
time.sleep(2)
print("Fechando o navegador...")
except Exception as e:
print(f"Erro durante a execução: {e}")
finally:
# Fechar o navegador
driver.quit()
print("Script finalizado.")
def run_with_retry(max_retries=3, retry_delay=120):
"""Executa main() com retentativas em caso de erro de timeout/conexão/arquivo."""
for attempt in range(1, max_retries + 1):
try:
main()
return True # Sucesso
except Exception as e:
error_str = str(e).lower()
# Verificar se é erro de timeout/conexão/arquivo temporário que vale retry
is_retryable = any(keyword in error_str for keyword in [
'read timed out',
'timed out',
'timeout',
'connectionpool',
'connection refused',
'connection reset',
'httpconnectionpool',
'no such file or directory',
'.com.google.chrome',
'filenotfounderror'
])
if is_retryable and attempt < max_retries:
print(f"\n{'=' * 60}")
print(f"Erro de conexão/timeout detectado (tentativa {attempt}/{max_retries})")
print(f"Erro: {e}")
print(f"Aguardando {retry_delay} segundos antes de tentar novamente...")
print(f"{'=' * 60}\n")
time.sleep(retry_delay)
else:
if attempt == max_retries and is_retryable:
print(f"\n{'=' * 60}")
print(f"Erro persistente após {max_retries} tentativas.")
print(f"Último erro: {e}")
print(f"{'=' * 60}\n")
raise
return False
if __name__ == "__main__":
if USE_DATE_RANGE:
print("=" * 60)
print("MODO LOOP ATIVADO - O script executará continuamente")
print(f"Intervalo: {DATE_RANGE_START} até {DATE_RANGE_END}")
print("=" * 60)
while not RANGE_COMPLETED:
try:
run_with_retry(max_retries=3, retry_delay=120)
if RANGE_COMPLETED:
break
print("\n" + "=" * 60)
print("Execução concluída. Aguardando 10 segundos antes da próxima execução...")
print("=" * 60 + "\n")
time.sleep(10) # Aguarda 10 segundos antes da próxima execução
except Exception as e:
print(f"\nErro na execução: {e}")
print("Tentando novamente em 10 segundos...")
time.sleep(10)
print("\n" + "=" * 60)
print("Intervalo de datas completado. Script finalizado.")
print("=" * 60)
else:
run_with_retry(max_retries=3, retry_delay=120)

290
refreshprice.py Normal file
View File

@ -0,0 +1,290 @@
import pyodbc
# ==============================
# Conexão com o banco de dados
# ==============================
DB_CONNECTION_STRING = (
'DRIVER={ODBC Driver 18 for SQL Server};'
'SERVER=10.77.77.10;'
'DATABASE=GINSENG;'
'UID=supginseng;'
'PWD=Ginseng@;'
'PORT=1433;'
'TrustServerCertificate=yes'
)
def refresh_prices():
"""Atualiza a tabela price com os dados mais recentes do draft_historico"""
print("="*60)
print("REFRESH PRICE - Atualização de Preços")
print("="*60)
conn = pyodbc.connect(DB_CONNECTION_STRING)
cursor = conn.cursor()
# 1. Buscar dados mais recentes do draft_historico
print("\n[1/4] Buscando dados mais recentes do draft_historico...")
query_draft = """
SELECT
[loja_id] as PDV,
[code] as SKU,
[pricesellin] as PRICE,
[data]
FROM [GINSENG].[dbo].[draft_historico]
WHERE [data] = (
SELECT MAX([data])
FROM [GINSENG].[dbo].[draft_historico]
)
"""
cursor.execute(query_draft)
draft_data = cursor.fetchall()
if not draft_data:
print(" ✗ Nenhum dado encontrado no draft_historico")
cursor.close()
conn.close()
return None
# Pegar a data mais recente para exibir
data_mais_recente = draft_data[0][3] if draft_data else None
print(f" ✓ Data mais recente: {data_mais_recente}")
print(f"{len(draft_data)} registros encontrados")
# 2. Criar conjunto de chaves PDV+SKU
print("\n[2/4] Criando chaves únicas PDV+SKU...")
# Criar dicionário com chave PDV+SKU e valor PRICE
draft_dict = {}
for row in draft_data:
pdv = str(row[0])
sku = str(row[1])
price = row[2]
key = f"{pdv}_{sku}"
draft_dict[key] = {"pdv": pdv, "sku": sku, "price": price}
print(f"{len(draft_dict)} chaves únicas criadas")
# 3. Deletar registros existentes na tabela price usando tabela temporária
print("\n[3/4] Deletando registros existentes na tabela price...")
# Criar tabela temporária
print(" Criando tabela temporária...")
cursor.execute("IF OBJECT_ID('tempdb..#keys_to_delete') IS NOT NULL DROP TABLE #keys_to_delete")
cursor.execute("CREATE TABLE #keys_to_delete (PDV VARCHAR(50), SKU VARCHAR(50))")
conn.commit()
# Inserir chaves na tabela temporária em batches
print(" Inserindo chaves na tabela temporária...")
keys_list = [(d['pdv'], d['sku']) for d in draft_dict.values()]
batch_size = 1000
for i in range(0, len(keys_list), batch_size):
batch = keys_list[i:i + batch_size]
placeholders = ",".join(["(?, ?)" for _ in batch])
insert_temp_query = f"INSERT INTO #keys_to_delete (PDV, SKU) VALUES {placeholders}"
params = [item for pair in batch for item in pair]
cursor.execute(insert_temp_query, params)
if (i + batch_size) % 10000 == 0 or (i + batch_size) >= len(keys_list):
print(f"{min(i + batch_size, len(keys_list))}/{len(keys_list)} chaves inseridas na temp...")
conn.commit()
# Deletar usando JOIN (muito mais rápido)
print(" Executando DELETE com JOIN...")
delete_query = """
DELETE p
FROM [GINSENG].[dbo].[price] p
INNER JOIN #keys_to_delete t ON p.[PDV] = t.[PDV] AND p.[SKU] = t.[SKU]
"""
cursor.execute(delete_query)
deleted_count = cursor.rowcount
conn.commit()
# Limpar tabela temporária
cursor.execute("DROP TABLE #keys_to_delete")
conn.commit()
print(f"{deleted_count} registros deletados no total")
# 4. Inserir novos registros na tabela price (diretamente do draft_historico)
print("\n[4/4] Inserindo novos registros na tabela price...")
insert_query = """
INSERT INTO [GINSENG].[dbo].[price] ([PDV], [SKU], [PRICE])
SELECT [loja_id], [code], [pricesellin]
FROM [GINSENG].[dbo].[draft_historico]
WHERE [data] = (
SELECT MAX([data])
FROM [GINSENG].[dbo].[draft_historico]
)
"""
cursor.execute(insert_query)
inserted_count = cursor.rowcount
conn.commit()
errors = 0
print(f"{inserted_count} registros inseridos")
# ============================================================
# PARTE 2: Processar intra_planejamento_lancamento
# ============================================================
print(f"\n{'='*60}")
print("PARTE 2: INTRA_PLANEJAMENTO_LANCAMENTO")
print(f"{'='*60}")
# 5. Buscar dados do intra_planejamento_lancamento
print("\n[5/8] Buscando dados do intra_planejamento_lancamento...")
query_intra = """
SELECT [PDV],
[PRODUTO_LANCAMENTO] as SKU,
[PRECO_UND] as PRICE,
[DT_ATUALIZACAO]
FROM [GINSENG].[dbo].[intra_planejamento_lancamento]
"""
cursor.execute(query_intra)
intra_data = cursor.fetchall()
if not intra_data:
print(" ✗ Nenhum dado encontrado no intra_planejamento_lancamento")
intra_deleted_count = 0
intra_inserted_count = 0
else:
print(f"{len(intra_data)} registros encontrados")
# 6. Criar conjunto de chaves PDV+SKU
print("\n[6/8] Criando chaves únicas PDV+SKU...")
intra_dict = {}
for row in intra_data:
pdv = str(row[0])
sku = str(row[1])
price = row[2]
key = f"{pdv}_{sku}"
intra_dict[key] = {"pdv": pdv, "sku": sku, "price": price}
print(f"{len(intra_dict)} chaves únicas criadas")
# 7. Deletar registros existentes na tabela price usando tabela temporária
print("\n[7/8] Deletando registros existentes na tabela price...")
# Criar tabela temporária
print(" Criando tabela temporária...")
cursor.execute("IF OBJECT_ID('tempdb..#keys_to_delete_intra') IS NOT NULL DROP TABLE #keys_to_delete_intra")
cursor.execute("CREATE TABLE #keys_to_delete_intra (PDV VARCHAR(50), SKU VARCHAR(50))")
conn.commit()
# Inserir chaves na tabela temporária em batches
print(" Inserindo chaves na tabela temporária...")
intra_keys_list = [(d['pdv'], d['sku']) for d in intra_dict.values()]
batch_size = 1000
for i in range(0, len(intra_keys_list), batch_size):
batch = intra_keys_list[i:i + batch_size]
placeholders = ",".join(["(?, ?)" for _ in batch])
insert_temp_query = f"INSERT INTO #keys_to_delete_intra (PDV, SKU) VALUES {placeholders}"
params = [item for pair in batch for item in pair]
cursor.execute(insert_temp_query, params)
if (i + batch_size) % 10000 == 0 or (i + batch_size) >= len(intra_keys_list):
print(f"{min(i + batch_size, len(intra_keys_list))}/{len(intra_keys_list)} chaves inseridas na temp...")
conn.commit()
# Deletar usando JOIN
print(" Executando DELETE com JOIN...")
delete_query_intra = """
DELETE p
FROM [GINSENG].[dbo].[price] p
INNER JOIN #keys_to_delete_intra t ON p.[PDV] = t.[PDV] AND p.[SKU] = t.[SKU]
"""
cursor.execute(delete_query_intra)
intra_deleted_count = cursor.rowcount
conn.commit()
# Limpar tabela temporária
cursor.execute("DROP TABLE #keys_to_delete_intra")
conn.commit()
print(f"{intra_deleted_count} registros deletados no total")
# 8. Inserir novos registros na tabela price
print("\n[8/8] Inserindo novos registros na tabela price...")
insert_query_intra = """
INSERT INTO [GINSENG].[dbo].[price] ([PDV], [SKU], [PRICE])
SELECT [PDV], [PRODUTO_LANCAMENTO], [PRECO_UND]
FROM [GINSENG].[dbo].[intra_planejamento_lancamento]
"""
cursor.execute(insert_query_intra)
intra_inserted_count = cursor.rowcount
conn.commit()
print(f"{intra_inserted_count} registros inseridos")
cursor.close()
conn.close()
# Resumo final
print(f"\n{'='*60}")
print("RESUMO FINAL")
print(f"{'='*60}")
print("\n📦 DRAFT_HISTORICO:")
print(f" Data processada: {data_mais_recente}")
print(f" Registros no draft: {len(draft_data)}")
print(f" Chaves únicas: {len(draft_dict)}")
print(f" Deletados do price: {deleted_count}")
print(f" Inseridos no price: {inserted_count}")
print("\n📦 INTRA_PLANEJAMENTO_LANCAMENTO:")
print(f" Registros no intra: {len(intra_data) if intra_data else 0}")
print(f" Chaves únicas: {len(intra_dict) if 'intra_dict' in locals() else 0}")
print(f" Deletados do price: {intra_deleted_count if 'intra_deleted_count' in locals() else 0}")
print(f" Inseridos no price: {intra_inserted_count if 'intra_inserted_count' in locals() else 0}")
total_deleted = deleted_count + (intra_deleted_count if 'intra_deleted_count' in locals() else 0)
total_inserted = inserted_count + (intra_inserted_count if 'intra_inserted_count' in locals() else 0)
print(f"\n📊 TOTAL:")
print(f" Total deletados: {total_deleted}")
print(f" Total inseridos: {total_inserted}")
print(f" Erros: {errors}")
print(f"{'='*60}")
if errors == 0:
print("✓ SUCESSO TOTAL!")
else:
print("⚠ Concluído com alguns problemas")
return {
"data": data_mais_recente,
"draft_records": len(draft_data),
"draft_unique_keys": len(draft_dict),
"draft_deleted": deleted_count,
"draft_inserted": inserted_count,
"intra_records": len(intra_data) if intra_data else 0,
"intra_deleted": intra_deleted_count if 'intra_deleted_count' in locals() else 0,
"intra_inserted": intra_inserted_count if 'intra_inserted_count' in locals() else 0,
"total_deleted": total_deleted,
"total_inserted": total_inserted,
"errors": errors
}
# ==============================
# EXECUTAR
# ==============================
if __name__ == "__main__":
refresh_prices()

908
rgb_fiscal_invoices.py Normal file
View File

@ -0,0 +1,908 @@
import requests
import pyodbc
import json
from datetime import datetime, timedelta
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
# Configuração de logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# ===== CONFIGURAÇÕES DO SCRIPT =====
# Usar data do dia anterior automaticamente
USE_YESTERDAY = True # True: busca apenas o dia anterior | False: usa intervalo de datas abaixo
# Intervalo de datas para buscar invoices (formato: YYYY-MM-DD)
# Usado apenas quando USE_YESTERDAY = False
START_DATE = "2025-06-01" # Data inicial
END_DATE = "2025-12-17" # Data final (inclusiva)
# ===================================
class RGBFiscalInvoicesExtractor:
def __init__(self):
# Configurações da API
self.api_url = "https://api.grupoboticario.com.br/global/v1/franchising/gb-stores-data/fiscal/invoices"
# Configurações do banco de dados
self.driver = "ODBC Driver 17 for SQL Server"
self.connection_string = (
f'DRIVER={{{self.driver}}};'
'SERVER=10.77.77.10;'
'DATABASE=GINSENG;'
'UID=supginseng;'
'PWD=Ginseng@;'
'PORT=1433;'
'TrustServerCertificate=yes;'
'Encrypt=yes'
)
def get_bearer_token_from_db(self):
"""
Busca o token de autenticação na tabela rgb_token
"""
connection = None
cursor = None
try:
logger.info("Conectando ao banco de dados para buscar token...")
connection = pyodbc.connect(self.connection_string)
cursor = connection.cursor()
# Query para buscar o token mais recente
query = "SELECT TOP 1 token FROM [GINSENG].[dbo].[rgb_token] ORDER BY updatedAt DESC"
cursor.execute(query)
result = cursor.fetchone()
if result:
token = result[0]
logger.info("Token recuperado com sucesso do banco de dados")
return token
else:
logger.error("Nenhum token encontrado na tabela rgb_token")
raise Exception("Token não encontrado no banco de dados")
except pyodbc.Error as e:
logger.error(f"Erro ao conectar ao banco de dados: {e}")
raise
finally:
if cursor:
cursor.close()
if connection:
connection.close()
def fetch_page(self, headers, updated_at_date, start, count, page_num, max_retries=3):
"""
Faz uma única requisição para buscar uma página específica
Implementa retry automático com backoff exponencial para erros 429
"""
params = {
'invoice.updatedAt': updated_at_date,
'start': start,
'count': count
}
for attempt in range(max_retries):
try:
if attempt > 0:
logger.info(f"Tentativa {attempt + 1}/{max_retries} - Página {page_num} (start: {start})")
else:
logger.info(f"Fazendo requisição - Página {page_num} (start: {start}, count: {count})")
response = requests.get(self.api_url, headers=headers, params=params, timeout=60)
response.raise_for_status()
data = response.json()
items = data.get('items', [])
logger.info(f"Página {page_num}: recebidos {len(items)} registros")
return {
'page': page_num,
'start': start,
'items': items,
'total': data.get('total', 0)
}
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429: # Too Many Requests
if attempt < max_retries - 1:
# Backoff exponencial: 2s, 4s, 8s
wait_time = 2 ** (attempt + 1)
logger.warning(f"Erro 429 na página {page_num}. Aguardando {wait_time}s antes de tentar novamente...")
time.sleep(wait_time)
continue
else:
logger.error(f"Erro 429 na página {page_num} após {max_retries} tentativas")
raise
else:
# Outros erros HTTP, não tenta novamente
raise
except Exception as e:
# Outros erros, não tenta novamente
raise
def get_fiscal_invoices_data(self, updated_at_date="2025-09-27"):
"""
Extrai dados da API de invoices fiscais do Grupo Boticário
Implementa paginação PARALELA para buscar todos os registros rapidamente
Faz até 5 requisições simultâneas
"""
try:
# Buscar token do banco de dados
bearer_token = self.get_bearer_token_from_db()
headers = {
'Authorization': f'Bearer {bearer_token}',
'Content-Type': 'application/json'
}
count = 25 # Tamanho da página (padrão da API)
max_workers = 3 # Número de requisições paralelas (reduzido para evitar rate limit)
logger.info(f"Iniciando busca PARALELA na API de invoices fiscais - Data: {updated_at_date}")
logger.info(f"Configuração: {max_workers} requisições simultâneas, {count} registros por página")
logger.info("Sistema de retry automático ativado para erros 429 (Too Many Requests)")
# Primeira requisição para descobrir o total de registros
logger.info("Fazendo requisição inicial para descobrir total de registros...")
first_response = self.fetch_page(headers, updated_at_date, 0, count, 1)
total_records = first_response['total']
all_invoices = first_response['items']
logger.info(f"Total de registros disponíveis: {total_records}")
if total_records <= count:
# Se tem apenas uma página, retorna direto
logger.info("Apenas uma página encontrada, busca concluída")
return {
'start': 0,
'count': len(all_invoices),
'total': total_records,
'items': all_invoices
}
# Calcular quantas páginas faltam buscar
total_pages = (total_records + count - 1) // count # Arredonda para cima
remaining_pages = []
for page_num in range(2, total_pages + 1):
start = (page_num - 1) * count
remaining_pages.append((start, page_num))
logger.info(f"Total de páginas: {total_pages}. Buscando {len(remaining_pages)} páginas restantes em paralelo...")
failed_pages = []
# Buscar páginas restantes em paralelo
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# Criar futures para todas as páginas
futures = {
executor.submit(self.fetch_page, headers, updated_at_date, start, count, page_num): (start, page_num)
for start, page_num in remaining_pages
}
# Coletar resultados conforme completam
for future in as_completed(futures):
start, page_num = futures[future]
try:
result = future.result()
all_invoices.extend(result['items'])
except Exception as e:
logger.error(f"Erro ao buscar página {page_num}: {e}")
failed_pages.append((start, page_num))
# Tentar páginas que falharam novamente (sequencialmente)
if failed_pages:
logger.warning(f"Tentando novamente {len(failed_pages)} página(s) que falharam...")
for start, page_num in failed_pages:
try:
logger.info(f"Retry sequencial - Página {page_num}")
time.sleep(2) # Espera 2s entre tentativas
result = self.fetch_page(headers, updated_at_date, start, count, page_num)
all_invoices.extend(result['items'])
logger.info(f"Página {page_num} recuperada com sucesso!")
except Exception as e:
logger.error(f"Falha definitiva na página {page_num}: {e}")
logger.info(f"Busca paginada paralela concluída. Total de registros coletados: {len(all_invoices)}")
# Retornar dados no mesmo formato da resposta original
final_data = {
'start': 0,
'count': len(all_invoices),
'total': total_records,
'items': all_invoices
}
return final_data
except requests.exceptions.RequestException as e:
logger.error(f"Erro na requisição da API: {e}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Status Code: {e.response.status_code}")
logger.error(f"Response: {e.response.text}")
raise
except json.JSONDecodeError as e:
logger.error(f"Erro ao decodificar JSON: {e}")
raise
def connect_database(self):
"""
Conecta ao banco de dados SQL Server
"""
try:
logger.info("Conectando ao banco de dados...")
connection = pyodbc.connect(self.connection_string)
logger.info("Conexão estabelecida com sucesso")
return connection
except pyodbc.Error as e:
logger.error(f"Erro ao conectar ao banco de dados: {e}")
raise
def parse_datetime(self, date_string):
"""
Converte string de data para formato datetime
"""
if not date_string:
return None
try:
# Remove o timezone offset e converte
if date_string.endswith('-03:00') or date_string.endswith('+00:00'):
date_string = date_string[:-6]
elif 'T' in date_string and date_string.endswith('.000'):
date_string = date_string[:-4]
# Tenta diferentes formatos de data
formats = [
'%Y-%m-%dT%H:%M:%S.%f',
'%Y-%m-%dT%H:%M:%S',
'%Y-%m-%d %H:%M:%S',
'%Y-%m-%d'
]
for fmt in formats:
try:
return datetime.strptime(date_string, fmt)
except ValueError:
continue
logger.warning(f"Não foi possível converter a data: {date_string}")
return None
except Exception as e:
logger.error(f"Erro ao converter data {date_string}: {e}")
return None
def insert_invoice_item(self, cursor, invoice_data, item_data=None):
"""
Insere um item de invoice na tabela rgb_fiscal_invoices
Se item_data for None, insere apenas os dados da invoice (sem itens)
"""
insert_query = """
INSERT INTO [GINSENG].[dbo].[rgb_fiscal_invoices]
([invoiceId], [storeId], [stockLocationId], [fiscalOperationId], [fiscalOperationDescription],
[supplierId], [supplierName], [clientId], [clientName], [emissionDate], [operationDate],
[dateOfPassageOnFiscalPost], [deletedAt], [updatedAt], [invoiceNumber], [serie], [key],
[buyerEmployeeId], [emitterEmployeeId], [cfop], [operationType], [shippingType],
[paymentCondition], [fiscalDocumentType], [modality], [updateCost], [updateStock],
[composeABC], [situation], [observation], [importType], [classification],
[financeGenerationType], [shippingValue], [otherExpensesValue], [insuranceValue],
[discountValue], [totalItemsValue], [documentValue], [valorDoDAE], [baseDeCalculoDoICMS],
[valorDoICMS], [baseDeCalculoDoICMSSubstituicaoTributaria], [valorDoICMSSubstituicaoTributaria],
[valorDoIPI], [valorDoPIS], [valorDoCOFINS], [valorDoICMSDesonerado], [baseDeCalculoFecop],
[valorFecop], [baseDeCalculoFecopSubstituicaoTributaria], [valorFecopSubstituicaoTributaria],
[itemId], [itemProductId], [itemSequential], [itemOrderNumber], [itemOrderItemSequencial],
[itemComposeTotal], [itemCfop], [itemUnitType], [itemQuantityOfItensOnUnit], [itemQuantity],
[itemCompleteQuantity], [itemUnitValue], [itemDiscountInputType], [itemUntaxedDiscountValue],
[itemTaxedDiscountValue], [itemDiscountPercentage], [itemShippingInputType], [itemShippingValue],
[itemShippingPercentage], [itemInsuranceInputType], [itemInsuranceValue], [itemInsurancePercentage],
[itemOtherExpensesInputValue], [itemOtherExpensesValue], [itemOtherExpensesPercentage],
[itemTotalValue], [itemTaxedPercentage], [itemProductCost], [itemFiscalCost], [itemAverageCost],
[itemTipoDeEntradaDAE], [itemValorDoDAE], [itemPercentualDoDAE], [itemFiscalSituationId],
[itemCsosn], [itemOutrasDespesasCompoeBaseDeCalculoIcms], [itemTributacao], [itemNcm],
[itemCest], [itemAliquotaNacional], [itemAliquotaImportado], [itemAliquotaEstadual],
[itemAliquotaMunicipal], [itemModalidadeDaBaseDeCalculo], [itemPercentualICMSDeCompra],
[itemValorDoICMS], [itemValorDoICMSNoSimples], [itemBaseDeCalculoDoICMS],
[itemBaseDeCalculoDoICMSComSubstituicaoTributaria], [itemAliquotaDoICMSComSubstituicaoTributaria],
[itemValorDoICMSComSubstituicaoTributaria], [itemPercentualDeAgregacao],
[itemPercentualDeReducaoDASubstituicaoTributaria], [itemAliquotaDoICMS], [itemAliquotaDoICMSDeVenda],
[itemAliquotaDoICMSAntecipado], [itemValorDoICMSAntecipado], [itemAliquotaNoSimples],
[itemCstDoIPI], [itemBaseDeCalculoDoIPI], [itemAliquotaDoIPI], [itemTipoDeEntradaIPI],
[itemValorDoIPI], [itemPercentualDoIPI], [itemBaseDeCalculoDoPIS], [itemAliquotaDoPIS],
[itemValorDoPIS], [itemBaseDeCalculoDoCOFINS], [itemAliquotaDoCOFINS], [itemValorDoCOFINS],
[itemCodigoNaturezaDeImpostoFederal], [itemBaseDeCalculoDoFecop], [itemAliquotaDoFecop],
[itemValorDoFecop], [itemBaseDeCalculoDoFecopSubstituto], [itemAliquotaDoFecopSubstituto],
[itemValorDoFecopSubstituto], [itemValorDoICMSDesonerado], [itemMotivoDesoneracao],
[itemCodigoBeneficioFiscal], [itemPercentualDiferimento], [itemValorICMSDiferimento])
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
# Preparar os dados da invoice (campos principais)
invoice_values = [
invoice_data.get('id'), # invoiceId
invoice_data.get('storeId'),
invoice_data.get('stockLocationId'),
invoice_data.get('fiscalOperationId'),
invoice_data.get('fiscalOperationDescription'),
invoice_data.get('supplierId'),
invoice_data.get('supplierName'),
invoice_data.get('clientId'),
invoice_data.get('clientName'),
self.parse_datetime(invoice_data.get('emissionDate')),
self.parse_datetime(invoice_data.get('operationDate')),
self.parse_datetime(invoice_data.get('dateOfPassageOnFiscalPost')),
self.parse_datetime(invoice_data.get('deletedAt')),
self.parse_datetime(invoice_data.get('updatedAt')),
invoice_data.get('invoiceNumber'),
invoice_data.get('serie'),
invoice_data.get('key'),
invoice_data.get('buyerEmployeeId'),
invoice_data.get('emitterEmployeeId'),
invoice_data.get('cfop'),
invoice_data.get('operationType'),
invoice_data.get('shippingType'),
invoice_data.get('paymentCondition'),
invoice_data.get('fiscalDocumentType'),
invoice_data.get('modality'),
invoice_data.get('updateCost'),
invoice_data.get('updateStock'),
invoice_data.get('composeABC'),
invoice_data.get('situation'),
invoice_data.get('observation'),
invoice_data.get('importType'),
invoice_data.get('classification'),
invoice_data.get('financeGenerationType'),
invoice_data.get('shippingValue'),
invoice_data.get('otherExpensesValue'),
invoice_data.get('insuranceValue'),
invoice_data.get('discountValue'),
invoice_data.get('totalItemsValue'),
invoice_data.get('documentValue'),
invoice_data.get('valorDoDAE'),
invoice_data.get('baseDeCalculoDoICMS'),
invoice_data.get('valorDoICMS'),
invoice_data.get('baseDeCalculoDoICMSSubstituicaoTributaria'),
invoice_data.get('valorDoICMSSubstituicaoTributaria'),
invoice_data.get('valorDoIPI'),
invoice_data.get('valorDoPIS'),
invoice_data.get('valorDoCOFINS'),
invoice_data.get('valorDoICMSDesonerado'),
invoice_data.get('baseDeCalculoFecop'),
invoice_data.get('valorFecop'),
invoice_data.get('baseDeCalculoFecopSubstituicaoTributaria'),
invoice_data.get('valorFecopSubstituicaoTributaria')
]
# Preparar os dados do item (se existir)
if item_data:
item_values = [
item_data.get('id'), # itemId
item_data.get('productId'), # itemProductId
item_data.get('sequential'), # itemSequential
item_data.get('orderNumber'), # itemOrderNumber
item_data.get('orderItemSequencial'), # itemOrderItemSequencial
item_data.get('composeTotal'), # itemComposeTotal
item_data.get('cfop'), # itemCfop
item_data.get('unitType'), # itemUnitType
item_data.get('quantityOfItensOnUnit'), # itemQuantityOfItensOnUnit
item_data.get('quantity'), # itemQuantity
item_data.get('completeQuantity'), # itemCompleteQuantity
item_data.get('unitValue'), # itemUnitValue
item_data.get('discountInputType'), # itemDiscountInputType
item_data.get('untaxedDiscountValue'), # itemUntaxedDiscountValue
item_data.get('taxedDiscountValue'), # itemTaxedDiscountValue
item_data.get('discountPercentage'), # itemDiscountPercentage
item_data.get('shippingInputType'), # itemShippingInputType
item_data.get('shippingValue'), # itemShippingValue
item_data.get('shippingPercentage'), # itemShippingPercentage
item_data.get('insuranceInputType'), # itemInsuranceInputType
item_data.get('insuranceValue'), # itemInsuranceValue
item_data.get('insurancePercentage'), # itemInsurancePercentage
item_data.get('otherExpensesInputValue'), # itemOtherExpensesInputValue
item_data.get('otherExpensesValue'), # itemOtherExpensesValue
item_data.get('otherExpensesPercentage'), # itemOtherExpensesPercentage
item_data.get('totalValue'), # itemTotalValue
item_data.get('taxedPercentage'), # itemTaxedPercentage
item_data.get('productCost'), # itemProductCost
item_data.get('fiscalCost'), # itemFiscalCost
item_data.get('averageCost'), # itemAverageCost
item_data.get('tipoDeEntradaDAE'), # itemTipoDeEntradaDAE
item_data.get('valorDoDAE'), # itemValorDoDAE
item_data.get('percentualDoDAE'), # itemPercentualDoDAE
item_data.get('fiscalSituationId'), # itemFiscalSituationId
item_data.get('csosn'), # itemCsosn
item_data.get('outrasDespesasCompoeBaseDeCalculoIcms'), # itemOutrasDespesasCompoeBaseDeCalculoIcms
item_data.get('tributacao'), # itemTributacao
item_data.get('ncm'), # itemNcm
item_data.get('cest'), # itemCest
item_data.get('aliquotaNacional'), # itemAliquotaNacional
item_data.get('aliquotaImportado'), # itemAliquotaImportado
item_data.get('aliquotaEstadual'), # itemAliquotaEstadual
item_data.get('aliquotaMunicipal'), # itemAliquotaMunicipal
item_data.get('modalidadeDaBaseDeCalculo'), # itemModalidadeDaBaseDeCalculo
item_data.get('percentualICMSDeCompra'), # itemPercentualICMSDeCompra
item_data.get('valorDoICMS'), # itemValorDoICMS
item_data.get('valorDoICMSNoSimples'), # itemValorDoICMSNoSimples
item_data.get('baseDeCalculoDoICMS'), # itemBaseDeCalculoDoICMS
item_data.get('baseDeCalculoDoICMSComSubstituicaoTributaria'), # itemBaseDeCalculoDoICMSComSubstituicaoTributaria
item_data.get('aliquotaDoICMSComSubstituicaoTributaria'), # itemAliquotaDoICMSComSubstituicaoTributaria
item_data.get('valorDoICMSComSubstituicaoTributaria'), # itemValorDoICMSComSubstituicaoTributaria
item_data.get('percentualDeAgregacao'), # itemPercentualDeAgregacao
item_data.get('percentualDeReducaoDASubstituicaoTributaria'), # itemPercentualDeReducaoDASubstituicaoTributaria
item_data.get('aliquotaDoICMS'), # itemAliquotaDoICMS
item_data.get('aliquotaDoICMSDeVenda'), # itemAliquotaDoICMSDeVenda
item_data.get('aliquotaDoICMSAntecipado'), # itemAliquotaDoICMSAntecipado
item_data.get('valorDoICMSAntecipado'), # itemValorDoICMSAntecipado
item_data.get('aliquotaNoSimples'), # itemAliquotaNoSimples
item_data.get('cstDoIPI'), # itemCstDoIPI
item_data.get('baseDeCalculoDoIPI'), # itemBaseDeCalculoDoIPI
item_data.get('aliquotaDoIPI'), # itemAliquotaDoIPI
item_data.get('tipoDeEntradaIPI'), # itemTipoDeEntradaIPI
item_data.get('valorDoIPI'), # itemValorDoIPI
item_data.get('percentualDoIPI'), # itemPercentualDoIPI
item_data.get('baseDeCalculoDoPIS'), # itemBaseDeCalculoDoPIS
item_data.get('aliquotaDoPIS'), # itemAliquotaDoPIS
item_data.get('valorDoPIS'), # itemValorDoPIS
item_data.get('baseDeCalculoDoCOFINS'), # itemBaseDeCalculoDoCOFINS
item_data.get('aliquotaDoCOFINS'), # itemAliquotaDoCOFINS
item_data.get('valorDoCOFINS'), # itemValorDoCOFINS
item_data.get('codigoNaturezaDeImpostoFederal'), # itemCodigoNaturezaDeImpostoFederal
item_data.get('baseDeCalculoDoFecop'), # itemBaseDeCalculoDoFecop
item_data.get('aliquotaDoFecop'), # itemAliquotaDoFecop
item_data.get('valorDoFecop'), # itemValorDoFecop
item_data.get('baseDeCalculoDoFecopSubstituto'), # itemBaseDeCalculoDoFecopSubstituto
item_data.get('aliquotaDoFecopSubstituto'), # itemAliquotaDoFecopSubstituto
item_data.get('valorDoFecopSubstituto'), # itemValorDoFecopSubstituto
item_data.get('valorDoICMSDesonerado'), # itemValorDoICMSDesonerado
item_data.get('motivoDesoneracao'), # itemMotivoDesoneracao
item_data.get('codigoBeneficioFiscal'), # itemCodigoBeneficioFiscal
item_data.get('percentualDiferimento'), # itemPercentualDiferimento
item_data.get('valorICMSDiferimento') # itemValorICMSDiferimento
]
else:
# Se não há item, preencher com None
item_values = [None] * 82 # 82 campos de item
# Combinar valores da invoice e do item
all_values = invoice_values + item_values
try:
cursor.execute(insert_query, all_values)
logger.info(f"Invoice/Item inserido: Invoice ID {invoice_data.get('id')}")
return True
except pyodbc.Error as e:
logger.error(f"Erro ao inserir invoice ID {invoice_data.get('id')}: {e}")
return False
def prepare_invoice_values(self, invoice_data, item_data=None):
"""
Prepara os valores para inserção sem executar o INSERT
Retorna a tupla de valores pronta para executemany
"""
# Preparar os dados da invoice (campos principais)
invoice_values = [
invoice_data.get('id'), # invoiceId
invoice_data.get('storeId'),
invoice_data.get('stockLocationId'),
invoice_data.get('fiscalOperationId'),
invoice_data.get('fiscalOperationDescription'),
invoice_data.get('supplierId'),
invoice_data.get('supplierName'),
invoice_data.get('clientId'),
invoice_data.get('clientName'),
self.parse_datetime(invoice_data.get('emissionDate')),
self.parse_datetime(invoice_data.get('operationDate')),
self.parse_datetime(invoice_data.get('dateOfPassageOnFiscalPost')),
self.parse_datetime(invoice_data.get('deletedAt')),
self.parse_datetime(invoice_data.get('updatedAt')),
invoice_data.get('invoiceNumber'),
invoice_data.get('serie'),
invoice_data.get('key'),
invoice_data.get('buyerEmployeeId'),
invoice_data.get('emitterEmployeeId'),
invoice_data.get('cfop'),
invoice_data.get('operationType'),
invoice_data.get('shippingType'),
invoice_data.get('paymentCondition'),
invoice_data.get('fiscalDocumentType'),
invoice_data.get('modality'),
invoice_data.get('updateCost'),
invoice_data.get('updateStock'),
invoice_data.get('composeABC'),
invoice_data.get('situation'),
invoice_data.get('observation'),
invoice_data.get('importType'),
invoice_data.get('classification'),
invoice_data.get('financeGenerationType'),
invoice_data.get('shippingValue'),
invoice_data.get('otherExpensesValue'),
invoice_data.get('insuranceValue'),
invoice_data.get('discountValue'),
invoice_data.get('totalItemsValue'),
invoice_data.get('documentValue'),
invoice_data.get('valorDoDAE'),
invoice_data.get('baseDeCalculoDoICMS'),
invoice_data.get('valorDoICMS'),
invoice_data.get('baseDeCalculoDoICMSSubstituicaoTributaria'),
invoice_data.get('valorDoICMSSubstituicaoTributaria'),
invoice_data.get('valorDoIPI'),
invoice_data.get('valorDoPIS'),
invoice_data.get('valorDoCOFINS'),
invoice_data.get('valorDoICMSDesonerado'),
invoice_data.get('baseDeCalculoFecop'),
invoice_data.get('valorFecop'),
invoice_data.get('baseDeCalculoFecopSubstituicaoTributaria'),
invoice_data.get('valorFecopSubstituicaoTributaria')
]
# Preparar os dados do item (se existir)
if item_data:
item_values = [
item_data.get('id'),
item_data.get('productId'),
item_data.get('sequential'),
item_data.get('orderNumber'),
item_data.get('orderItemSequencial'),
item_data.get('composeTotal'),
item_data.get('cfop'),
item_data.get('unitType'),
item_data.get('quantityOfItensOnUnit'),
item_data.get('quantity'),
item_data.get('completeQuantity'),
item_data.get('unitValue'),
item_data.get('discountInputType'),
item_data.get('untaxedDiscountValue'),
item_data.get('taxedDiscountValue'),
item_data.get('discountPercentage'),
item_data.get('shippingInputType'),
item_data.get('shippingValue'),
item_data.get('shippingPercentage'),
item_data.get('insuranceInputType'),
item_data.get('insuranceValue'),
item_data.get('insurancePercentage'),
item_data.get('otherExpensesInputValue'),
item_data.get('otherExpensesValue'),
item_data.get('otherExpensesPercentage'),
item_data.get('totalValue'),
item_data.get('taxedPercentage'),
item_data.get('productCost'),
item_data.get('fiscalCost'),
item_data.get('averageCost'),
item_data.get('tipoDeEntradaDAE'),
item_data.get('valorDoDAE'),
item_data.get('percentualDoDAE'),
item_data.get('fiscalSituationId'),
item_data.get('csosn'),
item_data.get('outrasDespesasCompoeBaseDeCalculoIcms'),
item_data.get('tributacao'),
item_data.get('ncm'),
item_data.get('cest'),
item_data.get('aliquotaNacional'),
item_data.get('aliquotaImportado'),
item_data.get('aliquotaEstadual'),
item_data.get('aliquotaMunicipal'),
item_data.get('modalidadeDaBaseDeCalculo'),
item_data.get('percentualICMSDeCompra'),
item_data.get('valorDoICMS'),
item_data.get('valorDoICMSNoSimples'),
item_data.get('baseDeCalculoDoICMS'),
item_data.get('baseDeCalculoDoICMSComSubstituicaoTributaria'),
item_data.get('aliquotaDoICMSComSubstituicaoTributaria'),
item_data.get('valorDoICMSComSubstituicaoTributaria'),
item_data.get('percentualDeAgregacao'),
item_data.get('percentualDeReducaoDASubstituicaoTributaria'),
item_data.get('aliquotaDoICMS'),
item_data.get('aliquotaDoICMSDeVenda'),
item_data.get('aliquotaDoICMSAntecipado'),
item_data.get('valorDoICMSAntecipado'),
item_data.get('aliquotaNoSimples'),
item_data.get('cstDoIPI'),
item_data.get('baseDeCalculoDoIPI'),
item_data.get('aliquotaDoIPI'),
item_data.get('tipoDeEntradaIPI'),
item_data.get('valorDoIPI'),
item_data.get('percentualDoIPI'),
item_data.get('baseDeCalculoDoPIS'),
item_data.get('aliquotaDoPIS'),
item_data.get('valorDoPIS'),
item_data.get('baseDeCalculoDoCOFINS'),
item_data.get('aliquotaDoCOFINS'),
item_data.get('valorDoCOFINS'),
item_data.get('codigoNaturezaDeImpostoFederal'),
item_data.get('baseDeCalculoDoFecop'),
item_data.get('aliquotaDoFecop'),
item_data.get('valorDoFecop'),
item_data.get('baseDeCalculoDoFecopSubstituto'),
item_data.get('aliquotaDoFecopSubstituto'),
item_data.get('valorDoFecopSubstituto'),
item_data.get('valorDoICMSDesonerado'),
item_data.get('motivoDesoneracao'),
item_data.get('codigoBeneficioFiscal'),
item_data.get('percentualDiferimento'),
item_data.get('valorICMSDiferimento')
]
else:
item_values = [None] * 82
return tuple(invoice_values + item_values)
def delete_existing_invoices(self, cursor, invoice_keys):
"""
Deleta invoices existentes no banco de dados baseado nas keys
Processa em lotes de 1000 keys para evitar limite de 2100 parâmetros do SQL Server
"""
if not invoice_keys:
return 0
try:
total_deleted = 0
batch_size = 1000 # Limite seguro abaixo dos 2100 parâmetros
# Processar em lotes
for i in range(0, len(invoice_keys), batch_size):
batch = invoice_keys[i:i + batch_size]
# Criar lista de placeholders para a query
placeholders = ','.join(['?' for _ in batch])
delete_query = f"DELETE FROM [GINSENG].[dbo].[rgb_fiscal_invoices] WHERE [key] IN ({placeholders})"
cursor.execute(delete_query, batch)
deleted_count = cursor.rowcount
total_deleted += deleted_count
logger.info(f"Lote {i//batch_size + 1}: Deletados {deleted_count} registros de {len(batch)} keys")
logger.info(f"Total deletado: {total_deleted} registros antigos de {len(invoice_keys)} keys")
return total_deleted
except pyodbc.Error as e:
logger.error(f"Erro ao deletar invoices existentes: {e}")
raise
def process_invoices_to_database(self, invoices_data, batch_size=500):
"""
Processa as invoices e insere no banco de dados EM LOTES
Usa executemany para inserção rápida em lotes de 500 registros
ANTES de inserir, deleta registros antigos com mesmo invoiceId
"""
connection = None
cursor = None
try:
# Conectar ao banco de dados
connection = self.connect_database()
cursor = connection.cursor()
# Extrair lista de invoices
if isinstance(invoices_data, dict):
if 'items' in invoices_data:
invoices = invoices_data['items']
else:
invoices = [invoices_data]
elif isinstance(invoices_data, list):
invoices = invoices_data
else:
invoices = [invoices_data]
total_invoices = len(invoices)
logger.info(f"Processando {total_invoices} invoices para inserção no banco de dados...")
logger.info(f"Usando inserção em lotes de {batch_size} registros")
# PASSO 1: Coletar todas as keys para verificar se já existem
invoice_keys = [invoice.get('key') for invoice in invoices if invoice.get('key')]
logger.info(f"Verificando {len(invoice_keys)} keys no banco de dados...")
# PASSO 2: Deletar invoices existentes
deleted_count = self.delete_existing_invoices(cursor, invoice_keys)
connection.commit()
if deleted_count > 0:
logger.info(f"Invoices antigas deletadas com sucesso. Prosseguindo com inserção dos novos dados...")
# Query de inserção (será usada com executemany)
insert_query = """
INSERT INTO [GINSENG].[dbo].[rgb_fiscal_invoices]
([invoiceId], [storeId], [stockLocationId], [fiscalOperationId], [fiscalOperationDescription],
[supplierId], [supplierName], [clientId], [clientName], [emissionDate], [operationDate],
[dateOfPassageOnFiscalPost], [deletedAt], [updatedAt], [invoiceNumber], [serie], [key],
[buyerEmployeeId], [emitterEmployeeId], [cfop], [operationType], [shippingType],
[paymentCondition], [fiscalDocumentType], [modality], [updateCost], [updateStock],
[composeABC], [situation], [observation], [importType], [classification],
[financeGenerationType], [shippingValue], [otherExpensesValue], [insuranceValue],
[discountValue], [totalItemsValue], [documentValue], [valorDoDAE], [baseDeCalculoDoICMS],
[valorDoICMS], [baseDeCalculoDoICMSSubstituicaoTributaria], [valorDoICMSSubstituicaoTributaria],
[valorDoIPI], [valorDoPIS], [valorDoCOFINS], [valorDoICMSDesonerado], [baseDeCalculoFecop],
[valorFecop], [baseDeCalculoFecopSubstituicaoTributaria], [valorFecopSubstituicaoTributaria],
[itemId], [itemProductId], [itemSequential], [itemOrderNumber], [itemOrderItemSequencial],
[itemComposeTotal], [itemCfop], [itemUnitType], [itemQuantityOfItensOnUnit], [itemQuantity],
[itemCompleteQuantity], [itemUnitValue], [itemDiscountInputType], [itemUntaxedDiscountValue],
[itemTaxedDiscountValue], [itemDiscountPercentage], [itemShippingInputType], [itemShippingValue],
[itemShippingPercentage], [itemInsuranceInputType], [itemInsuranceValue], [itemInsurancePercentage],
[itemOtherExpensesInputValue], [itemOtherExpensesValue], [itemOtherExpensesPercentage],
[itemTotalValue], [itemTaxedPercentage], [itemProductCost], [itemFiscalCost], [itemAverageCost],
[itemTipoDeEntradaDAE], [itemValorDoDAE], [itemPercentualDoDAE], [itemFiscalSituationId],
[itemCsosn], [itemOutrasDespesasCompoeBaseDeCalculoIcms], [itemTributacao], [itemNcm],
[itemCest], [itemAliquotaNacional], [itemAliquotaImportado], [itemAliquotaEstadual],
[itemAliquotaMunicipal], [itemModalidadeDaBaseDeCalculo], [itemPercentualICMSDeCompra],
[itemValorDoICMS], [itemValorDoICMSNoSimples], [itemBaseDeCalculoDoICMS],
[itemBaseDeCalculoDoICMSComSubstituicaoTributaria], [itemAliquotaDoICMSComSubstituicaoTributaria],
[itemValorDoICMSComSubstituicaoTributaria], [itemPercentualDeAgregacao],
[itemPercentualDeReducaoDASubstituicaoTributaria], [itemAliquotaDoICMS], [itemAliquotaDoICMSDeVenda],
[itemAliquotaDoICMSAntecipado], [itemValorDoICMSAntecipado], [itemAliquotaNoSimples],
[itemCstDoIPI], [itemBaseDeCalculoDoIPI], [itemAliquotaDoIPI], [itemTipoDeEntradaIPI],
[itemValorDoIPI], [itemPercentualDoIPI], [itemBaseDeCalculoDoPIS], [itemAliquotaDoPIS],
[itemValorDoPIS], [itemBaseDeCalculoDoCOFINS], [itemAliquotaDoCOFINS], [itemValorDoCOFINS],
[itemCodigoNaturezaDeImpostoFederal], [itemBaseDeCalculoDoFecop], [itemAliquotaDoFecop],
[itemValorDoFecop], [itemBaseDeCalculoDoFecopSubstituto], [itemAliquotaDoFecopSubstituto],
[itemValorDoFecopSubstituto], [itemValorDoICMSDesonerado], [itemMotivoDesoneracao],
[itemCodigoBeneficioFiscal], [itemPercentualDiferimento], [itemValorICMSDiferimento])
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
# Coletar todos os registros para inserir
batch_values = []
total_items_count = 0
for i, invoice in enumerate(invoices, 1):
invoice_id = invoice.get('id')
store_id = invoice.get('storeId')
items = invoice.get('items', [])
if items:
for item in items:
batch_values.append(self.prepare_invoice_values(invoice, item))
total_items_count += 1
else:
batch_values.append(self.prepare_invoice_values(invoice, None))
total_items_count += 1
# Inserir em lotes
if len(batch_values) >= batch_size:
logger.info(f"Inserindo lote de {len(batch_values)} registros... (Invoice {i}/{total_invoices})")
cursor.executemany(insert_query, batch_values)
connection.commit()
logger.info(f"Lote inserido com sucesso!")
batch_values = []
# Inserir registros restantes
if batch_values:
logger.info(f"Inserindo lote final de {len(batch_values)} registros...")
cursor.executemany(insert_query, batch_values)
connection.commit()
logger.info(f"Lote final inserido com sucesso!")
# Relatório final
logger.info("=" * 60)
logger.info("RELATÓRIO DE INSERÇÃO NO BANCO DE DADOS")
logger.info("=" * 60)
logger.info(f"Total de invoices processadas: {total_invoices}")
logger.info(f"Registros antigos deletados: {deleted_count}")
logger.info(f"Total de registros inseridos: {total_items_count}")
logger.info("=" * 60)
return total_items_count, 0, deleted_count
except Exception as e:
logger.error(f"Erro geral no processamento: {e}")
if connection:
connection.rollback()
raise
finally:
if cursor:
cursor.close()
if connection:
connection.close()
logger.info("Conexão com banco de dados fechada")
def main():
"""
Função principal para executar o extrator de invoices fiscais
"""
extractor = RGBFiscalInvoicesExtractor()
print("=== EXTRATOR DE INVOICES FISCAIS RGB - GRUPO BOTICÁRIO ===")
print("Iniciando busca de invoices fiscais...")
# Determinar datas a processar
if USE_YESTERDAY:
# Usar data do dia anterior
yesterday = datetime.now() - timedelta(days=1)
search_dates = [yesterday.strftime("%Y-%m-%d")]
print(f"Modo: Busca automática do dia anterior")
print(f"Data a processar: {search_dates[0]}\n")
else:
# Gerar lista de datas entre START_DATE e END_DATE
start = datetime.strptime(START_DATE, "%Y-%m-%d")
end = datetime.strptime(END_DATE, "%Y-%m-%d")
search_dates = []
current = start
while current <= end:
search_dates.append(current.strftime("%Y-%m-%d"))
current += timedelta(days=1)
print(f"Modo: Intervalo de datas configurado")
print(f"Intervalo de datas: {START_DATE} até {END_DATE}")
print(f"Total de datas a processar: {len(search_dates)}\n")
# Contadores totais
total_invoices_all_dates = 0
total_inserted_all_dates = 0
total_errors_all_dates = 0
total_deleted_all_dates = 0
try:
# Processar cada data
for idx, search_date in enumerate(search_dates, 1):
print("\n" + "="*80)
print(f"PROCESSANDO DATA {idx}/{len(search_dates)}: {search_date}")
print("="*80)
# Buscar dados da API para esta data
data = extractor.get_fiscal_invoices_data(updated_at_date=search_date)
# Mostrar resumo dos dados recebidos
if isinstance(data, dict) and 'items' in data:
invoices = data['items']
elif isinstance(data, list):
invoices = data
else:
invoices = [data] if data else []
print(f"Dados recebidos da API: {len(invoices)} invoices encontradas para {search_date}")
total_invoices_all_dates += len(invoices)
# Contar total de itens
total_items = 0
for invoice in invoices:
items = invoice.get('items', [])
total_items += len(items) if items else 1 # Se não tem itens, conta como 1 registro
print(f"Total de registros que serão processados: {total_items}")
# Salvar no banco de dados
if len(invoices) > 0:
print("\nIniciando inserção no banco de dados...")
inserted, errors, deleted = extractor.process_invoices_to_database(data)
total_inserted_all_dates += inserted
total_errors_all_dates += errors
total_deleted_all_dates += deleted
print(f"\nResumo da inserção para {search_date}:")
print(f"- Registros antigos deletados: {deleted}")
print(f"- Registros inseridos: {inserted}")
print(f"- Erros: {errors}")
else:
print(f"Nenhuma invoice encontrada para {search_date} - pulando inserção")
# Relatório final consolidado
print("\n" + "="*80)
print("RELATÓRIO FINAL CONSOLIDADO")
print("="*80)
print(f"Total de datas processadas: {len(search_dates)}")
print(f"Total de invoices encontradas: {total_invoices_all_dates}")
print(f"Total de registros antigos deletados: {total_deleted_all_dates}")
print(f"Total de registros inseridos: {total_inserted_all_dates}")
print(f"Total de erros: {total_errors_all_dates}")
print("="*80)
print("\nProcessamento concluído com sucesso!")
except Exception as e:
print(f"\nErro durante o processamento: {e}")
logger.error(f"Erro fatal: {e}")
return 1
return 0
if __name__ == "__main__":
main()

336
rgb_products.py Normal file
View File

@ -0,0 +1,336 @@
import requests
import pyodbc
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
import time
from datetime import datetime, timedelta
# ==============================
# Conexão com o banco de dados
# ==============================
DB_CONNECTION_STRING = (
'DRIVER={ODBC Driver 18 for SQL Server};'
'SERVER=10.77.77.10;'
'DATABASE=GINSENG;'
'UID=supginseng;'
'PWD=Ginseng@;'
'PORT=1433;'
'TrustServerCertificate=yes'
)
# Configurações de paralelismo
MAX_WORKERS = 5 # Número de requisições paralelas
PAGE_SIZE = 50 # Itens por página
MAX_RETRIES = 5 # Máximo de tentativas por página (dentro do paralelo)
RETRY_DELAY = 5 # Segundos entre tentativas
FINAL_RETRY_DELAY = 30 # Segundos entre rodadas de retry final
# Lock para print thread-safe
print_lock = threading.Lock()
# Estatísticas globais
stats = {
"pages_downloaded": 0,
"pages_failed": 0,
"items_downloaded": 0,
"items_inserted": 0,
"errors": 0
}
stats_lock = threading.Lock()
# ==============================
# 1) Buscar token da API
# ==============================
def get_token():
url = "https://api.grupoginseng.com.br/api/rgb_token"
response = requests.get(url)
if response.status_code != 200:
raise Exception(f"Erro ao buscar token: {response.status_code} {response.text}")
data = response.json()
token = data["data"][0]["token"]
return token
# ==============================
# 2) Consultar produtos
# ==============================
def insert_items_to_db(cursor, items):
"""Insere uma lista de itens no banco de dados, deletando SKUs existentes antes"""
# Query para deletar SKU existente
delete_query = "DELETE FROM [dbo].[rgb_product] WHERE [sku] = ?"
insert_query = """
INSERT INTO [dbo].[rgb_product] (
[sku], [marketId], [tacticId], [strategicId], [brand], [internalCode],
[description], [discontinued], [purpose], [discountAllowed], [maxDiscount],
[createdAt], [discontinuedDate], [updatedAt], [ncmId], [cest],
[grossWeight], [netWeight], [purchaseBlocked]
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
inserted = 0
deleted = 0
for item in items:
try:
sku = item.get("id")
# 1. Deletar SKU se já existir
cursor.execute(delete_query, sku)
if cursor.rowcount > 0:
deleted += cursor.rowcount
# 2. Inserir o novo registro
cursor.execute(insert_query,
sku,
item.get("marketId"),
item.get("tacticId"),
item.get("strategicId"),
item.get("brand"),
item.get("internalCode"),
item.get("description"),
item.get("discontinued"),
item.get("purpose"),
item.get("discountAllowed"),
item.get("maxDiscount"),
item.get("createdAt"),
item.get("discontinuedDate"),
item.get("updatedAt"),
item.get("ncmId"),
item.get("cest"),
item.get("grossWeight"),
item.get("netWeight"),
item.get("purchaseBlocked")
)
inserted += 1
except Exception as e:
with stats_lock:
stats["errors"] += 1
if stats["errors"] <= 5:
print(f" ✗ Erro ao inserir SKU {item.get('id')}: {e}")
# Atualizar estatística de deletados
with stats_lock:
stats["skus_updated"] = stats.get("skus_updated", 0) + deleted
return inserted
def fetch_and_insert_page(token, start, total_pages, cursor, db_lock):
"""Busca uma página específica com retry e insere no banco imediatamente"""
url = "https://api.grupoboticario.com.br/global/v1/franchising/gb-stores-data/product/products"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# Data de ontem para filtrar produtos atualizados
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
params = {
"count": PAGE_SIZE,
"start": start,
"product.updatedAt": yesterday
}
page_num = (start // PAGE_SIZE) + 1
for attempt in range(1, MAX_RETRIES + 1):
try:
response = requests.get(url, headers=headers, params=params, timeout=60)
if response.status_code == 200:
data = response.json()
items = data.get("items", [])
# Inserir no banco imediatamente (com lock para thread-safety)
with db_lock:
inserted = insert_items_to_db(cursor, items)
cursor.connection.commit()
with stats_lock:
stats["pages_downloaded"] += 1
stats["items_downloaded"] += len(items)
stats["items_inserted"] += inserted
with print_lock:
print(f" ✓ Página {page_num}/{total_pages}: {len(items)} baixados, {inserted} inseridos (Total: {stats['items_inserted']})")
return {"start": start, "success": True}
else:
with print_lock:
print(f" ✗ Página {page_num} (start={start}): Erro {response.status_code} - Tentativa {attempt}/{MAX_RETRIES}")
except Exception as e:
with print_lock:
print(f" ✗ Página {page_num} (start={start}): {str(e)[:50]} - Tentativa {attempt}/{MAX_RETRIES}")
if attempt < MAX_RETRIES:
time.sleep(RETRY_DELAY)
with stats_lock:
stats["pages_failed"] += 1
return {"start": start, "success": False}
def get_total_products(token, date_filter):
"""Faz uma requisição inicial para descobrir o total de produtos"""
url = "https://api.grupoboticario.com.br/global/v1/franchising/gb-stores-data/product/products"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
params = {
"count": 1,
"start": 0,
"product.updatedAt": date_filter
}
response = requests.get(url, headers=headers, params=params, timeout=60)
if response.status_code == 200:
data = response.json()
return data.get("total", 0)
return None
def fetch_and_insert_all_products(token):
"""Consulta todos os produtos com paginação paralela e insere no banco imediatamente"""
# Calcular data de ontem (dia anterior)
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
print(f"Consultando produtos e inserindo no banco...")
print(f" Filtro de data: {yesterday} (produtos atualizados ontem)")
# Reset stats
global stats
stats = {
"pages_downloaded": 0,
"pages_failed": 0,
"items_downloaded": 0,
"items_inserted": 0,
"errors": 0,
"skus_updated": 0 # SKUs que já existiam e foram atualizados
}
# 1. Descobrir o total de produtos
print(" Descobrindo total de produtos...")
total = get_total_products(token, yesterday)
if total is None:
print(" ✗ Erro ao descobrir total de produtos")
return None
print(f" Total de produtos na API: {total}")
if total == 0:
print(f" ⚠ Nenhum produto atualizado em {yesterday}")
return stats
# 2. Calcular páginas necessárias
total_pages = (total + PAGE_SIZE - 1) // PAGE_SIZE
starts = [i * PAGE_SIZE for i in range(total_pages)]
print(f" Total de páginas: {total_pages}")
print(f" Requisições paralelas: {MAX_WORKERS}")
# 3. Preparar banco de dados
print(" Preparando banco de dados...")
conn = pyodbc.connect(DB_CONNECTION_STRING)
cursor = conn.cursor()
print(f" Estratégia: Deletar SKU existente antes de inserir (upsert)")
print(f" Iniciando download e inserção...\n")
# Lock para acesso ao banco (uma conexão compartilhada)
db_lock = threading.Lock()
# 4. Buscar páginas em paralelo e inserir imediatamente
failed_pages = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = {executor.submit(fetch_and_insert_page, token, start, total_pages, cursor, db_lock): start for start in starts}
for future in as_completed(futures):
result = future.result()
if not result["success"]:
failed_pages.append(result["start"])
# 5. Verificar páginas que falharam - continua tentando até todas serem bem-sucedidas
retry_round = 0
while failed_pages:
retry_round += 1
print(f"\n ⚠ Rodada de retry #{retry_round}: {len(failed_pages)} páginas falharam")
print(f" Páginas: {failed_pages[:10]}{'...' if len(failed_pages) > 10 else ''}")
print(f" Aguardando {FINAL_RETRY_DELAY} segundos antes de tentar novamente...")
time.sleep(FINAL_RETRY_DELAY)
still_failed = []
for start in failed_pages:
result = fetch_and_insert_page(token, start, total_pages, cursor, db_lock)
if not result["success"]:
still_failed.append(start)
failed_pages = still_failed
if failed_pages:
print(f" Ainda restam {len(failed_pages)} páginas com falha. Tentando novamente...")
# Fechar conexão
cursor.close()
conn.close()
print(f"\n Download e inserção concluídos!")
return stats
# ==============================
# EXECUTAR
# ==============================
if __name__ == "__main__":
print("="*60)
print("RGB PRODUCTS - Consulta de Produtos")
print("="*60)
# 1. Buscar token
print("\n[1/2] Buscando token...")
token = get_token()
print(f"✓ Token obtido com sucesso!")
print(f" Token: {token[:50]}...")
# 2. Consultar produtos e inserir no banco (paralelo)
print("\n[2/2] Baixando e inserindo produtos...")
result = fetch_and_insert_all_products(token)
if result:
print(f"\n{'='*60}")
print("RESUMO FINAL")
print(f"{'='*60}")
print(f" Páginas baixadas: {result['pages_downloaded']}")
print(f" Páginas com falha: {result['pages_failed']}")
print(f" Itens baixados: {result['items_downloaded']}")
print(f" Itens inseridos: {result['items_inserted']}")
print(f" SKUs atualizados: {result.get('skus_updated', 0)} (já existiam)")
print(f" Erros de inserção: {result['errors']}")
print(f"{'='*60}")
if result['pages_failed'] == 0 and result['errors'] == 0:
print("✓ SUCESSO TOTAL!")
else:
print("⚠ Concluído com alguns problemas")
else:
print("\n✗ Não foi possível obter os produtos.")

1427
rgb_sale_receipts.py Normal file

File diff suppressed because it is too large Load Diff

134
rgb_token_client.py Normal file
View File

@ -0,0 +1,134 @@
import requests
import json
import pyodbc
from datetime import datetime, timedelta
class RGBTokenClient:
"""Cliente para obter token JWT da API do Grupo Boticário (execução única)"""
def __init__(self):
self.base_url = "https://api.grupoboticario.com.br/global/v2/jwt-token/token"
self.client_id = "88ymKwAUNfu06sD85i0RiokCxWGSkFBkx9ytgI5y1ZKxX3OQ"
self.client_secret = "YDFz43qAzL6ApNIKVCxu3dAmS9GWOqJbcc2aPnFDkmEaBXexSpsHGfcItg56i2dE"
# Configurações do banco de dados
self.driver = self._get_available_sql_server_driver()
self.connection_string = (
f'DRIVER={self.driver};'
'SERVER=10.77.77.10;'
'DATABASE=GINSENG;'
'UID=supginseng;'
'PWD=Ginseng@;'
'PORT=1433;'
'TrustServerCertificate=yes;'
'Encrypt=yes'
)
def _get_available_sql_server_driver(self) -> str:
"""Detecta automaticamente o driver SQL Server disponível"""
drivers_to_try = [
'{ODBC Driver 18 for SQL Server}',
'{ODBC Driver 17 for SQL Server}',
'{ODBC Driver 13 for SQL Server}',
'{ODBC Driver 11 for SQL Server}',
'{SQL Server Native Client 11.0}',
'{SQL Server Native Client 10.0}',
'{SQL Server}'
]
available_drivers = pyodbc.drivers()
print("Drivers ODBC disponíveis:")
for d in available_drivers:
print(f" - {d}")
for preferred in drivers_to_try:
if preferred.strip("{}") in available_drivers:
print(f"✅ Usando driver: {preferred}")
return preferred
if available_drivers:
fallback = f"{{{available_drivers[0]}}}"
print(f"⚠️ Nenhum driver padrão encontrado. Usando: {fallback}")
return fallback
raise Exception("Nenhum driver ODBC encontrado no sistema.")
def get_token(self):
"""Obtém o token JWT da API"""
try:
print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Solicitando novo token...")
response = requests.post(
self.base_url,
params={"grant_type": "client_credentials"},
data={
"client_id": self.client_id,
"client_secret": self.client_secret
},
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json"
},
timeout=30
)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
data = response.json()
token = data.get("access_token")
expires_in = data.get("expires_in", 0)
expira = datetime.now() + timedelta(seconds=expires_in)
print(f"✅ Token obtido com sucesso! Expira em: {expira}")
return token
else:
print(f"❌ Erro na requisição: {response.status_code}")
print(f"Resposta: {response.text}")
return None
except Exception as e:
print(f"❌ Erro ao obter token: {e}")
return None
def save_token(self, token: str) -> bool:
"""Atualiza o token no banco de dados"""
try:
print("Conectando ao banco de dados...")
conn = pyodbc.connect(self.connection_string)
cursor = conn.cursor()
query = "UPDATE dbo.rgb_token SET token = ?, updatedAt = GETDATE() WHERE id = 1"
cursor.execute(query, token)
conn.commit()
rows = cursor.rowcount
cursor.close()
conn.close()
if rows > 0:
print(f"✅ Token atualizado com sucesso no banco ({rows} registro(s)).")
return True
else:
print("⚠️ Nenhum registro atualizado (verifique o ID = 1).")
return False
except Exception as e:
print(f"❌ Erro ao salvar token no banco: {e}")
return False
def main():
client = RGBTokenClient()
token = client.get_token()
if token:
client.save_token(token)
print("🎯 Execução finalizada com sucesso.")
else:
print("❌ Falha ao obter ou salvar o token.")
if __name__ == "__main__":
main()

602
ruptura.py Normal file
View File

@ -0,0 +1,602 @@
import requests
import json
from datetime import datetime, timedelta
import time
import pyodbc
import pandas as pd
import os
from io import BytesIO
# ==============================
# 1) Buscar token dinâmico
# ==============================
def get_token():
url = "https://api.grupoginseng.com.br/api/tokens"
response = requests.get(url)
if response.status_code != 200:
raise Exception(f"Erro ao buscar token: {response.status_code} {response.text}")
data = response.json()
# Extrair o token dentro de data[0]["token"]
token = data["data"][0]["token"]
return token
# ==============================
# 2) Buscar cycles
# ==============================
def get_cycles(token):
url = "https://api-extranet.grupoboticario.digital/api/v2/cycles"
headers = {
"accept": "application/json, text/plain, */*",
"authorization": token,
"user-agent": "Mozilla/5.0"
}
response = requests.get(url, headers=headers)
return response.json()
# ==============================
# 3) Determinar ciclo atual formatado para cada marca
# ==============================
def ciclo_formatado_por_marca(cycles_json):
hoje = datetime.now()
ano = hoje.year
ignorar = [ "VD Multimarca"] # Marcas excluídas
MAPA_NOMES = {
"O Boticário": "BOT",
"O.U.I": "OUI",
"Quem Disse Berenice": "QDB",
"Eudora": "EUD"
}
resultado = {}
for brand in cycles_json["data"]:
nome_marca = brand["brandName"]
# Pula marcas indesejadas
if nome_marca in ignorar:
continue
ciclo_formatado = None
for cycle in brand["cycles"]:
start = datetime.fromisoformat(cycle["startDate"])
end = datetime.fromisoformat(cycle["endDate"])
if cycle["isCurrent"] or (start <= hoje <= end):
ciclo_num = f"{cycle['number']:02d}"
ciclo_formatado = int(f"{ano}{ciclo_num}")
break
nome_final = MAPA_NOMES.get(nome_marca, nome_marca) # usa abreviação
resultado[nome_final] = ciclo_formatado
return resultado
# ==============================
# 4) Fazer a requisição do EXPORT
# ==============================
def export_with_token(token, business_unit, cycle):
"""
Faz a requisição de exportação para uma marca específica.
Args:
token: Token de autenticação
business_unit: Unidade de negócio (BOT, OUI, QDB)
cycle: Ciclo formatado (ex: 202516)
Returns:
ID da requisição se sucesso, None caso contrário
"""
url = "https://mar-api-gateway-front.demanda-abastecimento.grupoboticario.digital/orders-bff/api/export/RUPTURE_INDICATOR_PAGE"
headers = {
"accept": "*/*",
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
"authorization": token,
"content-type": "application/json",
"origin": "https://extranet.grupoboticario.com.br",
"priority": "u=1, i",
"referer": "https://extranet.grupoboticario.com.br/",
"sec-ch-ua": "\"Chromium\";v=\"142\", \"Google Chrome\";v=\"142\", \"Not_A Brand\";v=\"99\"",
"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/142.0.0.0 Safari/537.36",
"x-correlation-id": "bd6f2e2d-c7ef-48d0-a0f5-e40de830442b",
"x-user-id": "163165",
"x-username": "daniel.rodrigue"
}
payload = {
"storeCodes": [],
"cpId": 10269,
"fileType": "XLSX",
"userId": "163165",
"metadata": {
"storeCodes": [],
"userName": "Daniel Jose Medeiros Rodrigues",
"fileFormattedName": f"Indicadore de Ruptura {business_unit} {datetime.now().strftime('%d-%m-%Y')}.XLSX",
"create_at": datetime.now().isoformat() + "Z",
"exportType": "RUPTURE_INDICATOR_PAGE"
},
"filters": {
"cycles": [cycle],
"businessUnit": business_unit
},
"validateOnRequest": True
}
print(f"\n{'='*50}")
print(f"Exportando: {business_unit} - Ciclo {cycle}")
print(f"{'='*50}")
response = requests.post(url, headers=headers, json=payload)
print("Status:", response.status_code)
print("Resposta:")
print(response.text)
# Retornar o ID da requisição se a exportação foi criada
if response.status_code == 201:
response_data = response.json()
return response_data.get("id")
return None
# ==============================
# 5) Verificar status da exportação
# ==============================
def check_export_status(token, request_id, max_attempts=300, interval=5):
"""
Verifica o status da exportação periodicamente até que seja concluída.
Args:
token: Token de autenticação
request_id: ID da requisição de exportação
max_attempts: Número máximo de tentativas (padrão: 60 = 5 minutos)
interval: Intervalo entre verificações em segundos (padrão: 5)
Returns:
True se a exportação foi concluída com sucesso, False caso contrário
"""
url = "https://mar-api-gateway-front.demanda-abastecimento.grupoboticario.digital/orders-bff/api/export/RUPTURE_INDICATOR_PAGE/sidebar?userId=163165&viewed=true"
headers = {
"accept": "*/*",
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
"authorization": token,
"content-type": "application/json",
"origin": "https://extranet.grupoboticario.com.br",
"priority": "u=1, i",
"referer": "https://extranet.grupoboticario.com.br/",
"sec-ch-ua": "\"Chromium\";v=\"142\", \"Google Chrome\";v=\"142\", \"Not_A Brand\";v=\"99\"",
"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/142.0.0.0 Safari/537.36",
"x-correlation-id": "788106b9-3985-44a5-aba6-1e0f2d1eff6c",
"x-user-id": "163165",
"x-username": "daniel.rodrigue"
}
print("\n" + "="*50)
print("Verificando status da exportação...")
print("="*50)
attempt = 0
while attempt < max_attempts:
attempt += 1
response = requests.get(url, headers=headers)
print(f"\n[Tentativa {attempt}/{max_attempts}] Status da verificação: {response.status_code}")
if response.status_code == 200:
data = response.json()
items = data.get("items", [])
# Procurar pelo requestId na lista
for item in items:
if item.get("requestId") == request_id:
status = item.get('status')
print(f"✓ Exportação encontrada!")
print(f" Request ID: {item.get('requestId')}")
print(f" Status: {status}")
print(f" Nome do arquivo: {item.get('fileName', 'N/A')}")
print(f" Mensagem: {item.get('message', 'N/A')}")
if status == 'SUCCESS':
print(f"\n{'='*50}")
print(f"✓✓ EXPORTAÇÃO CONCLUÍDA COM SUCESSO! ✓✓")
print(f"{'='*50}")
# Retornar o nome do arquivo junto com o sucesso
file_name = item.get('fileName', 'arquivo_exportado.csv')
return True, file_name
elif status == 'FAILED' or status == 'ERROR':
print(f"\n{'='*50}")
print(f"✗✗ EXPORTAÇÃO FALHOU! ✗✗")
print(f"{'='*50}")
return False, None
else:
print(f"⚠ Exportação ainda em processamento...")
print(f"Aguardando {interval} segundos para próxima verificação...")
time.sleep(interval)
break
else:
# Se não encontrou o item na lista
print(f"✗ Exportação com ID '{request_id}' não encontrada na lista.")
print(f"Total de itens na lista: {len(items)}")
print(f"Aguardando {interval} segundos para próxima verificação...")
time.sleep(interval)
else:
print(f"Erro ao verificar status: {response.text}")
print(f"Aguardando {interval} segundos para próxima verificação...")
time.sleep(interval)
print(f"\n{'='*50}")
print(f"✗ Tempo limite excedido após {max_attempts} tentativas.")
print(f"{'='*50}")
return False, None
# ==============================
# 6) Conectar ao banco de dados
# ==============================
def get_db_connection():
"""
Cria e retorna uma conexão com o banco de dados SQL Server.
Returns:
Conexão pyodbc
"""
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
# ==============================
# 7) Baixar arquivo e inserir no banco
# ==============================
def download_and_insert_to_db(token, request_id, file_name, business_unit, cycle):
"""
Faz o download do arquivo exportado e insere os dados no banco de dados.
Remove dados existentes para a mesma data, marca e ciclo antes de inserir.
Args:
token: Token de autenticação
request_id: ID da requisição de exportação
file_name: Nome do arquivo (usado apenas para referência)
business_unit: Unidade de negócio (BOT, OUI, QDB)
cycle: Ciclo formatado (ex: 202516)
Returns:
True se o download e inserção foram bem-sucedidos, False caso contrário
"""
url = f"https://mar-api-gateway-front.demanda-abastecimento.grupoboticario.digital/orders-bff/api/export/{request_id}/download?redirect=false"
headers = {
"accept": "*/*",
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
"authorization": token,
"origin": "https://extranet.grupoboticario.com.br",
"priority": "u=1, i",
"referer": "https://extranet.grupoboticario.com.br/",
"sec-ch-ua": "\"Chromium\";v=\"142\", \"Google Chrome\";v=\"142\", \"Not_A Brand\";v=\"99\"",
"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/142.0.0.0 Safari/537.36",
"x-correlation-id": "788106b9-3985-44a5-aba6-1e0f2d1eff6c",
"x-user-id": "163165",
"x-username": "daniel.rodrigue"
}
print("\n" + "="*50)
print("Iniciando download e inserção no banco...")
print("="*50)
print(f"Request ID: {request_id}")
print(f"Arquivo: {file_name}")
try:
# Primeira requisição: obter a URL do S3
response = requests.get(url, headers=headers)
print(f"Status da requisição: {response.status_code}")
if response.status_code == 200:
# Parsear o JSON para obter a URL do arquivo
data = response.json()
file_url = data.get("fileUrl")
expire_in = data.get("expireIn")
if not file_url:
print(f"\n{'='*50}")
print(f"✗✗ URL DO ARQUIVO NÃO ENCONTRADA! ✗✗")
print(f"{'='*50}")
print(f"Resposta: {response.text}")
return False
print(f"URL do arquivo obtida com sucesso!")
print(f"Expira em: {expire_in}")
print(f"\nBaixando arquivo do S3...")
# Segunda requisição: baixar o arquivo do S3
file_response = requests.get(file_url)
print(f"Status do download: {file_response.status_code}")
if file_response.status_code == 200:
print(f"✓ Arquivo baixado com sucesso!")
print(f"\nLendo arquivo Excel...")
# Ler o arquivo Excel diretamente da memória
excel_data = BytesIO(file_response.content)
df = pd.read_excel(excel_data, engine='openpyxl')
print(f"✓ Arquivo lido com sucesso!")
print(f"Total de linhas: {len(df)}")
# Mapeamento das colunas do Excel para as colunas do banco
column_mapping = {
'Ciclo': 'Ciclo',
'Ponto de Venda': 'PontoDeVenda',
'Canal': 'Canal',
'SKU': 'SKU',
'Descrição': 'Descricao',
'Categoria': 'Categoria',
'Marca': 'Marca',
'Classe': 'Classe',
'Valor da Receita (R$)': 'ValorReceita',
'Valor da Ruptura (R$)': 'ValorRuptura',
'Percentual da Ruptura (%)': 'PercentualRuptura',
'Quantidade de Ruptura': 'QuantidadeRuptura',
'Macro Causa': 'MacroCausa',
'Origem Ruptura': 'OrigemRuptura'
}
# Renomear as colunas
df.rename(columns=column_mapping, inplace=True)
print(f"\nConectando ao banco de dados...")
conn = get_db_connection()
cursor = conn.cursor()
print(f"✓ Conectado ao banco de dados!")
# Calcular a data de ruptura (dia anterior)
dt_ruptura = (datetime.now() - timedelta(days=1)).date()
print(f"Data de ruptura: {dt_ruptura}")
# Deletar dados existentes para a mesma data, marca e ciclo
print(f"\nVerificando dados existentes para {business_unit} - Ciclo {cycle} em {dt_ruptura}...")
delete_query = """
DELETE FROM RupturaG
WHERE dt_ruptura = ? AND Marca = ? AND Ciclo = ?
"""
cursor.execute(delete_query, dt_ruptura, business_unit, cycle)
rows_deleted = cursor.rowcount
conn.commit()
if rows_deleted > 0:
print(f"{rows_deleted} registros anteriores removidos para {business_unit} - Ciclo {cycle} em {dt_ruptura}")
else:
print(f"✓ Nenhum registro anterior encontrado para {business_unit} - Ciclo {cycle} em {dt_ruptura}")
print(f"\nInserindo dados na tabela RupturaG...")
# Inserir os dados linha por linha
insert_query = """
INSERT INTO RupturaG (
Ciclo, PontoDeVenda, Canal, SKU, Descricao, Categoria,
Marca, Classe, ValorReceita, ValorRuptura, PercentualRuptura,
QuantidadeRuptura, MacroCausa, OrigemRuptura, dt_ruptura
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
rows_inserted = 0
for index, row in df.iterrows():
try:
cursor.execute(insert_query,
row['Ciclo'],
row['PontoDeVenda'],
row['Canal'],
row['SKU'],
row['Descricao'],
row['Categoria'],
row['Marca'],
row['Classe'],
row['ValorReceita'],
row['ValorRuptura'],
row['PercentualRuptura'],
row['QuantidadeRuptura'],
row['MacroCausa'],
row['OrigemRuptura'],
dt_ruptura
)
rows_inserted += 1
# Commit a cada 1000 linhas para melhor performance
if rows_inserted % 1000 == 0:
conn.commit()
print(f"{rows_inserted} linhas inseridas...")
except Exception as e:
print(f" ✗ Erro ao inserir linha {index}: {str(e)}")
continue
# Commit final
conn.commit()
cursor.close()
conn.close()
print(f"\n{'='*50}")
print(f"✓✓ DADOS INSERIDOS NO BANCO COM SUCESSO! ✓✓")
print(f"{'='*50}")
print(f"Total de linhas inseridas: {rows_inserted}/{len(df)}")
return True
else:
print(f"\n{'='*50}")
print(f"✗✗ ERRO AO BAIXAR ARQUIVO DO S3! ✗✗")
print(f"{'='*50}")
print(f"Status: {file_response.status_code}")
return False
else:
print(f"\n{'='*50}")
print(f"✗✗ ERRO AO OBTER URL DO ARQUIVO! ✗✗")
print(f"{'='*50}")
print(f"Resposta: {response.text}")
return False
except Exception as e:
print(f"\n{'='*50}")
print(f"✗✗ ERRO AO PROCESSAR ARQUIVO! ✗✗")
print(f"{'='*50}")
print(f"Erro: {str(e)}")
import traceback
traceback.print_exc()
return False
# ==============================
# 7) Processar exportação de uma marca
# ==============================
def processar_marca(token, business_unit, cycle):
"""
Processa a exportação completa de uma marca (exportar, verificar, baixar).
Args:
token: Token de autenticação
business_unit: Unidade de negócio (BOT, OUI, QDB)
cycle: Ciclo formatado (ex: 202516)
Returns:
True se todo o processo foi bem-sucedido, False caso contrário
"""
print(f"\n{'#'*300}")
print(f"# PROCESSANDO MARCA: {business_unit} - CICLO: {cycle}")
print(f"{'#'*300}")
# 1. Enviar requisição de exportação
request_id = export_with_token(token, business_unit, cycle)
if not request_id:
print(f"\n✗ Não foi possível obter o ID da requisição para {business_unit}.")
return False
print(f"\nID da requisição gerado: {request_id}")
# 2. Verificar o status da exportação
success, file_name = check_export_status(token, request_id)
if not success:
print(f"\n✗ Exportação não foi concluída com sucesso para {business_unit}.")
return False
if not file_name:
print(f"\n⚠ Exportação concluída, mas nome do arquivo não disponível para {business_unit}.")
return False
# 3. Fazer o download do arquivo e inserir no banco
download_success = download_and_insert_to_db(token, request_id, file_name, business_unit, cycle)
if download_success:
print(f"\n{'='*60}")
print(f"✓✓✓ MARCA {business_unit} PROCESSADA COM SUCESSO! ✓✓✓")
print(f"{'='*60}")
return True
else:
print(f"\n✗ Erro ao processar arquivo para {business_unit}.")
return False
# ==============================
# EXECUTAR
# ==============================
if __name__ == "__main__":
print("="*60)
print("INICIANDO PROCESSO DE EXPORTAÇÃO DE RUPTURAS")
print("="*60)
# 1. Buscar token
print("\n[1/3] Buscando token...")
token = get_token()
print("✓ Token obtido com sucesso!")
# 2. Buscar ciclos de todas as marcas
print("\n[2/3] Buscando ciclos das marcas...")
cycles_json = get_cycles(token)
ciclos_por_marca = ciclo_formatado_por_marca(cycles_json)
print("✓ Ciclos obtidos:")
for marca, ciclo in ciclos_por_marca.items():
print(f" - {marca}: {ciclo}")
# 3. Processar cada marca sequencialmente
print("\n[3/3] Processando exportações...")
resultados = {}
for business_unit, cycle in ciclos_por_marca.items():
if cycle is None:
print(f"\n⚠ Ciclo não encontrado para {business_unit}, pulando...")
resultados[business_unit] = False
continue
sucesso = processar_marca(token, business_unit, cycle)
resultados[business_unit] = sucesso
# Pequena pausa entre marcas para não sobrecarregar a API
if business_unit != list(ciclos_por_marca.keys())[-1]: # Se não for a última marca
print(f"\n{'~'*60}")
print("Aguardando 3 segundos antes de processar próxima marca...")
print(f"{'~'*60}")
time.sleep(3)
# Resumo final
print(f"\n{'='*60}")
print("RESUMO FINAL")
print(f"{'='*60}")
for marca, sucesso in resultados.items():
status = "✓ SUCESSO" if sucesso else "✗ FALHOU"
print(f"{marca}: {status}")
total_sucesso = sum(1 for s in resultados.values() if s)
total_marcas = len(resultados)
print(f"\nTotal: {total_sucesso}/{total_marcas} marcas processadas com sucesso")
print(f"{'='*60}")