commit e250eb3cf6874b26c86f02193a6a87049dee54c6 Author: daniel.rodrigues Date: Fri Feb 20 09:00:12 2026 -0300 first commit diff --git a/Grgb_sale_receipts.py b/Grgb_sale_receipts.py new file mode 100644 index 0000000..45b0036 --- /dev/null +++ b/Grgb_sale_receipts.py @@ -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) diff --git a/Notas-calamo.py b/Notas-calamo.py new file mode 100644 index 0000000..a07e81d --- /dev/null +++ b/Notas-calamo.py @@ -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() diff --git a/Pedidos_mar.py b/Pedidos_mar.py new file mode 100644 index 0000000..ef64b8e --- /dev/null +++ b/Pedidos_mar.py @@ -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) diff --git a/ciclos.py b/ciclos.py new file mode 100644 index 0000000..78536e6 --- /dev/null +++ b/ciclos.py @@ -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.") diff --git a/draft_mar.py b/draft_mar.py new file mode 100644 index 0000000..6cd8b42 --- /dev/null +++ b/draft_mar.py @@ -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() + diff --git a/estoque_mar.py b/estoque_mar.py new file mode 100644 index 0000000..ca6a167 --- /dev/null +++ b/estoque_mar.py @@ -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() + diff --git a/extracao_vendashora_rgb.py b/extracao_vendashora_rgb.py new file mode 100644 index 0000000..d1fb197 --- /dev/null +++ b/extracao_vendashora_rgb.py @@ -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) + diff --git a/refreshprice.py b/refreshprice.py new file mode 100644 index 0000000..3aa33eb --- /dev/null +++ b/refreshprice.py @@ -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() diff --git a/rgb_fiscal_invoices.py b/rgb_fiscal_invoices.py new file mode 100644 index 0000000..fa7348b --- /dev/null +++ b/rgb_fiscal_invoices.py @@ -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() \ No newline at end of file diff --git a/rgb_products.py b/rgb_products.py new file mode 100644 index 0000000..9b76f87 --- /dev/null +++ b/rgb_products.py @@ -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.") diff --git a/rgb_sale_receipts.py b/rgb_sale_receipts.py new file mode 100644 index 0000000..410f316 --- /dev/null +++ b/rgb_sale_receipts.py @@ -0,0 +1,1427 @@ +#!/usr/bin/env python3 +""" +Script para acessar a API do Grupo Boticário - Sale Receipts +Busca recibos de vendas por data específica +""" + +import requests +import pyodbc +from datetime import datetime +from typing import Dict, List, Optional +from concurrent.futures import ThreadPoolExecutor, as_completed +import threading +import time + +# ============================== +# Configurações de paralelismo +# ============================== +MAX_WORKERS = 5 # Número de requisições paralelas +PAGE_SIZE = 50 # Itens por página (padrão da API) +MAX_RETRIES = 5 # Máximo de tentativas por página +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() + + +class BoticarioAPI: + """Cliente para acessar a API do Grupo Boticário""" + + def __init__(self, bearer_token: str): + """ + Inicializa o cliente da API + + Args: + bearer_token: Token de autenticação Bearer + """ + self.base_url = "https://api.grupoboticario.com.br" + self.bearer_token = bearer_token + self.headers = { + "Authorization": f"Bearer {bearer_token}", + "Content-Type": "application/json", + "Accept": "application/json" + } + self.token_lock = threading.Lock() + + def refresh_token(self): + """Renova o token de autenticação buscando do banco de dados""" + with self.token_lock: + print("\n🔄 Renovando token de autenticação...") + new_token = get_bearer_token_from_database() + if new_token: + self.bearer_token = new_token + self.headers["Authorization"] = f"Bearer {new_token}" + print("✅ Token renovado com sucesso!") + return True + else: + print("❌ Falha ao renovar token") + return False + + def update_token(self, new_token: str): + """Atualiza o token manualmente""" + with self.token_lock: + self.bearer_token = new_token + self.headers["Authorization"] = f"Bearer {new_token}" + + def get_sale_receipts_page(self, sale_date: str, start: int = 0, silent: bool = False) -> Optional[Dict]: + """ + Busca uma página de recibos de vendas por data + + Args: + sale_date: Data da venda no formato YYYY-MM-DD + start: Índice inicial para paginação (padrão: 0) + silent: Se True, não imprime logs (para uso em paralelo) + + Returns: + Resposta da API ou None em caso de erro + Retorna {"token_expired": True} se o token expirou + """ + endpoint = "/global/v1/franchising/gb-stores-data/sale/receipts" + url = f"{self.base_url}{endpoint}" + + params = { + "receipt.saleDate": sale_date, + "start": start + } + + try: + if not silent: + print(f"Fazendo requisição para: {url}") + print(f"Parâmetros: {params}") + + response = requests.get( + url=url, + headers=self.headers, + params=params, + timeout=60 + ) + + if not silent: + print(f"Status Code: {response.status_code}") + + if response.status_code == 200: + return response.json() + elif response.status_code in [401, 403]: + # Token expirado ou inválido + if not silent: + print(f"⚠️ Token expirado ou inválido (HTTP {response.status_code})") + return {"token_expired": True} + else: + if not silent: + print(f"Erro na requisição: {response.status_code}") + print(f"Resposta: {response.text}") + return None + + except requests.exceptions.RequestException as e: + if not silent: + print(f"Erro na requisição: {e}") + return None + + def fetch_page_with_retry(self, sale_date: str, start: int, total_pages: int, page_num: int) -> Dict: + """ + Busca uma página específica com retry e retorna resultado + + Args: + sale_date: Data da venda + start: Índice inicial + total_pages: Total de páginas (para log) + page_num: Número da página atual (para log) + + Returns: + Dict com 'success', 'start', 'items', 'token_expired' + """ + for attempt in range(1, MAX_RETRIES + 1): + try: + result = self.get_sale_receipts_page(sale_date, start=start, silent=True) + + # Verificar se token expirou + if result and result.get('token_expired'): + with print_lock: + print(f" ⚠️ Página {page_num} (start={start}): Token expirado!") + return {"success": False, "start": start, "items": [], "token_expired": True} + + if result and result.get('items') is not None: + items = result.get('items', []) + + with print_lock: + print(f" ✓ Página {page_num}/{total_pages}: {len(items)} recibos (start={start})") + + return {"success": True, "start": start, "items": items, "token_expired": False} + else: + with print_lock: + print(f" ✗ Página {page_num} (start={start}): Erro - 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) + + return {"success": False, "start": start, "items": [], "token_expired": False} + + def get_all_sale_receipts(self, sale_date: str) -> Optional[Dict]: + """ + Busca TODOS os recibos de vendas por data usando requisições PARALELAS + Com renovação automática de token quando expirar + + Args: + sale_date: Data da venda no formato YYYY-MM-DD + + Returns: + Dicionário com todos os recibos consolidados ou None em caso de erro + """ + print(f"🔍 Iniciando busca paralela de recibos para {sale_date}") + + # Primeira requisição para descobrir o total + first_page = self.get_sale_receipts_page(sale_date, start=0) + + # Se token expirou na primeira requisição, renovar e tentar novamente + if first_page and first_page.get('token_expired'): + print("⚠️ Token expirado na primeira requisição. Renovando...") + if self.refresh_token(): + first_page = self.get_sale_receipts_page(sale_date, start=0) + else: + print("❌ Falha ao renovar token") + return None + + if not first_page: + print("❌ Erro na primeira requisição") + return None + + # Extrair informações de paginação + total = first_page.get('total', 0) + first_items = first_page.get('items', []) + page_size = len(first_items) if first_items else PAGE_SIZE + + print(f"📊 Informações iniciais:") + print(f" - Total de recibos: {total}") + print(f" - Itens por página: {page_size}") + + # Se já temos todos os registros, retornar + if len(first_items) >= total: + print("✅ Todos os registros obtidos na primeira requisição") + return first_page + + # 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}") + print(f"\n📥 Iniciando download paralelo...") + + # Coletar todos os items + all_items = [] + failed_pages = [] + token_expired_detected = False + + # Buscar páginas em paralelo + with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + futures = { + executor.submit( + self.fetch_page_with_retry, + sale_date, + start, + total_pages, + (start // page_size) + 1 + ): start for start in starts + } + + for future in as_completed(futures): + result = future.result() + + if result["success"]: + all_items.extend(result["items"]) + else: + failed_pages.append(result["start"]) + if result.get("token_expired"): + token_expired_detected = True + + # Se detectou token expirado, renovar e reprocessar páginas que falharam + if token_expired_detected and failed_pages: + print("\n🔄 Token expirado detectado. Renovando token...") + if self.refresh_token(): + print(f"✅ Token renovado. Reprocessando {len(failed_pages)} páginas...") + + # Retry para páginas que falharam (com token renovado se necessário) + retry_round = 0 + max_retry_rounds = 10 # Limite de rodadas de retry + while failed_pages and retry_round < max_retry_rounds: + retry_round += 1 + print(f"\n ⚠ Rodada de retry #{retry_round}: {len(failed_pages)} páginas falharam") + print(f" Aguardando {FINAL_RETRY_DELAY} segundos...") + time.sleep(FINAL_RETRY_DELAY) + + still_failed = [] + token_expired_in_retry = False + + for start in failed_pages: + page_num = (start // page_size) + 1 + result = self.fetch_page_with_retry(sale_date, start, total_pages, page_num) + if result["success"]: + all_items.extend(result["items"]) + else: + still_failed.append(start) + if result.get("token_expired"): + token_expired_in_retry = True + + # Se token expirou durante retry, renovar + if token_expired_in_retry and still_failed: + print("\n🔄 Token expirou durante retry. Renovando...") + self.refresh_token() + + failed_pages = still_failed + + if failed_pages: + print(f" Ainda restam {len(failed_pages)} páginas com falha. Tentando novamente...") + + # Criar resposta consolidada + consolidated_response = { + "start": 0, + "count": len(all_items), + "total": total, + "items": all_items + } + + print(f"\n🎉 Download paralelo concluído!") + print(f" - Total de registros obtidos: {len(all_items)}") + print(f" - Total esperado: {total}") + print(f" - Sucesso: {'✅' if len(all_items) == total else '⚠️'}") + + if failed_pages: + print(f" - ⚠️ Páginas não recuperadas: {len(failed_pages)}") + + return consolidated_response + + + + def connect_to_database(self): + """ + Conecta ao banco de dados SQL Server + + Returns: + Conexão pyodbc ou None em caso de erro + """ + # Lista de drivers para tentar em ordem de preferência + drivers = [ + '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' + ] + + print("🔍 Verificando drivers ODBC disponíveis...") + available_drivers = pyodbc.drivers() + print(f"Drivers encontrados: {available_drivers}") + + for driver in drivers: + if driver in available_drivers: + print(f"🔄 Tentando conectar com driver: {driver}") + try: + connection_string = ( + f'DRIVER={{{driver}}};' + 'SERVER=10.77.77.10;' + 'DATABASE=GINSENG;' + 'UID=supginseng;' + 'PWD=Ginseng@;' + 'PORT=1433;' + 'TrustServerCertificate=yes;' + 'Encrypt=yes' + ) + + conn = pyodbc.connect(connection_string) + print(f"✅ Conexão estabelecida com sucesso usando: {driver}") + return conn + + except Exception as e: + print(f"❌ Falha com {driver}: {e}") + continue + + # Se nenhum driver funcionou, tentar sem TrustServerCertificate e Encrypt + print("🔄 Tentando conexão sem SSL...") + for driver in drivers: + if driver in available_drivers: + try: + connection_string = ( + f'DRIVER={{{driver}}};' + 'SERVER=10.77.77.10;' + 'DATABASE=GINSENG;' + 'UID=supginseng;' + 'PWD=Ginseng@;' + 'PORT=1433' + ) + + conn = pyodbc.connect(connection_string) + print(f"✅ Conexão estabelecida sem SSL usando: {driver}") + return conn + + except Exception as e: + print(f"❌ Falha sem SSL com {driver}: {e}") + continue + + print("❌ Não foi possível conectar com nenhum driver disponível") + print("💡 Sugestão: Instale o Microsoft ODBC Driver for SQL Server") + print(" Download: https://docs.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server") + return None + + def check_and_install_odbc_driver(self): + """ + Verifica se há drivers ODBC disponíveis e fornece instruções de instalação + """ + print("\n🔧 DIAGNÓSTICO DE DRIVERS ODBC") + print("="*40) + + available_drivers = pyodbc.drivers() + print(f"Drivers ODBC encontrados: {len(available_drivers)}") + + if available_drivers: + for i, driver in enumerate(available_drivers, 1): + print(f" {i}. {driver}") + else: + print(" ❌ Nenhum driver ODBC encontrado!") + + # Verificar se há drivers SQL Server específicos + sql_server_drivers = [d for d in available_drivers if 'SQL Server' in d] + + if not sql_server_drivers: + print("\n❌ Nenhum driver SQL Server encontrado!") + print("\n💡 SOLUÇÕES:") + print("1. Instalar Microsoft ODBC Driver 18 for SQL Server:") + print(" https://go.microsoft.com/fwlink/?linkid=2249006") + print("\n2. Ou executar no PowerShell como Administrador:") + print(" winget install Microsoft.ODBCDriver.18.SQLServer") + print("\n3. Ou baixar manualmente:") + print(" https://docs.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server") + return False + else: + print(f"\n✅ Encontrados {len(sql_server_drivers)} driver(s) SQL Server:") + for driver in sql_server_drivers: + print(f" • {driver}") + return True + + def insert_receipts_to_database(self, receipts_data: Dict, skip_duplicates: bool = True) -> bool: + """ + Insere os dados dos recibos no banco de dados SQL Server + + Args: + receipts_data: Dados dos recibos retornados pela API + skip_duplicates: Se True, pula registros duplicados; se False, tenta inserir todos + + Returns: + True se inserção foi bem-sucedida, False caso contrário + """ + conn = self.connect_to_database() + if not conn: + return False + + try: + cursor = conn.cursor() + + # Detectar a data de venda dos dados + sale_date = self.get_sale_date_from_data(receipts_data) + + if not sale_date: + print("❌ Não foi possível detectar a data de venda. Abortando inserção.") + return False + + # Deletar dados existentes para essa data + print(f"\n🧹 LIMPEZA DE DADOS EXISTENTES") + print("="*40) + delete_success = self.delete_existing_data_by_date(sale_date) + + if not delete_success: + print("❌ Falha ao deletar dados existentes. Abortando inserção.") + return False + + # Obter lista de items + items = receipts_data.get('items', []) + total_records = 0 + batch_size = 1000 + + print(f"\n🔄 INICIANDO INSERÇÃO DE NOVOS DADOS") + print("="*40) + print(f"📅 Data: {sale_date}") + print(f"📊 Recibos para processar: {len(items)}") + print(f"📦 Processando em lotes de {batch_size} registros") + if skip_duplicates: + print("⚠️ Modo: Pular registros duplicados") + else: + print("🔄 Modo: Inserir todos os registros") + + # Processar em lotes usando inserção em massa + total_batches = (len(items) + batch_size - 1) // batch_size + + for batch_num in range(total_batches): + start_idx = batch_num * batch_size + end_idx = min(start_idx + batch_size, len(items)) + batch_items = items[start_idx:end_idx] + + print(f"\n📦 Processando lote {batch_num + 1}/{total_batches} ({len(batch_items)} recibos)") + + try: + # Preparar dados do lote para inserção em massa + batch_values = self._prepare_batch_data(batch_items) + + if batch_values: + # Preparar SQL para inserção em massa + fields = [ + # Campos do cupom (29 campos) + 'cupomId', 'cupomReceiptSequence', 'cupomCashRegisterNumber', 'cupomStoreId', 'cupomCoo', + 'cupomEmployeeId', 'cupomEmployeeName', 'cupomValue', 'cupomAdditionalValue', 'cupomDiscountValue', + 'cupomItemsQuantity', 'cupomUnitsQuantity', 'cupomCancelledItemsQuantity', 'cupomCancelledItemsValue', + 'cupomSaleType', 'cupomCancelledUnitsQuantity', 'cupomSaleDate', 'cupomInvoiceXMLStatus', + 'cupomReceiptOpeningDateTime', 'cupomReceiptClosingDateTime', 'cupomEletronicKey', 'cupomSaleOrderId', + 'cupomExternalId', 'cupomDiscountReason', 'cupomLoyaltyDiscountValue', 'cupomCancellingReason', + 'cupomCancelledReceiptSequence', 'cupomChannel', 'cupomChannelDescription', + # Campos do item (27 campos) + 'itemId', 'itemCancelled', 'itemProductId', 'itemSellerId', 'itemSellerName', 'itemQuantity', + 'itemUnitValue', 'itemGrossValue', 'itemAdditionalValue', 'itemDiscountValue', 'itemTotalValue', + 'itemTabelaA', 'itemNcm', 'itemNcmExcecao', 'itemNatureza', 'itemCfop', 'itemCsosn', + 'itemCstICMS', 'itemAliquotaICMS', 'itemValorReducaoAliquotaICMS', 'itemValorICMSDesonerado', + 'itemValorFecop', 'itemAliquotaFecop', 'itemCstPIS', 'itemAliquotaPIS', 'itemCstCOFINS', 'itemAliquotaCOFINS', + # Campos do payment (16 campos) + 'paymentId', 'paymentMethodId', 'paymentMethodDescription', 'paymentValue', 'paymentChange', + 'paymentInstallmentQuantity', 'paymentCheckIssuer', 'paymentCardAuthorization', 'paymentCardFlag', + 'paymentCardFlagDescription', 'paymentCardModality', 'paymentRedeAdquirente', 'paymentNsu', + 'paymentAuthorizationNsu', 'paymentNsuCancelling', 'paymentCardBinNumber', + # Campos do tef (12 campos) + 'teftransId', 'teftransSequential', 'teftransPaymentMethodDescription', 'teftransValue', + 'teftransCardModality', 'teftransCancellingModality', 'teftransCardType', 'teftranSitefNsu', + 'teftransAuthorizerHostNsu', 'teftransAuthorizationCode', 'teftransInstallmentQuantity', 'teftransFirstIntallmentDate' + ] + + fields_str = ', '.join([f'[{field}]' for field in fields]) + placeholders = ', '.join(['?'] * len(fields)) + + sql = f""" + INSERT INTO [GINSENG].[dbo].[rgb_sale_receipts] ( + {fields_str} + ) VALUES ( + {placeholders} + ) + """ + + # Inserção em massa usando executemany + print(f" 🚀 Inserindo {len(batch_values)} registros em massa...") + cursor.executemany(sql, batch_values) + + batch_inserted = len(batch_values) + total_records += batch_inserted + + print(f" ✅ Lote {batch_num + 1} concluído: {batch_inserted} registros inseridos") + else: + print(f" ⚠️ Lote {batch_num + 1} vazio - nenhum registro para inserir") + + except Exception as e: + print(f" ❌ Erro no lote {batch_num + 1}: {e}") + if not skip_duplicates: + # Se não está pulando duplicatas, continuar com próximo lote + continue + else: + raise e + + # Commit do lote + conn.commit() + + print(f"\n🎉 INSERÇÃO EM LOTES COMPLETA!") + print(f" 📊 Total de registros inseridos: {total_records}") + print(f" 📈 Total de recibos processados: {len(items)}") + print(f" � Total de lotes processados: {total_batches}") + print(f" ⚡ Inserção otimizada em lotes de {batch_size} registros") + + return True + + except Exception as e: + print(f"❌ Erro durante inserção no banco: {e}") + conn.rollback() + return False + finally: + conn.close() + print("🔌 Conexão com banco de dados fechada") + + def delete_existing_data_by_date(self, sale_date: str) -> bool: + """ + Deleta todos os registros existentes para uma data específica + + Args: + sale_date: Data da venda no formato YYYY-MM-DD + + Returns: + True se deleção foi bem-sucedida, False caso contrário + """ + conn = self.connect_to_database() + if not conn: + return False + + try: + cursor = conn.cursor() + + # Primeiro, verificar quantos registros existem para essa data + count_sql = """ + SELECT COUNT(*) FROM [GINSENG].[dbo].[rgb_sale_receipts] + WHERE [cupomSaleDate] = ? + """ + cursor.execute(count_sql, (sale_date,)) + existing_count = cursor.fetchone()[0] + + if existing_count > 0: + print(f"🗑️ Encontrados {existing_count} registros existentes para a data {sale_date}") + print(f"🔄 Deletando registros antigos...") + + # Deletar registros existentes + delete_sql = """ + DELETE FROM [GINSENG].[dbo].[rgb_sale_receipts] + WHERE [cupomSaleDate] = ? + """ + cursor.execute(delete_sql, (sale_date,)) + deleted_count = cursor.rowcount + + # Commit da deleção + conn.commit() + + print(f"✅ {deleted_count} registros deletados com sucesso!") + return True + else: + print(f"ℹ️ Nenhum registro existente encontrado para a data {sale_date}") + return True + + except Exception as e: + print(f"❌ Erro ao deletar dados existentes: {e}") + conn.rollback() + return False + finally: + conn.close() + + def get_sale_date_from_data(self, receipts_data: Dict) -> str: + """ + Extrai e valida a data de venda dos dados dos recibos + + Args: + receipts_data: Dados dos recibos retornados pela API + + Returns: + Data da venda no formato YYYY-MM-DD ou None se não encontrada + """ + items = receipts_data.get('items', []) + + if not items: + print("⚠️ Nenhum recibo encontrado nos dados") + return None + + # Pegar a data do primeiro recibo + first_receipt = items[0] + sale_date = first_receipt.get('saleDate') + + if not sale_date: + print("⚠️ Não foi possível detectar a data de venda dos dados") + return None + + # Validar se todos os recibos são da mesma data + different_dates = [] + for i, receipt in enumerate(items): + receipt_date = receipt.get('saleDate') + if receipt_date != sale_date: + different_dates.append(f"Recibo {i+1}: {receipt_date}") + + if different_dates: + print(f"⚠️ ATENÇÃO: Encontradas datas diferentes nos recibos!") + print(f" Data principal: {sale_date}") + for diff_date in different_dates[:5]: # Mostrar apenas os primeiros 5 + print(f" {diff_date}") + if len(different_dates) > 5: + print(f" ... e mais {len(different_dates) - 5} recibos com datas diferentes") + + print(f"🔄 Usando data principal: {sale_date}") + + print(f"📅 Data de venda detectada: {sale_date}") + return sale_date + + def _prepare_batch_data(self, receipts_batch: List[Dict]) -> List[List]: + """ + Prepara dados de um lote de recibos para inserção em massa + + Args: + receipts_batch: Lista de recibos para processar + + Returns: + Lista de listas com valores para inserção + """ + batch_values = [] + + for receipt in receipts_batch: + # Processar cada recibo e extrair todos os registros (linhas) + receipt_records = self._extract_receipt_records(receipt) + batch_values.extend(receipt_records) + + return batch_values + + def _extract_receipt_records(self, receipt: Dict) -> List[List]: + """ + Extrai todos os registros (linhas) de um recibo + Cada item vira uma linha separada + """ + records = [] + + # Dados do cupom (serão repetidos para cada item) + cupom_data = self._extract_cupom_data(receipt) + + # Dados consolidados de payments e tef + consolidated_payment_data = self._consolidate_payments(receipt.get('payments', [])) + consolidated_tef_data = self._consolidate_tef_transactions(receipt.get('tefTransactionItems', [])) + + # Processar items + items = receipt.get('items', []) + + if not items: + # Se não há items, criar uma linha apenas com dados do cupom + record_values = self._build_record_values(cupom_data, None, consolidated_payment_data, consolidated_tef_data) + records.append(record_values) + else: + # Para cada item, criar uma linha + for item in items: + item_data = self._extract_item_data(item) + record_values = self._build_record_values(cupom_data, item_data, consolidated_payment_data, consolidated_tef_data) + records.append(record_values) + + return records + + def _extract_cupom_data(self, receipt: Dict) -> Dict: + """Extrai dados do cupom""" + return { + 'cupomId': receipt.get('id'), + 'cupomReceiptSequence': receipt.get('receiptSequence'), + 'cupomCashRegisterNumber': receipt.get('cashRegisterNumber'), + 'cupomStoreId': receipt.get('storeId'), + 'cupomCoo': receipt.get('coo'), + 'cupomEmployeeId': receipt.get('employeeId'), + 'cupomEmployeeName': receipt.get('employeeName'), + 'cupomValue': receipt.get('value'), + 'cupomAdditionalValue': receipt.get('additionalValue'), + 'cupomDiscountValue': receipt.get('discountValue'), + 'cupomItemsQuantity': receipt.get('itemsQuantity'), + 'cupomUnitsQuantity': receipt.get('unitsQuantity'), + 'cupomCancelledItemsQuantity': receipt.get('cancelledItemsQuantity'), + 'cupomCancelledItemsValue': receipt.get('cancelledItemsValue'), + 'cupomSaleType': receipt.get('saleType'), + 'cupomCancelledUnitsQuantity': receipt.get('cancelledUnitsQuantity'), + 'cupomSaleDate': receipt.get('saleDate'), + 'cupomInvoiceXMLStatus': receipt.get('invoiceXMLStatus'), + 'cupomReceiptOpeningDateTime': receipt.get('receiptOpeningDateTime'), + 'cupomReceiptClosingDateTime': receipt.get('receiptClosingDateTime'), + 'cupomEletronicKey': receipt.get('eletronicKey'), + 'cupomSaleOrderId': receipt.get('saleOrderId'), + 'cupomExternalId': receipt.get('externalId'), + 'cupomDiscountReason': receipt.get('discountReason'), + 'cupomLoyaltyDiscountValue': receipt.get('loyaltyDiscountValue'), + 'cupomCancellingReason': receipt.get('cancellingReason'), + 'cupomCancelledReceiptSequence': receipt.get('cancelledReceiptSequence'), + 'cupomChannel': receipt.get('channel'), + 'cupomChannelDescription': receipt.get('channelDescription') + } + + def _extract_item_data(self, item: Dict) -> Dict: + """Extrai dados do item""" + return { + 'itemId': item.get('id'), + 'itemCancelled': item.get('cancelled'), + 'itemProductId': item.get('productId'), + 'itemSellerId': item.get('sellerId'), + 'itemSellerName': item.get('sellerName'), + 'itemQuantity': item.get('quantity'), + 'itemUnitValue': item.get('unitValue'), + 'itemGrossValue': item.get('grossValue'), + 'itemAdditionalValue': item.get('additionalValue'), + 'itemDiscountValue': item.get('discountValue'), + 'itemTotalValue': item.get('totalValue'), + 'itemTabelaA': item.get('tabelaA'), + 'itemNcm': item.get('ncm'), + 'itemNcmExcecao': item.get('ncmExcecao'), + 'itemNatureza': item.get('natureza'), + 'itemCfop': item.get('cfop'), + 'itemCsosn': item.get('csosn'), + 'itemCstICMS': item.get('cstICMS'), + 'itemAliquotaICMS': item.get('aliquotaICMS'), + 'itemValorReducaoAliquotaICMS': item.get('valorReducaoAliquotaICMS'), + 'itemValorICMSDesonerado': item.get('valorICMSDesonerado'), + 'itemValorFecop': item.get('valorFecop'), + 'itemAliquotaFecop': item.get('aliquotaFecop'), + 'itemCstPIS': item.get('cstPIS'), + 'itemAliquotaPIS': item.get('aliquotaPIS'), + 'itemCstCOFINS': item.get('cstCOFINS'), + 'itemAliquotaCOFINS': item.get('aliquotaCOFINS') + } + + def _build_record_values(self, cupom_data: Dict, item_data: Dict, payment_data: Dict, tef_data: Dict) -> List: + """ + Constrói uma lista de valores na ordem correta para inserção + """ + values = [] + + # Campos do cupom (29 valores) + cupom_fields = [ + 'cupomId', 'cupomReceiptSequence', 'cupomCashRegisterNumber', 'cupomStoreId', 'cupomCoo', + 'cupomEmployeeId', 'cupomEmployeeName', 'cupomValue', 'cupomAdditionalValue', 'cupomDiscountValue', + 'cupomItemsQuantity', 'cupomUnitsQuantity', 'cupomCancelledItemsQuantity', 'cupomCancelledItemsValue', + 'cupomSaleType', 'cupomCancelledUnitsQuantity', 'cupomSaleDate', 'cupomInvoiceXMLStatus', + 'cupomReceiptOpeningDateTime', 'cupomReceiptClosingDateTime', 'cupomEletronicKey', 'cupomSaleOrderId', + 'cupomExternalId', 'cupomDiscountReason', 'cupomLoyaltyDiscountValue', 'cupomCancellingReason', + 'cupomCancelledReceiptSequence', 'cupomChannel', 'cupomChannelDescription' + ] + + for field in cupom_fields: + values.append(cupom_data.get(field) if cupom_data else None) + + # Campos do item (27 valores) + item_fields = [ + 'itemId', 'itemCancelled', 'itemProductId', 'itemSellerId', 'itemSellerName', 'itemQuantity', + 'itemUnitValue', 'itemGrossValue', 'itemAdditionalValue', 'itemDiscountValue', 'itemTotalValue', + 'itemTabelaA', 'itemNcm', 'itemNcmExcecao', 'itemNatureza', 'itemCfop', 'itemCsosn', + 'itemCstICMS', 'itemAliquotaICMS', 'itemValorReducaoAliquotaICMS', 'itemValorICMSDesonerado', + 'itemValorFecop', 'itemAliquotaFecop', 'itemCstPIS', 'itemAliquotaPIS', 'itemCstCOFINS', 'itemAliquotaCOFINS' + ] + + for field in item_fields: + values.append(item_data.get(field) if item_data else None) + + # Campos do pagamento (16 valores) + payment_fields = [ + 'paymentId', 'paymentMethodId', 'paymentMethodDescription', 'paymentValue', 'paymentChange', + 'paymentInstallmentQuantity', 'paymentCheckIssuer', 'paymentCardAuthorization', 'paymentCardFlag', + 'paymentCardFlagDescription', 'paymentCardModality', 'paymentRedeAdquirente', 'paymentNsu', + 'paymentAuthorizationNsu', 'paymentNsuCancelling', 'paymentCardBinNumber' + ] + + for field in payment_fields: + values.append(payment_data.get(field) if payment_data else None) + + # Campos do TEF (12 valores) + tef_fields = [ + 'teftransId', 'teftransSequential', 'teftransPaymentMethodDescription', 'teftransValue', + 'teftransCardModality', 'teftransCancellingModality', 'teftransCardType', 'teftranSitefNsu', + 'teftransAuthorizerHostNsu', 'teftransAuthorizationCode', 'teftransInstallmentQuantity', 'teftransFirstIntallmentDate' + ] + + for field in tef_fields: + values.append(tef_data.get(field) if tef_data else None) + + return values + + def _insert_receipt_data(self, cursor, receipt: Dict, skip_duplicates: bool = True) -> Dict: + """ + Insere dados de um recibo específico - UMA LINHA POR ITEM + Cada item gera uma linha separada com os dados do cupom repetidos + + Args: + cursor: Cursor do banco de dados + receipt: Dados de um recibo específico + skip_duplicates: Se True, pula registros duplicados + + Returns: + Dict com estatísticas: {'inserted': int, 'duplicates': int} + """ + records_inserted = 0 + duplicates_skipped = 0 + + # Dados do cupom (receipt) - serão repetidos para cada item + cupom_data = { + 'cupomId': receipt.get('id'), + 'cupomReceiptSequence': receipt.get('receiptSequence'), + 'cupomCashRegisterNumber': receipt.get('cashRegisterNumber'), + 'cupomStoreId': receipt.get('storeId'), + 'cupomCoo': receipt.get('coo'), + 'cupomEmployeeId': receipt.get('employeeId'), + 'cupomEmployeeName': receipt.get('employeeName'), + 'cupomValue': receipt.get('value'), + 'cupomAdditionalValue': receipt.get('additionalValue'), + 'cupomDiscountValue': receipt.get('discountValue'), + 'cupomItemsQuantity': receipt.get('itemsQuantity'), + 'cupomUnitsQuantity': receipt.get('unitsQuantity'), + 'cupomCancelledItemsQuantity': receipt.get('cancelledItemsQuantity'), + 'cupomCancelledItemsValue': receipt.get('cancelledItemsValue'), + 'cupomSaleType': receipt.get('saleType'), + 'cupomCancelledUnitsQuantity': receipt.get('cancelledUnitsQuantity'), + 'cupomSaleDate': receipt.get('saleDate'), + 'cupomInvoiceXMLStatus': receipt.get('invoiceXMLStatus'), + 'cupomReceiptOpeningDateTime': receipt.get('receiptOpeningDateTime'), + 'cupomReceiptClosingDateTime': receipt.get('receiptClosingDateTime'), + 'cupomEletronicKey': receipt.get('eletronicKey'), + 'cupomSaleOrderId': receipt.get('saleOrderId'), + 'cupomExternalId': receipt.get('externalId'), + 'cupomDiscountReason': receipt.get('discountReason'), + 'cupomLoyaltyDiscountValue': receipt.get('loyaltyDiscountValue'), + 'cupomCancellingReason': receipt.get('cancellingReason'), + 'cupomCancelledReceiptSequence': receipt.get('cancelledReceiptSequence'), + 'cupomChannel': receipt.get('channel'), + 'cupomChannelDescription': receipt.get('channelDescription') + } + + # Preparar dados de payments e tef (serão repetidos para cada item) + payments = receipt.get('payments', []) + tef_transactions = receipt.get('tefTransactionItems', []) + + # Preparar dados consolidados de payments (todos os payments em uma estrutura) + consolidated_payment_data = self._consolidate_payments(payments) + consolidated_tef_data = self._consolidate_tef_transactions(tef_transactions) + + # Processar items - CADA ITEM = UMA LINHA + items = receipt.get('items', []) + + if not items: + # Se não há items, inserir apenas dados do cupom (cupom vazio) + result = self._insert_single_record( + cursor, cupom_data, None, consolidated_payment_data, consolidated_tef_data, skip_duplicates + ) + if result == 1: + records_inserted += 1 + elif result == -1: + duplicates_skipped += 1 + else: + # Para cada item, criar UMA LINHA com cupom + item + todos os payments + todos os tef + for item in items: + item_data = { + 'itemId': item.get('id'), + 'itemCancelled': item.get('cancelled'), + 'itemProductId': item.get('productId'), + 'itemSellerId': item.get('sellerId'), + 'itemSellerName': item.get('sellerName'), + 'itemQuantity': item.get('quantity'), + 'itemUnitValue': item.get('unitValue'), + 'itemGrossValue': item.get('grossValue'), + 'itemAdditionalValue': item.get('additionalValue'), + 'itemDiscountValue': item.get('discountValue'), + 'itemTotalValue': item.get('totalValue'), + 'itemTabelaA': item.get('tabelaA'), + 'itemNcm': item.get('ncm'), + 'itemNcmExcecao': item.get('ncmExcecao'), + 'itemNatureza': item.get('natureza'), + 'itemCfop': item.get('cfop'), + 'itemCsosn': item.get('csosn'), + 'itemCstICMS': item.get('cstICMS'), + 'itemAliquotaICMS': item.get('aliquotaICMS'), + 'itemValorReducaoAliquotaICMS': item.get('valorReducaoAliquotaICMS'), + 'itemValorICMSDesonerado': item.get('valorICMSDesonerado'), + 'itemValorFecop': item.get('valorFecop'), + 'itemAliquotaFecop': item.get('aliquotaFecop'), + 'itemCstPIS': item.get('cstPIS'), + 'itemAliquotaPIS': item.get('aliquotaPIS'), + 'itemCstCOFINS': item.get('cstCOFINS'), + 'itemAliquotaCOFINS': item.get('aliquotaCOFINS') + } + + # Inserir UMA linha por item (com dados do cupom, payments e tef repetidos) + result = self._insert_single_record( + cursor, cupom_data, item_data, consolidated_payment_data, consolidated_tef_data, skip_duplicates + ) + if result == 1: + records_inserted += 1 + elif result == -1: + duplicates_skipped += 1 + + return { + 'inserted': records_inserted, + 'duplicates': duplicates_skipped + } + + def _consolidate_payments(self, payments: List[Dict]) -> Dict: + """ + Consolida múltiplos payments em uma estrutura única + Se houver múltiplos payments, pega o primeiro ou consolida conforme necessário + """ + if not payments: + return None + + # Por enquanto, vamos pegar apenas o primeiro payment + # Você pode ajustar esta lógica conforme necessário + first_payment = payments[0] + + return { + 'paymentId': first_payment.get('id'), + 'paymentMethodId': first_payment.get('paymentMethodId'), + 'paymentMethodDescription': first_payment.get('paymentMethodDescription'), + 'paymentValue': first_payment.get('value'), + 'paymentChange': first_payment.get('change'), + 'paymentInstallmentQuantity': first_payment.get('installmentQuantity'), + 'paymentCheckIssuer': first_payment.get('checkIssuer'), + 'paymentCardAuthorization': first_payment.get('cardAuthorization'), + 'paymentCardFlag': first_payment.get('cardFlag'), + 'paymentCardFlagDescription': first_payment.get('cardFlagDescription'), + 'paymentCardModality': first_payment.get('cardModality'), + 'paymentRedeAdquirente': first_payment.get('redeAdquirente'), + 'paymentNsu': first_payment.get('nsu'), + 'paymentAuthorizationNsu': first_payment.get('authorizationNsu'), + 'paymentNsuCancelling': first_payment.get('nsuCancelling'), + 'paymentCardBinNumber': first_payment.get('cardBinNumber') + } + + def _consolidate_tef_transactions(self, tef_transactions: List[Dict]) -> Dict: + """ + Consolida múltiplas transações TEF em uma estrutura única + Se houver múltiplas transações, pega a primeira ou consolida conforme necessário + """ + if not tef_transactions: + return None + + # Por enquanto, vamos pegar apenas a primeira transação TEF + # Você pode ajustar esta lógica conforme necessário + first_tef = tef_transactions[0] + + return { + 'teftransId': first_tef.get('id'), + 'teftransSequential': first_tef.get('sequential'), + 'teftransPaymentMethodDescription': first_tef.get('paymentMethodDescription'), + 'teftransValue': first_tef.get('value'), + 'teftransCardModality': first_tef.get('cardModality'), + 'teftransCancellingModality': first_tef.get('cancellingModality'), + 'teftransCardType': first_tef.get('cardType'), + 'teftranSitefNsu': first_tef.get('sitefNsu'), + 'teftransAuthorizerHostNsu': first_tef.get('authorizerHostNsu'), + 'teftransAuthorizationCode': first_tef.get('authorizationCode'), + 'teftransInstallmentQuantity': first_tef.get('installmentQuantity'), + 'teftransFirstIntallmentDate': first_tef.get('firstIntallmentDate') + } + + def _insert_single_record(self, cursor, cupom_data: Dict, item_data: Dict = None, + payment_data: Dict = None, tef_data: Dict = None, skip_duplicates: bool = True) -> int: + """ + Insere um único registro na tabela rgb_sale_receipts + Verifica se já existe antes de inserir para evitar duplicatas + + Args: + cursor: Cursor do banco de dados + cupom_data: Dados do cupom + item_data: Dados do item (opcional) + payment_data: Dados do pagamento (opcional) + tef_data: Dados da transação TEF (opcional) + skip_duplicates: Se True, pula duplicatas; se False, tenta inserir + + Returns: + 1 se inserção foi bem-sucedida + -1 se registro já existe e foi pulado + 0 se houve erro na inserção + """ + try: + # Verificar se o registro já existe (baseado na restrição UNIQUE) apenas se skip_duplicates=True + if skip_duplicates: + cupom_id = cupom_data.get('cupomId') if cupom_data else None + item_id = item_data.get('itemId') if item_data else None + + if cupom_id and item_id: + # Verificar se já existe a combinação cupomId + itemId + check_sql = """ + SELECT COUNT(*) FROM [GINSENG].[dbo].[rgb_sale_receipts] + WHERE [cupomId] = ? AND [itemId] = ? + """ + cursor.execute(check_sql, (cupom_id, item_id)) + exists = cursor.fetchone()[0] > 0 + + if exists: + print(f"⚠️ Registro já existe: cupomId={cupom_id}, itemId={item_id} - Pulando...") + return -1 # Código para "duplicata pulada" + elif cupom_id: + # Se não há itemId, verificar apenas por cupomId (para cupons sem items) + check_sql = """ + SELECT COUNT(*) FROM [GINSENG].[dbo].[rgb_sale_receipts] + WHERE [cupomId] = ? AND [itemId] IS NULL + """ + cursor.execute(check_sql, (cupom_id,)) + exists = cursor.fetchone()[0] > 0 + + if exists: + print(f"⚠️ Cupom sem item já existe: cupomId={cupom_id} - Pulando...") + return -1 # Código para "duplicata pulada" + # Definir campos da tabela (baseado na estrutura fornecida) + fields = [ + # Campos do cupom (29 campos) + 'cupomId', 'cupomReceiptSequence', 'cupomCashRegisterNumber', 'cupomStoreId', 'cupomCoo', + 'cupomEmployeeId', 'cupomEmployeeName', 'cupomValue', 'cupomAdditionalValue', 'cupomDiscountValue', + 'cupomItemsQuantity', 'cupomUnitsQuantity', 'cupomCancelledItemsQuantity', 'cupomCancelledItemsValue', + 'cupomSaleType', 'cupomCancelledUnitsQuantity', 'cupomSaleDate', 'cupomInvoiceXMLStatus', + 'cupomReceiptOpeningDateTime', 'cupomReceiptClosingDateTime', 'cupomEletronicKey', 'cupomSaleOrderId', + 'cupomExternalId', 'cupomDiscountReason', 'cupomLoyaltyDiscountValue', 'cupomCancellingReason', + 'cupomCancelledReceiptSequence', 'cupomChannel', 'cupomChannelDescription', + # Campos do item (27 campos) + 'itemId', 'itemCancelled', 'itemProductId', 'itemSellerId', 'itemSellerName', 'itemQuantity', + 'itemUnitValue', 'itemGrossValue', 'itemAdditionalValue', 'itemDiscountValue', 'itemTotalValue', + 'itemTabelaA', 'itemNcm', 'itemNcmExcecao', 'itemNatureza', 'itemCfop', 'itemCsosn', + 'itemCstICMS', 'itemAliquotaICMS', 'itemValorReducaoAliquotaICMS', 'itemValorICMSDesonerado', + 'itemValorFecop', 'itemAliquotaFecop', 'itemCstPIS', 'itemAliquotaPIS', 'itemCstCOFINS', 'itemAliquotaCOFINS', + # Campos do payment (16 campos) + 'paymentId', 'paymentMethodId', 'paymentMethodDescription', 'paymentValue', 'paymentChange', + 'paymentInstallmentQuantity', 'paymentCheckIssuer', 'paymentCardAuthorization', 'paymentCardFlag', + 'paymentCardFlagDescription', 'paymentCardModality', 'paymentRedeAdquirente', 'paymentNsu', + 'paymentAuthorizationNsu', 'paymentNsuCancelling', 'paymentCardBinNumber', + # Campos do tef (12 campos) + 'teftransId', 'teftransSequential', 'teftransPaymentMethodDescription', 'teftransValue', + 'teftransCardModality', 'teftransCancellingModality', 'teftransCardType', 'teftranSitefNsu', + 'teftransAuthorizerHostNsu', 'teftransAuthorizationCode', 'teftransInstallmentQuantity', 'teftransFirstIntallmentDate' + ] + + # Total: 29 + 27 + 16 + 12 = 84 campos + # Nota: Não incluímos o campo 'id' pois pode ser auto-incremento + total_fields = len(fields) + + # Montar o SQL dinamicamente + fields_str = ', '.join([f'[{field}]' for field in fields]) + placeholders = ', '.join(['?'] * total_fields) + + sql = f""" + INSERT INTO [GINSENG].[dbo].[rgb_sale_receipts] ( + {fields_str} + ) VALUES ( + {placeholders} + ) + """ + + print(f"🔧 SQL preparado com {total_fields} campos e {total_fields} placeholders") + + # Preparar valores para inserção na ordem exata dos campos + values = [] + + # Dados do cupom (29 valores) + cupom_fields = [ + 'cupomId', 'cupomReceiptSequence', 'cupomCashRegisterNumber', 'cupomStoreId', 'cupomCoo', + 'cupomEmployeeId', 'cupomEmployeeName', 'cupomValue', 'cupomAdditionalValue', 'cupomDiscountValue', + 'cupomItemsQuantity', 'cupomUnitsQuantity', 'cupomCancelledItemsQuantity', 'cupomCancelledItemsValue', + 'cupomSaleType', 'cupomCancelledUnitsQuantity', 'cupomSaleDate', 'cupomInvoiceXMLStatus', + 'cupomReceiptOpeningDateTime', 'cupomReceiptClosingDateTime', 'cupomEletronicKey', 'cupomSaleOrderId', + 'cupomExternalId', 'cupomDiscountReason', 'cupomLoyaltyDiscountValue', 'cupomCancellingReason', + 'cupomCancelledReceiptSequence', 'cupomChannel', 'cupomChannelDescription' + ] + + for field in cupom_fields: + values.append(cupom_data.get(field) if cupom_data else None) + + # Dados do item (27 valores) + item_fields = [ + 'itemId', 'itemCancelled', 'itemProductId', 'itemSellerId', 'itemSellerName', 'itemQuantity', + 'itemUnitValue', 'itemGrossValue', 'itemAdditionalValue', 'itemDiscountValue', 'itemTotalValue', + 'itemTabelaA', 'itemNcm', 'itemNcmExcecao', 'itemNatureza', 'itemCfop', 'itemCsosn', + 'itemCstICMS', 'itemAliquotaICMS', 'itemValorReducaoAliquotaICMS', 'itemValorICMSDesonerado', + 'itemValorFecop', 'itemAliquotaFecop', 'itemCstPIS', 'itemAliquotaPIS', 'itemCstCOFINS', 'itemAliquotaCOFINS' + ] + + for field in item_fields: + values.append(item_data.get(field) if item_data else None) + + # Dados do pagamento (16 valores) + payment_fields = [ + 'paymentId', 'paymentMethodId', 'paymentMethodDescription', 'paymentValue', 'paymentChange', + 'paymentInstallmentQuantity', 'paymentCheckIssuer', 'paymentCardAuthorization', 'paymentCardFlag', + 'paymentCardFlagDescription', 'paymentCardModality', 'paymentRedeAdquirente', 'paymentNsu', + 'paymentAuthorizationNsu', 'paymentNsuCancelling', 'paymentCardBinNumber' + ] + + for field in payment_fields: + values.append(payment_data.get(field) if payment_data else None) + + # Dados do TEF (12 valores) + tef_fields = [ + 'teftransId', 'teftransSequential', 'teftransPaymentMethodDescription', 'teftransValue', + 'teftransCardModality', 'teftransCancellingModality', 'teftransCardType', 'teftranSitefNsu', + 'teftransAuthorizerHostNsu', 'teftransAuthorizationCode', 'teftransInstallmentQuantity', 'teftransFirstIntallmentDate' + ] + + for field in tef_fields: + values.append(tef_data.get(field) if tef_data else None) + + # Verificar se temos exatamente 84 valores + if len(values) != total_fields: + print(f"❌ ERRO: Esperado {total_fields} valores, mas temos {len(values)}") + return 0 + + print(f"✅ Preparados {len(values)} valores para inserção") + + # Executar inserção + cursor.execute(sql, values) + return 1 + + except Exception as e: + print(f"❌ Erro ao inserir registro: {e}") + return 0 + + +def get_bearer_token_from_database(): + """ + Busca o token de autenticação do banco de dados + + Returns: + str: Token de autenticação ou None em caso de erro + """ + # Lista de drivers para tentar em ordem de preferência + drivers = [ + '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' + ] + + print("🔍 Buscando token de autenticação no banco de dados...") + available_drivers = pyodbc.drivers() + + for driver in drivers: + if driver in available_drivers: + try: + connection_string = ( + f'DRIVER={{{driver}}};' + 'SERVER=10.77.77.10;' + 'DATABASE=GINSENG;' + 'UID=supginseng;' + 'PWD=Ginseng@;' + 'PORT=1433;' + 'TrustServerCertificate=yes;' + 'Encrypt=yes' + ) + + conn = pyodbc.connect(connection_string) + cursor = conn.cursor() + + # Buscar o token da tabela dbo.rgb_token com id = 1 + query = "SELECT token FROM dbo.rgb_token WHERE id = 1" + cursor.execute(query) + result = cursor.fetchone() + + conn.close() + + if result: + print("✅ Token obtido do banco de dados com sucesso!") + return result[0] + else: + print("❌ Token não encontrado na tabela dbo.rgb_token com id = 1") + return None + + except Exception as e: + print(f"❌ Erro ao buscar token com {driver}: {e}") + continue + + # Se nenhum driver funcionou, tentar sem SSL + print("🔄 Tentando buscar token sem SSL...") + for driver in drivers: + if driver in available_drivers: + try: + connection_string = ( + f'DRIVER={{{driver}}};' + 'SERVER=10.77.77.10;' + 'DATABASE=GINSENG;' + 'UID=supginseng;' + 'PWD=Ginseng@;' + 'PORT=1433' + ) + + conn = pyodbc.connect(connection_string) + cursor = conn.cursor() + + # Buscar o token da tabela dbo.rgb_token com id = 1 + query = "SELECT token FROM dbo.rgb_token WHERE id = 1" + cursor.execute(query) + result = cursor.fetchone() + + conn.close() + + if result: + print("✅ Token obtido do banco de dados com sucesso!") + return result[0] + else: + print("❌ Token não encontrado na tabela dbo.rgb_token com id = 1") + return None + + except Exception as e: + print(f"❌ Erro ao buscar token sem SSL com {driver}: {e}") + continue + + print("❌ Não foi possível conectar ao banco para buscar o token") + return None + + +def main(): + """Função principal do script""" + + # Buscar token de autenticação do banco de dados + print("🔐 Obtendo token de autenticação...") + BEARER_TOKEN = get_bearer_token_from_database() + + if not BEARER_TOKEN: + print("❌ Erro: Não foi possível obter o token de autenticação do banco de dados") + print("💡 Verifique se:") + print(" - A tabela dbo.rgb_token existe") + print(" - Existe um registro com id = 1") + print(" - A coluna 'token' contém um valor válido") + return + + # ======================================== + # CONFIGURAÇÃO DE DATA + # ======================================== + # USE_DATE_RANGE: + # True = Processar intervalo de datas (START_DATE até END_DATE) + # False = Usar data única (ontem ou MANUAL_DATE) + USE_DATE_RANGE = False + + # Intervalo de datas (usado quando USE_DATE_RANGE = True) + START_DATE = "2026-01-22" + END_DATE = "2026-01-22" + + # USE_MANUAL_DATE: + # True = Usar MANUAL_DATE + # False = Usar data de ONTEM automaticamente + USE_MANUAL_DATE = False + MANUAL_DATE = "2025-09-25" + + # Criar cliente da API + api_client = BoticarioAPI(BEARER_TOKEN) + + # Verificar drivers ODBC antes de começar + has_drivers = api_client.check_and_install_odbc_driver() + if not has_drivers: + print("\n⚠️ Pulando inserção no banco de dados devido à falta de drivers ODBC") + print(" Execute as instruções acima para instalar o driver e execute o script novamente") + return + + if USE_DATE_RANGE: + # ======================================== + # MODO INTERVALO DE DATAS + # ======================================== + from datetime import timedelta + + start_dt = datetime.strptime(START_DATE, "%Y-%m-%d") + end_dt = datetime.strptime(END_DATE, "%Y-%m-%d") + total_days = (end_dt - start_dt).days + 1 + + print("\n" + "="*60) + print("🚀 SCRIPT DE ACESSO À API DO GRUPO BOTICÁRIO") + print("="*60) + print(f"📅 MODO: Intervalo de datas") + print(f"📅 De: {START_DATE} até {END_DATE}") + print(f"📅 Total de dias: {total_days}") + print("="*60) + + # Estatísticas globais + total_stats = { + "dias_processados": 0, + "dias_com_erro": 0, + "total_recibos": 0, + "total_inseridos": 0 + } + + current_dt = start_dt + day_num = 0 + + while current_dt <= end_dt: + day_num += 1 + SALE_DATE = current_dt.strftime("%Y-%m-%d") + + print(f"\n{'='*60}") + print(f"📆 DIA {day_num}/{total_days}: {SALE_DATE}") + print(f"{'='*60}") + + # Buscar TODOS os recibos (com paginação automática) + receipts_data = api_client.get_all_sale_receipts(SALE_DATE) + + if receipts_data: + items = receipts_data.get('items', []) + total_stats["total_recibos"] += len(items) + + print(f"✅ {len(items)} recibos obtidos") + + if len(items) > 0: + # Inserir dados no banco de dados + success = api_client.insert_receipts_to_database(receipts_data, skip_duplicates=False) + + if success: + total_stats["dias_processados"] += 1 + print(f"✅ Dados inseridos com sucesso!") + else: + total_stats["dias_com_erro"] += 1 + print(f"❌ Erro ao inserir dados") + else: + total_stats["dias_processados"] += 1 + print(f"ℹ️ Nenhum recibo para esta data") + else: + total_stats["dias_com_erro"] += 1 + print(f"❌ Erro ao buscar dados da API") + + # Próximo dia + current_dt += timedelta(days=1) + + # Resumo final + print(f"\n{'='*60}") + print("🎉 PROCESSAMENTO DO INTERVALO CONCLUÍDO!") + print(f"{'='*60}") + print(f"📅 Período: {START_DATE} até {END_DATE}") + print(f"📊 Dias processados: {total_stats['dias_processados']}/{total_days}") + print(f"❌ Dias com erro: {total_stats['dias_com_erro']}") + print(f"📦 Total de recibos: {total_stats['total_recibos']}") + print(f"{'='*60}") + + else: + # ======================================== + # MODO DATA ÚNICA (comportamento original) + # ======================================== + from datetime import timedelta + + if USE_MANUAL_DATE: + SALE_DATE = MANUAL_DATE + print("📅 CONFIGURAÇÃO: Data manual ativada") + print(f"📅 Data selecionada: {SALE_DATE}") + else: + # Usar data de ONTEM + SALE_DATE = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + print("📅 CONFIGURAÇÃO: Data de ontem automática") + print(f"📅 Data selecionada: {SALE_DATE} (ontem)") + + print("\n" + "="*60) + print("🚀 SCRIPT DE ACESSO À API DO GRUPO BOTICÁRIO") + print("="*60) + print(f"🎯 Buscando recibos de vendas para a data: {SALE_DATE}") + print("-" * 60) + + # Buscar TODOS os recibos (com paginação automática) + receipts_data = api_client.get_all_sale_receipts(SALE_DATE) + + if receipts_data: + print("\n✅ Dados obtidos com sucesso!") + + if isinstance(receipts_data, dict): + print(f"\n📊 Resumo dos dados obtidos da API:") + print(f" - Start: {receipts_data.get('start', 'N/A')}") + print(f" - Count: {receipts_data.get('count', 'N/A')}") + print(f" - Total: {receipts_data.get('total', 'N/A')}") + + items = receipts_data.get('items', []) + print(f" - Items obtidos: {len(items)}") + + if receipts_data.get('total', 0) == len(items): + print(" - Status: ✅ Todos os registros obtidos da API") + else: + print(" - Status: ⚠️ Alguns registros podem estar faltando") + + print(" - Destino: 🗄️ Direto para o banco de dados (sem arquivo local)") + + # Inserir dados no banco de dados + print("\n" + "="*60) + print("🗄️ PROCESSAMENTO DO BANCO DE DADOS") + print("="*60) + print("🔄 Estratégia: Limpar dados existentes + Inserir novos dados") + print("📦 Inserção otimizada em lotes de 1000 registros") + + success = api_client.insert_receipts_to_database(receipts_data, skip_duplicates=False) + + if success: + print("\n🎉 PROCESSO COMPLETO!") + print("✅ Dados antigos removidos e novos dados inseridos com sucesso!") + else: + print("\n❌ PROCESSO FALHOU!") + print("❌ Falha durante o processamento do banco de dados") + + else: + print("\n❌ Não foi possível obter os dados da API") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/rgb_token_client.py b/rgb_token_client.py new file mode 100644 index 0000000..fcc9835 --- /dev/null +++ b/rgb_token_client.py @@ -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() diff --git a/ruptura.py b/ruptura.py new file mode 100644 index 0000000..f1f099e --- /dev/null +++ b/ruptura.py @@ -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}")