From 871738536aea8d9ede964b3548020176a6c5fc13 Mon Sep 17 00:00:00 2001 From: darlimcunha-07 Date: Tue, 25 Aug 2026 16:45:24 -0300 Subject: [PATCH] =?UTF-8?q?Crian=C3=A7=C3=A3o=20da=20nova=20draft=5Fv2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + Banco/db.py | 41 ++ draft_mar/draft_mar.py | 831 ++++++++++++++++++++++++++++++++++++++ draft_mar/draft_mar_v2.py | 592 +++++++++++++++++++++++++++ 4 files changed, 1467 insertions(+) create mode 100644 .gitignore create mode 100644 Banco/db.py create mode 100644 draft_mar/draft_mar.py create mode 100644 draft_mar/draft_mar_v2.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cff5543 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +__pycache__/ +*.pyc diff --git a/Banco/db.py b/Banco/db.py new file mode 100644 index 0000000..f51c667 --- /dev/null +++ b/Banco/db.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +import os +import pyodbc +from dotenv import load_dotenv + +load_dotenv() + +DB_DRIVER = os.getenv("DB_DRIVER", "{ODBC Driver 17 for SQL Server}") +DB_SERVER = os.getenv("DB_SERVER") +DB_DATABASE = os.getenv("DB_DATABASE") +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") + + +def get_connection(): + """Cria e retorna uma conexao com o banco de dados SQL Server, usando as credenciais definidas no .env.""" + faltantes = [ + nome for nome, valor in ( + ("DB_SERVER", DB_SERVER), + ("DB_DATABASE", DB_DATABASE), + ("DB_USER", DB_USER), + ("DB_PASSWORD", DB_PASSWORD), + ) + if not valor + ] + if faltantes: + raise ValueError( + f"Variaveis de ambiente ausentes no .env: {', '.join(faltantes)}" + ) + + connection_string = ( + f"DRIVER={DB_DRIVER};" + f"SERVER={DB_SERVER};" + f"DATABASE={DB_DATABASE};" + f"UID={DB_USER};" + f"PWD={DB_PASSWORD};" + f"TrustServerCertificate=yes;" + ) + + return pyodbc.connect(connection_string) diff --git a/draft_mar/draft_mar.py b/draft_mar/draft_mar.py new file mode 100644 index 0000000..766cff1 --- /dev/null +++ b/draft_mar/draft_mar.py @@ -0,0 +1,831 @@ +# -*- coding: utf-8 -*- + +import os +import sys +import requests +import json +import socket + +sys.path.append(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Banco")) +from db import get_connection +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", "23703", "23708","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" +} + +# Conexão com o banco de dados (credenciais via db.py) +conn = get_connection() + +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/draft_mar/draft_mar_v2.py b/draft_mar/draft_mar_v2.py new file mode 100644 index 0000000..e551780 --- /dev/null +++ b/draft_mar/draft_mar_v2.py @@ -0,0 +1,592 @@ +# -*- coding: utf-8 -*- + +import os +import sys +import json +import time +import socket +import requests +from dotenv import load_dotenv, set_key + +sys.path.append(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Banco")) +from db import get_connection +from requests.exceptions import RequestException +from datetime import datetime, timezone, timedelta + +ENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env") +load_dotenv(ENV_PATH) + +# URL da nova API (build-draft-v2) +BASE_URL = "https://mar-api-gateway.prd.demanda-abastecimento.app.grupoboticario.com.br/orders-bff/api/draft/build-draft-v2" +PAGE_SIZE = 25 + +# API interna que guarda o token Bearer atualizado (renovado por outro processo) +TOKENS_API_URL = "https://api.grupoginseng.com.br/api/tokens" + +# Token de autenticacao (Bearer) - expira em ~1h. Atualizado automaticamente via TOKENS_API_URL +# no inicio de cada execucao (funcao refresh_bearer_token). O valor no .env serve de fallback +# caso essa API esteja indisponivel. +BEARER_TOKEN = os.getenv("MAR_BEARER_TOKEN") + + +def build_authorization_header(token): + """Garante o prefixo 'Bearer ' no header, aceitando token com ou sem prefixo ja incluso""" + if not token: + return None + token = token.strip() + return token if token.lower().startswith("bearer ") else f"Bearer {token}" + + +HEADERS = { + "accept": "*/*", + "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", + "authorization": build_authorization_header(BEARER_TOKEN), + "content-type": "application/json", + "origin": "https://abastecimentogb.grupoboticario.com.br", + "referer": "https://abastecimentogb.grupoboticario.com.br/", + "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" +} + + +def refresh_bearer_token(): + """Busca o token Bearer mais recente em TOKENS_API_URL, salva no .env e atualiza HEADERS""" + global BEARER_TOKEN + + response = requests.get(TOKENS_API_URL, timeout=15) + response.raise_for_status() + body = response.json() + + registros = body.get("data", []) + if not registros: + raise ValueError("Resposta de TOKENS_API_URL nao contem registros em 'data'.") + + registro_mais_recente = max(registros, key=lambda r: r.get("updated_at", "")) + token_value = registro_mais_recente.get("token") + if not token_value: + raise ValueError("Registro mais recente de TOKENS_API_URL nao contem o campo 'token'.") + + set_key(ENV_PATH, "MAR_BEARER_TOKEN", token_value) + + BEARER_TOKEN = token_value + HEADERS["authorization"] = build_authorization_header(BEARER_TOKEN) + return BEARER_TOKEN + +# Lista de codigos 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", "23703", "23475", "23156", "14668", "24253", + "24254", "24255", "24258", "24257", "24268", "24269", "24293", "23813", "24449", "24450", "24455", "24458", "24454", "24447", "24448", "24451", "24456", "24457", "24453", "24452" +] + +# storeType da API v2 e definido pelo CANAL e pela MARCA ABR do PDV em base_pdvs: +# - PDV dedicado de marca propria (MARCA ABR = QDB - Quem Disse, Berenice?; EUD - Eudora; OUI - O.U.I, +# ex: PDVs 910173/910291) -> storeType = a propria sigla da marca; a API retorna o catalogo inteiro +# desse PDV sem precisar de filtro de marca (brandGroupCode None). Confirmado via DevTools so para QDB; +# EUD/OUI seguem por analogia (nenhum PDV assim existe em base_pdvs ainda). +# - Demais PDVs (marca BOT, vazio, etc.) -> storeType pelo CANAL: 'LJ' -> "BOT"; qualquer outro canal +# (VD, HIB, CD, ...) -> "VDF". +# +# Alem do PDV dedicado, BOT/EUD/OUI/QDB tambem podem ser vendidos DENTRO de um mesmo PDV comum, via o +# filtro filters.brandGroupCode (confirmado no DevTools, tela de filtro "Marcas" do MAR): +# - PDV tipo "BOT" (loja fisica, canal LJ): so vende marca BOT -> uma chamada com brandGroupCode ["BOT"]. +# - PDV tipo "VDF" (VD/HIB/CD): vende as 4 marcas dentro do mesmo PDV -> uma chamada por marca +# (["BOT"], ["EUD"], ["OUI"], ["QDB"]), resultados somados na mesma loja/dia. +# - PDV dedicado de marca propria ("QDB"/"EUD"/"OUI"): uma unica chamada sem filtro (brandGroupCode None). +MARCAS_PROPRIAS = {"QDB", "EUD", "OUI"} + +_store_type_cache = {} + + +def get_store_type(store_code, cursor): + """Consulta CANAL/MARCA ABR do PDV em base_pdvs e retorna o storeType para a API v2.""" + if store_code in _store_type_cache: + return _store_type_cache[store_code] + + cursor.execute("SELECT CANAL, [MARCA ABR] FROM base_pdvs WHERE PDV = ?", (store_code,)) + row = cursor.fetchone() + if not row or not row[0]: + raise ValueError( + f"Loja {store_code} nao encontrada (ou sem CANAL) na tabela base_pdvs." + ) + + canal = row[0].strip().upper() + marca_abr = (row[1] or "").strip().upper() + + if marca_abr in MARCAS_PROPRIAS: + store_type = marca_abr + else: + store_type = "BOT" if canal == "LJ" else "VDF" + + _store_type_cache[store_code] = store_type + return store_type + + +def get_brand_filters(store_type): + """Lista de valores de filters.brandGroupCode a percorrer para um dado storeType.""" + if store_type == "VDF": + return [["BOT"], ["EUD"], ["OUI"], ["QDB"]] + if store_type in MARCAS_PROPRIAS: + return [None] + return [["BOT"]] + + +def check_internet_connection(): + """Verifica se ha conexao com a internet""" + try: + socket.gethostbyname("google.com") + return True + except socket.error: + return False + + +def create_draft_v2_table(cursor, conn): + """Cria a tabela Draft_mar_2026_v2 se ela nao existir""" + try: + cursor.execute(""" + IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Draft_mar_2026_v2' AND schema_id = SCHEMA_ID('dbo')) + BEGIN + CREATE TABLE dbo.Draft_mar_2026_v2 ( + id INT IDENTITY(1,1) PRIMARY KEY, + date DATE DEFAULT CAST(GETDATE() AS DATE), + loja_id VARCHAR(50), + + metadata_id VARCHAR(50), + metadata_savedAt VARCHAR(50), + metadata_isDraftReplace VARCHAR(10), + thirdToLastCycle VARCHAR(5), + thirdToLastCycleEud VARCHAR(5), + secondToLastCycle VARCHAR(5), + secondToLastCycleEud VARCHAR(5), + lastCycle VARCHAR(5), + lastCycleEud VARCHAR(5), + currentCycle VARCHAR(5), + currentCycleEud VARCHAR(5), + nextCycle VARCHAR(5), + nextCycleEud VARCHAR(5), + secondToNextCycle VARCHAR(5), + secondToNextCycleEud VARCHAR(5), + + productId VARCHAR(50), + productIndex INT, + code VARCHAR(50), + description VARCHAR(255), + launch VARCHAR(50), + deactivation VARCHAR(50), + suggestionType VARCHAR(50), + discountPercent DECIMAL(18, 2), + + criticalItem_isCritical VARCHAR(10), + criticalItem_blockedWallet VARCHAR(10), + criticalItem_dtProvidedRegularization VARCHAR(50), + + sales_thirdToLastCycleSales INT, + sales_secondToLastCycleSales INT, + sales_lastCycleSales INT, + sales_currentCycleSales INT, + sales_nextCycleProjection INT, + sales_secondToNextCycleProjection INT, + + stock_actual INT, + stock_inTransit INT, + stock_safetyStockQuantity INT, + stock_syncSafetyStock VARCHAR(10), + stock_codeMaterial VARCHAR(50), + + cyclesPromotion NVARCHAR(MAX), + + strategy_currentCycleStrategy_cycle VARCHAR(5), + strategy_currentCycleStrategy_isIAF VARCHAR(10), + strategy_currentCycleStrategy_isFoco VARCHAR(10), + strategy_nextCycleStrategy_cycle VARCHAR(5), + strategy_nextCycleStrategy_isIAF VARCHAR(10), + strategy_nextCycleStrategy_isFoco VARCHAR(10), + strategy_secondToNextCycleStrategy_cycle VARCHAR(5), + strategy_secondToNextCycleStrategy_isIAF VARCHAR(10), + strategy_secondToNextCycleStrategy_isFoco VARCHAR(10), + + quantity INT, + analyzed VARCHAR(10), + quantityPerBox INT, + projectionSuggestionValue INT, + projectionValue INT, + launchType VARCHAR(50), + pendingOrder INT, + hasCoverage VARCHAR(10), + weekSuggestionValue INT, + salesCurve VARCHAR(50), + segmentedSalesCurve VARCHAR(50), + typeOfPromotion VARCHAR(255), + priceSellin DECIMAL(18, 2), + discountValue DECIMAL(18, 2), + promotionTypeValue VARCHAR(255), + targetOfPromotion VARCHAR(255), + restockSuggestion INT, + brandGroupCode VARCHAR(50), + isProductDeactivated VARCHAR(10), + status VARCHAR(50), + horizonInputLabel VARCHAR(50), + daysWithoutSales INT, + vlrStockPlanogram DECIMAL(18, 2), + + historicSales NVARCHAR(MAX), + similar NVARCHAR(MAX), + comboItems NVARCHAR(MAX) + ) + END + """) + conn.commit() + print("Tabela Draft_mar_2026_v2 verificada/criada com sucesso.") + except Exception as e: + print(f"Erro ao criar tabela Draft_mar_2026_v2: {e}") + conn.rollback() + raise + + +def to_json_text(value): + """Serializa listas/objetos em texto JSON (ou None se vazio)""" + if value is None: + return None + return json.dumps(value, ensure_ascii=False) + + +def process_and_insert_data(store_code, response_data, cursor, conn): + """Processa os produtos de uma pagina da API v2 e insere na tabela Draft_mar_2026_v2""" + metadata = response_data.get("metadata", {}) + products = response_data.get("content", []) + + if not products: + return 0 + + registros = [] + for product in products: + critical_item = product.get("criticalItem", {}) or {} + sales = product.get("sales", {}) or {} + stock = product.get("stock", {}) or {} + strategy = product.get("strategy", {}) or {} + current_strategy = strategy.get("currentCycleStrategy", {}) or {} + next_strategy = strategy.get("nextCycleStrategy", {}) or {} + second_next_strategy = strategy.get("secondToNextCycleStrategy", {}) or {} + + registro = ( + store_code, + + metadata.get("id"), + metadata.get("savedAt"), + metadata.get("isDraftReplace"), + metadata.get("thirdToLastCycle"), + metadata.get("thirdToLastCycleEud"), + metadata.get("secondToLastCycle"), + metadata.get("secondToLastCycleEud"), + metadata.get("lastCycle"), + metadata.get("lastCycleEud"), + metadata.get("currentCycle"), + metadata.get("currentCycleEud"), + metadata.get("nextCycle"), + metadata.get("nextCycleEud"), + metadata.get("secondToNextCycle"), + metadata.get("secondToNextCycleEud"), + + product.get("id"), + product.get("index"), + product.get("code"), + product.get("description"), + product.get("launch"), + product.get("deactivation"), + product.get("suggestionType"), + product.get("discountPercent"), + + critical_item.get("isCritical"), + critical_item.get("blockedWallet"), + critical_item.get("dtProvidedRegularization"), + + sales.get("thirdToLastCycleSales"), + sales.get("secondToLastCycleSales"), + sales.get("lastCycleSales"), + sales.get("currentCycleSales"), + sales.get("nextCycleProjection"), + sales.get("secondToNextCycleProjection"), + + stock.get("actual"), + stock.get("inTransit"), + stock.get("safetyStockQuantity"), + stock.get("syncSafetyStock"), + stock.get("codeMaterial"), + + to_json_text(product.get("cyclesPromotion")), + + current_strategy.get("cycle"), + current_strategy.get("IsIAF"), + current_strategy.get("IsFoco"), + next_strategy.get("cycle"), + next_strategy.get("IsIAF"), + next_strategy.get("IsFoco"), + second_next_strategy.get("cycle"), + second_next_strategy.get("IsIAF"), + second_next_strategy.get("IsFoco"), + + product.get("quantity"), + product.get("analyzed"), + product.get("quantityPerBox"), + product.get("projectionSuggestionValue"), + product.get("projectionValue"), + product.get("launchType"), + product.get("pendingOrder"), + product.get("hasCoverage"), + product.get("weekSuggestionValue"), + product.get("salesCurve"), + product.get("segmentedSalesCurve"), + product.get("typeOfPromotion"), + product.get("priceSellin"), + product.get("discountValue"), + product.get("promotionTypeValue"), + product.get("targetOfPromotion"), + product.get("restockSuggestion"), + product.get("brandGroupCode"), + product.get("isProductDeactivated"), + product.get("status"), + product.get("horizonInputLabel"), + product.get("daysWithoutSales"), + product.get("vlrStockPlanogram"), + + to_json_text(product.get("historicSales")), + to_json_text(product.get("similar")), + to_json_text(product.get("comboItems")), + ) + registros.append(registro) + + sql = """ + INSERT INTO dbo.Draft_mar_2026_v2 ( + loja_id, + metadata_id, metadata_savedAt, metadata_isDraftReplace, + thirdToLastCycle, thirdToLastCycleEud, secondToLastCycle, secondToLastCycleEud, + lastCycle, lastCycleEud, currentCycle, currentCycleEud, + nextCycle, nextCycleEud, secondToNextCycle, secondToNextCycleEud, + productId, productIndex, code, description, launch, deactivation, suggestionType, discountPercent, + criticalItem_isCritical, criticalItem_blockedWallet, criticalItem_dtProvidedRegularization, + sales_thirdToLastCycleSales, sales_secondToLastCycleSales, sales_lastCycleSales, + sales_currentCycleSales, sales_nextCycleProjection, sales_secondToNextCycleProjection, + stock_actual, stock_inTransit, stock_safetyStockQuantity, stock_syncSafetyStock, stock_codeMaterial, + cyclesPromotion, + strategy_currentCycleStrategy_cycle, strategy_currentCycleStrategy_isIAF, strategy_currentCycleStrategy_isFoco, + strategy_nextCycleStrategy_cycle, strategy_nextCycleStrategy_isIAF, strategy_nextCycleStrategy_isFoco, + strategy_secondToNextCycleStrategy_cycle, strategy_secondToNextCycleStrategy_isIAF, strategy_secondToNextCycleStrategy_isFoco, + quantity, analyzed, quantityPerBox, projectionSuggestionValue, projectionValue, launchType, + pendingOrder, hasCoverage, weekSuggestionValue, salesCurve, segmentedSalesCurve, + typeOfPromotion, priceSellin, discountValue, promotionTypeValue, targetOfPromotion, + restockSuggestion, brandGroupCode, isProductDeactivated, status, horizonInputLabel, + daysWithoutSales, vlrStockPlanogram, + historicSales, similar, comboItems + ) VALUES ( + ?, + ?, ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, + ?, ?, ?, + ?, ?, ?, + ?, ?, ?, ?, ?, + ?, + ?, ?, ?, + ?, ?, ?, + ?, ?, ?, + ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, + ?, ?, ? + ) + """ + cursor.executemany(sql, registros) + conn.commit() + return len(registros) + + +def fetch_draft_page(store_code, store_type, brand_group_code, page, cursor, conn): + """Busca uma pagina do draft (loja + marca) e insere no banco. Retorna (inseridos, ultima_pagina)""" + params = { + "draftType": "SEM", + "page": page, + "sort": "description,asc", + } + payload = { + "storeCode": store_code, + "storeType": store_type, + "size": PAGE_SIZE, + "filters": { + "promotion": None, + "purchaseSuggestion": None, + "codeProductLine": None, + "brandGroupCode": brand_group_code, + "analyzed": None, + "isCritical": None, + "isFoco": None, + "isIaf": None, + "itemStatus": None, + "search": "", + "subCategoryCode": None, + }, + "purchasingHorizon": "WEEK", + } + + response = requests.post(BASE_URL, headers=HEADERS, params=params, json=payload) + if response.status_code in (401, 403): + print(f"Token expirado/invalido (status {response.status_code}) na loja {store_code}. Renovando token...") + refresh_bearer_token() + response = requests.post(BASE_URL, headers=HEADERS, params=params, json=payload) + response.raise_for_status() + response_data = response.json() + + inseridos = process_and_insert_data(store_code, response_data, cursor, conn) + return inseridos, response_data.get("last", True) + + +def fetch_store_draft(store_code, cursor, conn): + """Busca o draft de uma loja (todas as marcas aplicaveis e todas as paginas) e insere no banco""" + store_type = get_store_type(store_code, cursor) + brand_filters = get_brand_filters(store_type) + + # Remove apenas os registros de HOJE dessa loja (permite reprocessar no mesmo dia sem duplicar), + # preservando o historico de dias anteriores. + delete_sql = "DELETE FROM dbo.Draft_mar_2026_v2 WHERE loja_id = ? AND date = CAST(GETDATE() AS DATE)" + cursor.execute(delete_sql, (store_code,)) + conn.commit() + + total_inseridos = 0 + + for brand_group_code in brand_filters: + page = 0 + while True: + inseridos, ultima_pagina = fetch_draft_page( + store_code, store_type, brand_group_code, page, cursor, conn + ) + total_inseridos += inseridos + if ultima_pagina: + break + page += 1 + + return total_inseridos + + +def fetch_and_insert_data(store_code, index, total, cursor, conn, retry_count=1): + """Baixa os dados da API v2 e insere diretamente no banco, com retentativa""" + attempt = 0 + while attempt < retry_count: + try: + num_produtos = fetch_store_draft(store_code, cursor, conn) + print(f"{index}/{total}: Loja {store_code} processada com sucesso ({num_produtos} produtos inseridos).") + return True + except RequestException as e: + print(f"{index}/{total}: Erro na requisicao para a loja {store_code}: {e}") + except ValueError as e: + print(f"{index}/{total}: {e}") + return False + 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} apos {retry_count} tentativas.") + return False + + +def update_api_status(status): + """Atualiza o status da API""" + try: + url_status = "https://api.grupoginseng.com.br/api/status/1" + 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_status = {"Content-Type": "application/json"} + 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(): + """Funcao principal""" + if not check_internet_connection(): + print("Sem conexao com a internet. Verifique sua conexao e tente novamente.") + update_api_status("FAIL") + return + + try: + refresh_bearer_token() + print("Token Bearer atualizado automaticamente via TOKENS_API_URL.") + except Exception as e: + print(f"Nao foi possivel atualizar o token automaticamente ({e}). Usando o token atual do .env, se houver.") + + if not BEARER_TOKEN: + print("MAR_BEARER_TOKEN nao definido no .env. Atualize o token antes de rodar o script.") + update_api_status("FAIL") + return + + conn = get_connection() + cursor = conn.cursor() + + try: + create_draft_v2_table(cursor, conn) + + failed_stores = [] + total = len(store_codes) + + for index, store_code in enumerate(store_codes, start=1): + success = fetch_and_insert_data(store_code, index, total, cursor, conn) + if not success: + failed_stores.append(store_code) + + 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, conn) + 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) + + if failed_stores: + print(f"\nNao foi possivel processar algumas lojas: {failed_stores}") + else: + print("\nTodos os arquivos foram processados com sucesso.") + + update_api_status("FAIL" if failed_stores else "OK") + + except Exception as e: + print(f"Erro durante a execucao: {e}") + update_api_status("FAIL") + finally: + cursor.close() + conn.close() + print("\nProcesso finalizado.") + + +if __name__ == "__main__": + main()