Compare commits

..

No commits in common. "446f0234258be1c40c6322d94bb57b78fedb791d" and "c7f31231aefcdde376b77278ef982f65aa31145e" have entirely different histories.

6 changed files with 0 additions and 7327 deletions

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

View File

@ -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 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.")

View File

@ -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.")