Ruptura_Projetada/relatório_improdutivo/relatório_improdutivo.py
2025-09-08 16:36:09 -03:00

314 lines
9.5 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), Jacobina e Iracê.
</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.")