Ruptura_Projetada/relatorio_ruptura/ruptura_projetada.py
2025-09-04 12:29:39 -03:00

299 lines
11 KiB
Python

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
import warnings
warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy")
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().tail(30)
)
# 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"""
<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><strong>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.</strong></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></p>
<p>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>
<p>Foram adicionados ao relatório os PDVs da região de Irecê e Jacobina.</p>
<img src="cid:{grafico_cid}">
<img src="cid:{grafico2_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("grafico2.png", 'rb') as img:
msg.get_payload()[1].add_related(img.read(), 'image', 'png', cid=grafico2_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.",hoje)
print("############################################################")