Compare commits
No commits in common. "446f0234258be1c40c6322d94bb57b78fedb791d" and "c7f31231aefcdde376b77278ef982f65aa31145e" have entirely different histories.
446f023425
...
c7f31231ae
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,234 +0,0 @@
|
||||
# enviar_email_excel.py
|
||||
|
||||
import smtplib
|
||||
import ssl
|
||||
import pyodbc
|
||||
import configparser
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
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
|
||||
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(r"C:\Users\joao.herculano\Documents\Enviador de email\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"C:\Users\joao.herculano\Documents\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)
|
||||
|
||||
|
||||
# 2. Criar e salvar gráfico
|
||||
plot_df = df2.groupby('UF')['valor_estoque_parado'].sum().reset_index()
|
||||
plt.figure(figsize=(6, 4))
|
||||
ax = sns.barplot(data=plot_df, x='UF', y='valor_estoque_parado', errorbar=None)
|
||||
ax.yaxis.set_major_formatter(mtick.StrMethodFormatter('R${x:,.2f}'))
|
||||
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=9)
|
||||
plt.title("Estoque parado por UF")
|
||||
plt.ylabel("Valor em Reais")
|
||||
plt.xlabel("UF")
|
||||
plt.tight_layout()
|
||||
plt.savefig("grafico.png")
|
||||
plt.close()
|
||||
|
||||
# Obtém a hora atual
|
||||
agora = datetime.now().time()
|
||||
# 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'] = agora
|
||||
|
||||
df3 = df2.groupby('DATA', as_index=False)['valor_estoque_parado'].sum()
|
||||
|
||||
path2 = r'C:\Users\joao.herculano\OneDrive - GRUPO GINSENG\Documentos\acompanhamentos\40D sem Venda\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
|
||||
|
||||
# Escreve sem cabeçalho se não for a primeira linha
|
||||
df3.to_excel(writer, index=False, header=not start_row > 1, startrow=start_row)
|
||||
|
||||
# 3. Criar e-mail com imagem embutida
|
||||
grafico_cid = make_msgid()[1:-1] # remove < >
|
||||
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 aos estados de
|
||||
Alagoas (AL), Bahia (BA), Sergipe (SE) e região de Vitória da Conquista (VDC).
|
||||
</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>
|
||||
<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')
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
# 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.")
|
||||
@ -1,221 +0,0 @@
|
||||
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,
|
||||
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',
|
||||
'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)
|
||||
|
||||
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.")
|
||||
Loading…
x
Reference in New Issue
Block a user