import smtplib import ssl import pyodbc import configparser import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt import matplotlib.dates as mdates from email.message import EmailMessage from email.utils import make_msgid from pathlib import Path from datetime import datetime, time from email.mime.image import MIMEImage hoje = datetime.today().strftime("%d/%m/%Y") config = configparser.ConfigParser() config.read(r"C:\Users\joao.herculano\Documents\Enviador de email\credenciais.ini") conn = pyodbc.connect( f"DRIVER={{SQL Server}};" f"SERVER={config['banco']['host']},1433;" f"DATABASE=GINSENG;" f"UID={config['banco']['user']};" f"PWD={config['banco']['password']}" ) calendario = pd.read_excel(r"C:\Users\joao.herculano\GRUPO GINSENG\Assistência Suprimentos - 2025\SUPRIMENTOS\BD_LANÇAMENTOS\BASE DE DADOS LANÇAMENTO\BOT\CICLO 9\CALENDARIO_CICLO\Ciclo_Expandido_com_Datas.xlsx") calendario.columns = calendario.columns.str.lower() calendario['date'] = pd.to_datetime(calendario['date']) today = pd.Timestamp("today").normalize() calendario = calendario[calendario['marca'] == "BOTICARIO"] calendario['num_ciclo'] = calendario['ciclo'].str[-2:].astype(int) calendario['ano_ciclo'] = calendario['ciclo'].str[0:5] calendario['ciclomais2'] = calendario['ano_ciclo'].astype(str) + (calendario['num_ciclo'] + 0).astype(str).str.zfill(2) ciclo_mais2 = calendario[calendario['date'].dt.normalize() == today]['ciclomais2'].iloc[0] filtered_calendario = calendario[calendario['ciclo'] == ciclo_mais2][:1].copy() filtered_calendario['dias_ate_fim'] = (filtered_calendario['fim ciclo'].iloc[0] - today).days print(filtered_calendario[['duração', 'dias_ate_fim']]) query = ''' SELECT businessunit AS marca, codcategory AS categoria, loja_id AS pdv, code AS sku, description AS descricao_produto, salescurve AS curva, CASE WHEN promotions_description IS NULL THEN 'REGULAR' ELSE 'PROMOÇÃO' END AS tipo_promocao, COALESCE(stock_actual, 0) AS estoque, stock_intransit AS transito, pendingorder AS pendente, nextcycleprojection AS pv_mar, currentcyclesales AS venda_atual, CASE WHEN criticalitem_iscritical = 0 THEN 'REGULAR' ELSE 'CRITICO' END AS status_item, ( COALESCE(thirdtolastcyclesales, 0) + COALESCE(secondtolastcyclesales, 0) + COALESCE(lastcyclesales, 0) + COALESCE(nextcycleprojection, 0) ) / 4.0 AS media_vendas FROM Draft WHERE isproductdeactivated = 0 AND codcategory NOT IN ('SUPORTE A VENDA','EMBALAGENS') ''' df = pd.read_sql(query, conn) conn.close() df.columns = df.columns.str.lower() filtered_calendario.columns = filtered_calendario.columns.str.lower() df['ddv'] = df['pv_mar'] / filtered_calendario['duração'].values[0] df['estoque_seguranca'] = np.ceil((df['media_vendas']/20)*14 - (df['ddv']*14)).astype(int) #media de vendas realizada+projetada df['estoque_seguranca'] = np.where(df['estoque_seguranca'] <1,1,df['estoque_seguranca']) df['risco_ruptura'] = np.where(df['estoque_seguranca'] > df['estoque'], "SIM", "NÃO") df['quantidade_ruptura'] = df['estoque_seguranca'] - df['estoque'] df['excesso'] = np.where(df['estoque'] - df['estoque_seguranca'] > 0, df['estoque'] - df['estoque_seguranca'], 0) remetente = config['credenciais']['remetente'] senha = config['credenciais']['senha'] destinatarios = [email.strip() for email in config['email_ruptura']['destinatarios'].split(',')] assunto = config['email_ruptura']['assunto'] df_rpt = pd.read_excel(r"C:\Users\joao.herculano\Downloads\Ruptura Cliente CP GINSENG - 09.06.xlsx") df_rpt.columns = df_rpt.columns.str.lower() df['pdv'] = df['pdv'].astype('Int64') df['sku'] = df['sku'].astype('Int64') df_rpt['cod_pdv'] = df_rpt['cod_pdv'].astype('Int64') df_rpt['sku1'] = df_rpt['sku1'].astype('Int64') df = pd.merge(df, df_rpt[['sku1','cod_pdv','estoque livre?']], left_on=['pdv','sku'], right_on=['cod_pdv','sku1'], how='left') df.drop(columns=['cod_pdv'], inplace=True) pdvs = pd.read_excel(r"C:\Users\joao.herculano\Documents\PDV_ATT.xlsx") pdvs.columns = pdvs.columns.str.lower() df['pdv'] = df['pdv'].astype('Int64') pdvs['pdv'] = pdvs['pdv'].astype('Int64') df2 = pd.merge(df, pdvs[['pdv','uf','canal','analista']], on='pdv', how='inner') idx = df2.groupby(['uf', 'sku'])['excesso'].idxmax() pdvs_maior_excesso = df2.loc[idx, ['uf', 'sku', 'pdv', 'excesso']].copy() pdvs_maior_excesso.columns = ['uf', 'sku', 'pdv_maior_excesso', 'maior_excesso_por_uf'] pdvs_maior_excesso.set_index(['uf', 'sku'], inplace=True) df2 = df2.join(pdvs_maior_excesso, on=['uf', 'sku']) df2['maior excesso na uf'] = df2['maior_excesso_por_uf'].apply(lambda x: 'não tem excesso no uf' if x == 0 else None) df2['maior excesso na uf'] = df2['maior excesso na uf'].combine_first(df2['pdv_maior_excesso']) df2['quantidade_ruptura'] = df2['quantidade_ruptura'].clip(lower=0) df2.drop(columns=['pdv_maior_excesso','sku1'], inplace=True) df2['estoque livre?'] = np.where( df2['estoque livre?'].isna() & (df2['status_item'] == 'CRITICO'), 'Não', df2['estoque livre?'] ) df2['estoque livre?'] = np.where( df2['estoque livre?'].isna() & (df2['status_item'] == 'REGULAR'), 'Sim', df2['estoque livre?'] ) excel_path = Path("relatorio.xlsx") colunas_ordenadas = [ 'uf', 'canal', 'analista', 'marca', 'categoria', 'pdv', 'sku', 'descricao_produto', 'curva', 'tipo_promocao', 'estoque', 'transito', 'pendente', 'pv_mar', 'venda_atual', 'status_item', 'ddv', 'estoque_seguranca', 'risco_ruptura', 'quantidade_ruptura', 'excesso', 'estoque livre?', 'maior_excesso_por_uf', 'maior excesso na uf' ] df2 = df2[colunas_ordenadas] df3 = df2[df2['canal'] != "LJ"] df3 = df3.groupby(['uf', 'canal','pdv'])['quantidade_ruptura'].sum().sort_values(ascending=False).reset_index() df4 = df3.groupby('uf', as_index=False)['quantidade_ruptura'].sum() df4['data'] = hoje df4['data'] = pd.to_datetime(df4['data'], dayfirst=True) path2 = r'C:\Users\joao.herculano\OneDrive - GRUPO GINSENG\Documentos\acompanhamentos\estudo ruptura\AcompanhamentoRuptura.xlsx' # Tenta abrir e escrever com append with pd.ExcelWriter(path2, mode='a', engine='openpyxl', if_sheet_exists='overlay') as writer: # Encontra a última linha preenchida book = writer.book sheet = writer.sheets['Sheet1'] if 'Sheet1' in writer.sheets else writer.book.active start_row = sheet.max_row # Escreve sem cabeçalho se não for a primeira linha df4.to_excel(writer, index=False, header=not start_row > 1, startrow=start_row) de_effi = pd.read_excel(r"C:\Users\joao.herculano\OneDrive - GRUPO GINSENG\Documentos\acompanhamentos\estudo ruptura\AcompanhamentoRuptura.xlsx") de_effi['data'] = pd.to_datetime(de_effi['data'], errors='coerce') # Step 2: Group and sort grouped = ( de_effi.groupby('data')['quantidade_ruptura'] .sum() .sort_index() ) # Step 3: Plot plt.figure(figsize=(12, 6)) plt.plot(grouped.index, grouped.values, marker='o', linestyle='-', color='cornflowerblue', linewidth=2) # Step 4: Format numbers with dot ('.') separator, no decimals for x, y in zip(grouped.index, grouped.values): label = f"{y:,.0f}".replace(",", ".") # format like 75.063 plt.text(x, y + max(grouped.values) * 0.015, label, ha='center', fontsize=9) # Step 5: Format chart plt.title('Evolução de Quantidade de Ruptura por Data', fontsize=14) plt.xlabel('Data', fontsize=8) plt.ylabel('Quantidade de Ruptura', fontsize=12) plt.grid(True, linestyle='--', alpha=0.6) # ✅ Set xticks to match the actual data points ax = plt.gca() ax.set_xticks(grouped.index) ax.xaxis.set_major_formatter(mdates.DateFormatter('%d/%m/%Y')) plt.xticks(rotation=45) # Format x-axis as dd/mm/yyyy plt.gca().set_xticklabels([d.strftime('%d/%m/%Y') for d in grouped.index]) plt.tight_layout() plt.savefig("grafico2.png") plt.close() with pd.ExcelWriter(excel_path, engine='openpyxl') as writer: df2.to_excel(writer, sheet_name='Detalhado', index=False) df3.to_excel(writer, sheet_name='Resumo', index=False) ruptura_total = df2['quantidade_ruptura'].sum() ruptura_por_uf_pct = ( df2.groupby('uf')['quantidade_ruptura'].sum() .sort_values(ascending=True) .apply(lambda x: (x / ruptura_total) * 100) ) print(ruptura_por_uf_pct) ax = ruptura_por_uf_pct.plot(kind='barh', figsize=(10, 6), color='skyblue') for i, v in enumerate(ruptura_por_uf_pct): ax.text(v + 0.3, i, f"{v:.1f}%", va='center') plt.xlabel('% da ruptura total') plt.title('Distribuição percentual de ruptura projetada por UF') plt.tight_layout() plt.savefig("grafico.png") plt.close() agora = datetime.now().time() if time(5, 0) <= agora <= time(12, 0): boa = "Bom dia!" elif time(12, 1) <= agora <= time(18, 0): boa = "Boa tarde!" else: boa = "Boa noite!" grafico_cid = make_msgid()[1:-1] grafico2_cid = make_msgid()[1:-1] msg = EmailMessage() msg['From'] = remetente msg['To'] = ', '.join(destinatarios) msg['Subject'] = assunto html_email = f"""
{boa}
Compartilhamos o relatório de ruptura projetada, com o objetivo de monitorar os itens com maior risco de ruptura nos próximos dias.
Atualizamos o relatório, e agora mandaremos diariamente às 10:00 automaticamente. Pedimos paciência nessa etapa e contamos com o feedback de vocês.
O relatório a seguir apresenta as seguintes informações:
Além disso, o material destaca as VDs com maior criticidade, permitindo uma atuação direcionada para mitigar impactos e priorizar ações de abastecimento e transferência.
Importante:
O relatório está em processo de desenvolvimento e pode sofrer mudanças futuras no layout. Ficamos à disposição para esclarecer quaisquer dúvidas.
Foram adicionados ao relatório os PDVs da região de Irecê e Jacobina.