Ruptura_Projetada/relatório_ruptura/ruptura projetada 23.05.py
2025-06-03 09:42:55 -03:00

223 lines
8.1 KiB
Python

import smtplib
import ssl
import pyodbc
import configparser
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
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
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
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['pv_mar'] + (15 * df['ddv'])).astype(int)
df['risco_ruptura'] = np.where(df['ddv'] * filtered_calendario['dias_ate_fim'].max() >= df['estoque'], "SIM", "NÃO")
df['quantidade_ruptura'] = np.ceil(df['ddv'] * filtered_calendario['dias_ate_fim'].max() - 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 (1).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)
print(df2[['uf', 'sku', 'excesso', 'maior_excesso_por_uf', 'maior excesso na uf']].head())
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()
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)
print(excel_path)
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]
msg = EmailMessage()
msg['From'] = remetente
msg['To'] = ', '.join(destinatarios)
msg['Subject'] = assunto
html_email = f"""
<html>
<body>
<p>{boa}</p>
<p>Compartilhamos o relatório de ruptura projetada, com o objetivo de monitorar os itens com maior risco de ruptura nos próximos dias.</p>
<p>O relatório a seguir apresenta as seguintes informações:</p>
<ul>
<li>Estoque atual;</li>
<li>Trânsito;</li>
<li>Ruptura projetada;</li>
<li>Maior excesso por estado, assim como o PDV que o possui.</li>
</ul>
<p>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.</p>
<p><strong>Importante:</strong> O relatório está em processo de desenvolvimento e pode sofrer mudanças futuras no layout. Ficamos à disposição para esclarecer quaisquer dúvidas.</p>
<img src="cid:{grafico_cid}">
</body>
</html>
"""
msg.set_content("Seu e-mail precisa de um visualizador HTML.")
msg.add_alternative(html_email, subtype='html')
with open("grafico.png", 'rb') as img:
msg.get_payload()[1].add_related(img.read(), 'image', 'png', cid=grafico_cid)
with open(excel_path, 'rb') as f:
msg.add_attachment(
f.read(),
maintype='application',
subtype='vnd.openxmlformats-officedocument.spreadsheetml.sheet',
filename=excel_path.name
)
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.")