314 lines
9.4 KiB
Python
314 lines
9.4 KiB
Python
# 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"""
|
|
<html>
|
|
<body>
|
|
<p>{boa}</p>
|
|
|
|
<p>
|
|
Segue o relatório semanal de estoque improdutivo referente às regiões de
|
|
Alagoas (AL), Bahia (BA), Sergipe (SE), Vitória da Conquista (VDC) e BA3.
|
|
</p>
|
|
|
|
<p>
|
|
Este relatório contempla exclusivamente os itens que possuem saldo em estoque,
|
|
mas que estão sem vendas há mais de 40 dias.
|
|
</p>
|
|
|
|
<p>
|
|
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.
|
|
</p>
|
|
|
|
<p>
|
|
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.
|
|
</p>
|
|
|
|
<p>
|
|
Para mais informações, favor consultar a planilha em anexo.
|
|
</p>
|
|
|
|
<p><b>Segue resumo:</b></p>
|
|
<p><img src="cid:{grafico_cid}"></p>
|
|
<p><img src="cid:{grafico2_cid}"></p>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
msg.set_content("Seu e-mail precisa de um visualizador HTML.")
|
|
msg.add_alternative(html_email, subtype='html')
|
|
|
|
# 4. Anexar gráfico inline
|
|
with open("grafico.png", 'rb') as img:
|
|
msg.get_payload()[1].add_related(img.read(), 'image', 'png', cid=grafico_cid)
|
|
|
|
with open("grafico2.png", 'rb') as img2:
|
|
msg.get_payload()[1].add_related(img2.read(), 'image', 'png', cid=grafico2_cid)
|
|
|
|
|
|
# 5. Anexar o arquivo Excel
|
|
with open(excel_path, 'rb') as f:
|
|
msg.add_attachment(
|
|
f.read(),
|
|
maintype='application',
|
|
subtype='vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
filename=excel_path.name
|
|
)
|
|
|
|
# 6. Enviar o e-mail via SMTP Outlook com configurações fornecidas
|
|
with smtplib.SMTP('smtp-mail.outlook.com', 587) as smtp:
|
|
smtp.ehlo()
|
|
smtp.starttls(context=ssl.create_default_context())
|
|
smtp.login(remetente, senha)
|
|
smtp.send_message(msg)
|
|
|
|
|
|
|
|
print("E-mail enviado com sucesso.")
|