# enviar_email_excel.py import smtplib import ssl import pyodbc import configparser import pandas as pd import matplotlib.pyplot as plt import matplotlib.dates as mdates import matplotlib.ticker as mtick import seaborn as sns from email.message import EmailMessage from email.utils import make_msgid from email.mime.image import MIMEImage from pathlib import Path from datetime import datetime, time from openpyxl.styles import NamedStyle config = configparser.ConfigParser() config.read(r"relatório_improdutivo\credenciais.ini") print(config['banco']['host'],config['banco']['user'],config['banco']['password']) # Conexão com o banco 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']}") # 1. Criar dados fictícios e gerar Excel query = ''' select *, CASE WHEN dayswithoutsales BETWEEN 40 AND 59 THEN 'mais de 40 dias' WHEN dayswithoutsales BETWEEN 60 AND 79 THEN 'mais de 60 dias' WHEN dayswithoutsales BETWEEN 80 AND 99 THEN 'mais de 80 dias' WHEN dayswithoutsales >= 100 THEN 'acima de 100 dias' ELSE 'menos de 40 dias' end as status_venda, pricesellin * (stock_actual + stock_intransit) AS valor_estoque_parado FROM Draft where dayswithoutsales > 40 and stock_actual > 0 and isproductdeactivated <> 1 and currentcyclesales = 0 ''' df = pd.read_sql(query, conn) conn.close() remetente = config['credenciais']['remetente'] senha = config['credenciais']['senha'] destinatarios = [email.strip() for email in config['email']['destinatarios'].split(',')] assunto = config['email']['assunto'] print(remetente,senha,destinatarios,assunto) pdvs = pd.read_excel(r"relatório_improdutivo\PDV_ATT.xlsx") df['loja_id'] = df['loja_id'].astype('Int64') pdvs['PDV'] = pdvs['PDV'].astype('Int64') df2= pd.merge(left=df,right=pdvs[['PDV','UF']],left_on='loja_id',right_on='PDV',how='inner') # Dicionário de renomeação colunas_traduzidas = { "loja_id": "id_loja", "code": "código", "description": "descrição", "launch": "lançamento", "deactivation": "desativação", "thirdtolastcyclesales": "venda_terceiro_ciclo_passado", "secondtolastcyclesales": "venda_penúltimo_ciclo", "lastcyclesales": "venda_último_ciclo", "currentcyclesales": "venda_ciclo_atual", "nextcycleprojection": "projeção_próximo_ciclo", "secondtonextcycleprojection": "projeção_segundo_próximo_ciclo", "stock_actual": "estoque_atual", "stock_intransit": "estoque_em_transito", "purchasesuggestion": "sugestao_compra", "smartpurchase_purchasesuggestioncycle": "compra_inteligente_ciclo", "smartpurchase_nextcyclepurchasesuggestion": "compra_inteligente_prox_ciclo", "pendingorder": "pedido_pendente", "salescurve": "curva_vendas", "promotions_description": "descrição_promocao", "promotions_discountpercent": "desconto_promocao_percentual", "pricesellin": "preço_sellin", "businessunit": "unidade_negócio", "codcategory": "código_categoria", "criticalitem_dtprovidedregularization": "dt_regularização_item_critico", "criticalitem_blockedwallet": "carteira_bloqueada_item_critico", "criticalitem_iscritical": "é_item_critico", "codsubcategory": "código_subcategoria", "isproductdeactivated": "produto_desativado", "brandgroupcode": "código_grupo_marca", "dayswithoutsales": "dias_sem_venda", "coveragedays": "dias_cobertura", "hascoverage": "tem_cobertura" } # Renomeando as colunas do DataFrame df2 = df2.rename(columns=colunas_traduzidas) excel_path = Path("relatorio.xlsx") df2.to_excel(excel_path, index=False) # Gerar gráfico melhorado - Estoque parado por UF plot_df = df2.groupby('UF')['valor_estoque_parado'].sum().reset_index() sns.set_theme(style="whitegrid") plt.figure(figsize=(12, 7)) ax = sns.barplot( data=plot_df, x='UF', y='valor_estoque_parado', palette='crest', errorbar=None ) ax.yaxis.set_major_formatter(mtick.StrMethodFormatter('R${x:,.2f}')) # Anotar valores em cima das barras for p in ax.patches: valor = p.get_height() ax.annotate( f'R$ {valor:,.2f}', (p.get_x() + p.get_width() / 2, valor), ha='center', va='bottom', fontsize=10, fontweight='bold' ) plt.title("Estoque Parado por UF", fontsize=18, fontweight='bold') plt.ylabel("Valor em Reais", fontsize=13) plt.xlabel("UF", fontsize=13) plt.xticks(fontsize=11) plt.yticks(fontsize=11) sns.despine() plt.tight_layout() plt.savefig("grafico.png") plt.close() # Obtém a hora atual agora = datetime.now().time() hoje = datetime.today().strftime("%d/%m/%Y") # Define os intervalos de tempo manhã_inicio = time(5, 0) manhã_fim = time(12, 0) tarde_inicio = time(12, 1) tarde_fim = time(18, 0) # noite é dividida em dois intervalos por causa da virada do dia noite_inicio = time(18, 1) noite_fim = time(4, 59) # Verifica em qual intervalo a hora atual está if manhã_inicio <= agora <= manhã_fim: boa = "Bom dia!" elif tarde_inicio <= agora <= tarde_fim: boa = "Boa tarde!" else: boa = "Boa noite!" df2['DATA'] = hoje df3 = df2.groupby('DATA', as_index=False)['valor_estoque_parado'].sum() path2 = r'relatório_improdutivo\acompanhamento40DSV.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 # cria estilo de data date_style = NamedStyle(name="date_style", number_format="DD-MM-YYYY") for row in sheet.iter_rows(min_row=start_row+1, max_row=sheet.max_row, min_col=1, max_col=len(df3.columns)): for cell in row: if cell.column == df3.columns.get_loc("Data") + 1: # pega a coluna 'Data' cell.style = date_style book.save(path2) de_effi = pd.read_excel(r'relatório_improdutivo\acompanhamento40DSV.xlsx') de_effi['Data'] = pd.to_datetime(de_effi['Data'], errors='coerce') # Step 2: Group and sort grouped = ( de_effi.groupby('Data')['Valor'] .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 Estoque Improdutivo por Data', fontsize=14) plt.xlabel('Data', fontsize=8) plt.ylabel('Estoque Improdutivo', 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() # 3. Criar e-mail com imagem embutida grafico_cid = make_msgid()[1:-1] # remove < > grafico2_cid = make_msgid()[1:-1] msg = EmailMessage() msg['From'] = remetente msg['To'] = ', '.join(destinatarios) msg['Subject'] = assunto # 4. Conteúdo do e-mail html_email = f"""
{boa}
Segue o relatório semanal de estoque improdutivo referente às regiões de Alagoas (AL), Bahia (BA), Sergipe (SE), Vitória da Conquista (VDC), Jacobina e Iracê.
Este relatório contempla exclusivamente os itens que possuem saldo em estoque, mas que estão sem vendas há mais de 40 dias.
O objetivo é trazer visibilidade para os produtos parados e reforçar a importância de ações para estimular a sua saída, contribuindo assim para a redução da cobertura de estoque e otimização dos recursos.
Contamos com o apoio de todos para análise e tratativa dos itens listados. Sugestões de ações como campanhas, transferências ou ajustes de sortimento são bem-vindas para acelerar a movimentação dos produtos.
Para mais informações, favor consultar a planilha em anexo.
Segue resumo: