innit 29.05

This commit is contained in:
João Herculano 2025-05-29 09:48:20 -03:00
parent c7f31231ae
commit b2fb8fd8e3
7 changed files with 5817 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,785 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "6ad35669",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np \n",
"import glob\n",
"import os "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9fcdc77a",
"metadata": {},
"outputs": [],
"source": [
"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\")\n",
"\n",
"calendario['Date'] = pd.to_datetime(calendario['Date'])\n",
"\n",
"# Get today (normalized to midnight)\n",
"today = pd.Timestamp(\"today\").normalize()\n",
"\n",
"calendario['NUM_CICLO'] = calendario['Ciclo'].str[-2:].astype(int)\n",
"\n",
"calendario['ANO_CICLO'] = calendario['Ciclo'].str[0:5]\n",
"\n",
"calendario = calendario[calendario['MARCA'] == \"BOTICARIO\"]\n",
"\n",
"calendario['CICLOMAIS2'] = calendario['ANO_CICLO'].astype(str) + (calendario['NUM_CICLO'].astype(int) + 3).astype(str).str.zfill(2) #<<< MUDAR O \"4\" (CICLO ATUAL + 4 PARA ACHAR O CICLO DA SUGESTÃO) EX: C202505 -> C202509\n",
"ciclo_mais2 = calendario[calendario['Date'].dt.normalize() == today]['CICLOMAIS2'].iloc[0]\n",
"\n",
"# Filter rows where date matches today\n",
"filtered_calendario = calendario[calendario['Ciclo'] == ciclo_mais2][:1]\n",
"\n",
"filtered_calendario['dias_ate_inicio'] = filtered_calendario['INICIO CICLO'].iloc[0] - today\n",
"\n",
"filtered_calendario['dias_ate_inicio'] = filtered_calendario['dias_ate_inicio'].dt.days.astype(int)\n",
"\n",
"filtered_calendario['match'] = 1\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bbec229d",
"metadata": {},
"outputs": [],
"source": [
"filtered_calendario"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "61ffc777",
"metadata": {},
"outputs": [],
"source": [
"df_similares = pd.read_excel(r\"C:\\Users\\joao.herculano\\GRUPO GINSENG\\Assistência Suprimentos - 2025\\SUPRIMENTOS\\BD_LANÇAMENTOS\\BOT\\BOT - C11\\arquivos para geração da sugestão\\SIMILARES\\PRODUTOS SIMILARES - BOT.xlsx\")\n",
"\n",
"df_similares = pd.merge(left=df_similares,right=calendario[['Ciclo','INICIO CICLO','FIM CICLO','DURAÇÃO']], how= 'left', left_on = 'CICLO SIMILAR',right_on = 'Ciclo' )\n",
"\n",
"df_similares = df_similares.drop(columns=['Ciclo'])\n",
"\n",
"df_similares = df_similares.rename(columns={'INICIO CICLO':'INICIO CICLO SIMILAR','FIM CICLO':'FIM CICLO SIMILAR','DURAÇÃO':'DURAÇÃO CICLO SIMILAR'})\n",
"df_similares.drop_duplicates(inplace=True)\n",
"\n",
"df_similares['MATCH'] = 1\n",
"\n",
"df_similares = df_similares.drop(columns=['INICIO DO CICLO',\n",
" 'FIM DO CICLO', 'DURAÇÃO CICLO','INICIO CICLO SIMILAR','FIM CICLO SIMILAR','DURAÇÃO CICLO SIMILAR'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "99ea95e6",
"metadata": {},
"outputs": [],
"source": [
"df_similares.columns"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fe922f62",
"metadata": {},
"outputs": [],
"source": [
"df_tabela = pd.read_excel(r\"C:\\Users\\joao.herculano\\GRUPO GINSENG\\Assistência Suprimentos - 2025\\SUPRIMENTOS\\BD_LANÇAMENTOS\\BOT\\BOT - C11\\arquivos para geração da sugestão\\TABELA DE PEDIDO\\Pedidos Semanais Especiais - BOT - 202511.xlsx\")\n",
"\n",
"df_tabela = df_tabela[df_tabela['Região'] == 'NNE'] \n",
"\n",
"df_tabela = df_tabela[(df_tabela['Canal'] != 'Ecomm') | (df_tabela['Canal'] != 'Ecomm | VD') | (df_tabela['Canal'] != 'Ecomm | Loja')] \n",
"\n",
"df_tabela['Canal'] = np.where((df_tabela['Canal'] == \"Loja\") | (df_tabela['Canal'] == \"Todos\") | (df_tabela['Canal'] == \"Loja | VD\"),\"TODOS\",\"VD\")\n",
"\n",
"df_tabela = df_tabela[df_tabela['Tipo de promoção'].str.contains('Lançamentos', na=False)]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a3a045d9",
"metadata": {},
"outputs": [],
"source": [
"df_pdv = pd.read_excel(r\"C:\\Users\\joao.herculano\\GRUPO GINSENG\\Assistência Suprimentos - 2025\\SUPRIMENTOS\\BD_LANÇAMENTOS\\BASE DE DADOS LANÇAMENTO\\BOT\\PDV\\PDV_ATT.xlsx\")\n",
"\n",
"df_pdv_origi = pd.read_excel(r\"C:\\Users\\joao.herculano\\GRUPO GINSENG\\Assistência Suprimentos - 2025\\SUPRIMENTOS\\BD_LANÇAMENTOS\\BASE DE DADOS LANÇAMENTO\\BOT\\PDV\\PDV_ATT.xlsx\")\n",
"\n",
"df_pdv = df_pdv.rename(columns={'DESCRIÇÃO':'DESCRIÇÃO PDV'})\n",
"\n",
"df_pdv = df_pdv.drop(columns=['REGIÃO', 'ESTADO','CIDADE','GESTÃO','MARCA'])\n",
"\n",
"df_pdv['PDV'] = df_pdv['PDV DESC'].str.split(\"-\").str[0].str.strip()\n",
"\n",
"df_pdv['UF'] = np.where(df_pdv['UF'] == 'VDC','BA',df_pdv['UF'])\n",
"\n",
"#ignorando a PDV que ainda não está online\n",
"df_pdv = df_pdv[df_pdv['DESCRIÇÃO PDV'] != '23813-COMERCIO-HIB VALENTE']\n",
"\n",
"df_pdv = df_pdv[df_pdv['status'] != \"INATIVO\"]\n",
"\n",
"df_pdv = df_pdv[df_pdv['status'] != \"MATRIZ\"]\n",
"\n",
"df_pdv = df_pdv[df_pdv['SUPERVISOR'] != 'Inativa']\n",
"\n",
"df_pdv['MATCH'] = 1"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "849d5297",
"metadata": {},
"outputs": [],
"source": [
"df_pdv.columns"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "df04a501",
"metadata": {},
"outputs": [],
"source": [
"df_similares = pd.merge(left=df_similares,right=df_pdv,right_on=['MATCH'],left_on=['MATCH'],how='inner')\n",
"\n",
"df_similares = df_similares.drop_duplicates()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0da911af",
"metadata": {},
"outputs": [],
"source": [
"# Caminho onde estão as subpastas com os arquivos CSV\n",
"\n",
"# Set the path to the folder containing CSV files\n",
"folder_path = r\"C:\\Users\\joao.herculano\\GRUPO GINSENG\\Assistência Suprimentos - 2025\\SUPRIMENTOS\\BD_LANÇAMENTOS\\BOT\\BOT - C11\\arquivos para geração da sugestão\\DRAFT\" # arquivo dos drafts\n",
"\n",
"# Pattern to match all CSV files\n",
"csv_files = glob.glob(os.path.join(folder_path, '*.csv'))\n",
"\n",
"# Read and concat all CSVs\n",
"df_draft = pd.concat([pd.read_csv(file) for file in csv_files], ignore_index=True)\n",
"\n",
"df_draft['match'] = 1 \n",
"\n",
"df_draft.shape\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0c8c7493",
"metadata": {},
"outputs": [],
"source": [
"df_draft = df_draft.drop(columns=['Categoria'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "91298cde",
"metadata": {},
"outputs": [],
"source": [
"df_draft.columns[7:25]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "34e179cb",
"metadata": {},
"outputs": [],
"source": [
"# Define as colunas mensais\n",
"colunas_mensais = df_draft.columns[7:25]\n",
"\n",
"# Agrupa por PDV e calcula crescimento médio por PDV\n",
"def calcular_crescimento(grupo):\n",
" soma_mensal = grupo[colunas_mensais].sum() # soma por mês\n",
" variacao_mensal = soma_mensal.pct_change().dropna() # variação percentual mês a mês\n",
" variacao_mensal = variacao_mensal[np.isfinite(variacao_mensal)]\n",
"\n",
" if len(variacao_mensal) == 0:\n",
" return pd.Series({'CRESCIMENTO': np.nan})\n",
"\n",
" media = variacao_mensal.mean()\n",
" desvio = variacao_mensal.std()\n",
"\n",
" limite_sup = media + 2 * desvio\n",
" limite_inf = media - 2 * desvio\n",
"\n",
" variacoes_filtradas = variacao_mensal[variacao_mensal.between(limite_inf, limite_sup)]\n",
" crescimento = round(variacoes_filtradas.mean(), 4)\n",
" return pd.Series({'CRESCIMENTO': crescimento})\n",
"\n",
"# Aplica a função por PDV\n",
"crescimento_por_pdv = df_draft.groupby('PDV').apply(calcular_crescimento)\n",
"\n",
"# Merge do resultado de volta no dataframe original\n",
"df_draft = df_draft.merge(crescimento_por_pdv, on='PDV', how='left')\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4bc8c2b4",
"metadata": {},
"outputs": [],
"source": [
"df_similares['PDV'] = df_similares['PDV'].astype('Int64')\n",
"\n",
"df_final = pd.merge(left=df_similares,right=df_draft,right_on=['PDV', 'SKU'],left_on=['PDV','PRODUTO SIMILAR'],how='left')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c1451562",
"metadata": {},
"outputs": [],
"source": [
"df_venda_diaria = pd.read_excel(r\"C:\\Users\\joao.herculano\\GRUPO GINSENG\\Assistência Suprimentos - 2025\\SUPRIMENTOS\\BD_LANÇAMENTOS\\BOT\\BOT - C11\\arquivos para geração da sugestão\\VENDAS_DIARIAS\\FormFiltroConsultaVendaSintetica_22_05_2025_16_26_17.xls\")\n",
"\n",
"df_venda_diaria.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "882e68aa",
"metadata": {},
"outputs": [],
"source": [
"df_venda_diaria.columns"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c7ddaf20",
"metadata": {},
"outputs": [],
"source": [
"df_venda_diaria['PDV'] = df_venda_diaria['Unidade de Negócio'].str.split(\"-\").str[0].str.strip()\n",
"\n",
"df_venda_diaria['Dia'] = pd.to_datetime(df_venda_diaria['Dia'], format='%d/%m/%Y')\n",
"\n",
"df_venda_diaria = pd.merge(left=df_venda_diaria,right=calendario[['Ciclo','Date']],left_on='Dia',right_on='Date',how='inner')\n",
"\n",
"df_venda_diaria = df_venda_diaria.drop(columns='Date')\n",
"\n",
"df_venda_diaria.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7119556a",
"metadata": {},
"outputs": [],
"source": [
"# 'Dia' já está em formato datetime, então renomeamos para 'Data' diretamente\n",
"# ou apenas usamos 'Dia' como referência de data\n",
"\n",
"# Ordena o DataFrame para garantir que a cumulativa funcione corretamente\n",
"df_venda_diaria = df_venda_diaria.sort_values(by=['Unidade de Negócio', 'Código do Produto', 'Dia'])\n",
"\n",
"# Calcula a quantidade acumulada até o dia para cada grupo\n",
"df_venda_diaria['Quantidade Acumulada'] = (\n",
" df_venda_diaria\n",
" .groupby(['Unidade de Negócio', 'Código do Produto'])['Quantidade']\n",
" .cumsum()\n",
") # acumulado por grupo até a data da linha\n",
"\n",
"df_venda_diaria.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c707a1b6",
"metadata": {},
"outputs": [],
"source": [
"df_venda_diaria = df_venda_diaria.drop_duplicates()\n",
"\n",
"df_venda_agrupado = df_venda_diaria.groupby(['PDV', 'Código do Produto','Ciclo'])['Quantidade Acumulada'].max().reset_index()\n",
"df_venda_agrupado"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dc452c72",
"metadata": {},
"outputs": [],
"source": [
"df_final = pd.merge(left=df_final, right=filtered_calendario[['Ciclo','INICIO CICLO','FIM CICLO','DURAÇÃO','match','dias_ate_inicio']], right_on='match',left_on='MATCH',how='left')\n",
"df_final.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c260e0e3",
"metadata": {},
"outputs": [],
"source": [
"#df_final = df_final.drop(columns=['PDV DESC','status','SKU','Descrição','Lançamento','Item analisado','Planograma','Quantidade por caixa'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8a05450c",
"metadata": {},
"outputs": [],
"source": [
"df_final = pd.merge(left=df_final, right=calendario[['Ciclo','INICIO CICLO','FIM CICLO','DURAÇÃO']], right_on='Ciclo',left_on='CICLO SIMILAR',how='left')\n",
"df_final.shape\n",
"\n",
"df_final = df_final.drop_duplicates()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cc65edab",
"metadata": {},
"outputs": [],
"source": [
"\n",
"df_venda_agrupado = df_venda_agrupado.rename(columns={'Quantidade Acumulada':'Vendas Ciclo Lançamento'})"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c5cd5f42",
"metadata": {},
"outputs": [],
"source": [
"df_final['PRODUTO LANÇAMENTO'] = df_final['PRODUTO LANÇAMENTO'].astype('Int64')\n",
"\n",
"df_venda_agrupado['PDV'] = df_venda_agrupado['PDV'].astype('Int64')\n",
"\n",
"df_final = pd.merge(left=df_final, right = df_venda_agrupado, right_on=['Ciclo','Código do Produto','PDV'],left_on=['CICLO SIMILAR','PRODUTO SIMILAR','PDV'],how='left')\n",
"\n",
"df_final = df_final.drop_duplicates()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "69c88d20",
"metadata": {},
"outputs": [],
"source": [
"df_final['PDV'].value_counts().min()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f5206f50",
"metadata": {},
"outputs": [],
"source": [
"df_final.columns[29:46]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0a1bb832",
"metadata": {},
"outputs": [],
"source": [
"# Suponha que os meses estão nas colunas 10 a 26 (17 colunas = 17 meses)\n",
"colunas_mensais = df_final.columns[29:46]\n",
"\n",
"# Passo 1: Soma todas as linhas (itens) por mês → resultado: total por mês\n",
"soma_mensal = df_final[colunas_mensais].sum()\n",
"\n",
"# Passo 2: Calcula a variação percentual de um mês para o outro\n",
"variacao_mensal = soma_mensal.pct_change()\n",
"variacao_mensal = variacao_mensal.dropna()\n",
"\n",
"variacao_mensal = variacao_mensal[np.isfinite(variacao_mensal)]\n",
"\n",
"# Passo 3: Calcula a média da variação (ignorando o primeiro NaN)\n",
"media_variacao = variacao_mensal[1:].mean()\n",
"\n",
"# Calcula média e desvio padrão\n",
"media = variacao_mensal.mean()\n",
"desvio = variacao_mensal.std()\n",
"\n",
"# Define limite (ex: 2 desvios padrão)\n",
"limite_superior = media + 2 * desvio\n",
"limite_inferior = media - 2 * desvio\n",
"\n",
"# Filtra dados dentro do limite\n",
"filtro = variacao_mensal.between(limite_inferior, limite_superior)\n",
"df_filtrado = variacao_mensal[filtro]\n",
"CRESCIMENTO = round(df_filtrado.mean(),4)\n",
"\n",
"df_final['CRESCIMENTO_GERAL'] = CRESCIMENTO\n",
"\n",
"CRESCIMENTO\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a9647c32",
"metadata": {},
"outputs": [],
"source": [
"df_final = df_final.drop(columns='Ciclo_y')\n",
"\n",
"df_final = df_final.rename(columns={'Ciclo_x': 'Ciclo',\t'INICIO CICLO_x': 'INICIO CICLO',\t'FIM CICLO_x':'FIM CICLO' ,'DURAÇÃO_x':'DURAÇÃO',\n",
" \t'INICIO CICLO_y': 'INICIO CICLO SIMILAR' ,\t'FIM CICLO_y': 'FIM CICLO SIMILAR','DURAÇÃO_y':'DURAÇÃO CICLO SIMILAR'})"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b107e519",
"metadata": {},
"outputs": [],
"source": [
"df_final.columns[40:47]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8290853c",
"metadata": {},
"outputs": [],
"source": [
"VENDA_SIMILAR_6_MESES= df_final.columns[40:47]\n",
"\n",
"df_final['Pico Vendas Similar Ultimos 6 ciclos'] = df_final[VENDA_SIMILAR_6_MESES].max(axis=1)\n",
"\n",
"\n",
"df_final['MEDIANA DO HISTÓRICO'] = df_final[colunas_mensais].dropna().median(axis=1)\n",
"\n",
"df_final['Vendas Ciclo Lançamento'] = df_final['Vendas Ciclo Lançamento'].fillna(0)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "07f043f2",
"metadata": {},
"outputs": [],
"source": [
"medi = df_final.groupby(['CANAL'])['MEDIANA DO HISTÓRICO'].max().reset_index()\n",
"medi = medi.rename(columns={'MEDIANA DO HISTÓRICO':'med_por_canal'})\n",
"medi"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "94abddce",
"metadata": {},
"outputs": [],
"source": [
"df_final = pd.merge(left=df_final, right=medi,on='CANAL',how='inner')\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "09cc2f82",
"metadata": {},
"outputs": [],
"source": [
"df_vdc = pd.read_csv(r\"C:\\Users\\joao.herculano\\GRUPO GINSENG\\Assistência Suprimentos - 2025\\SUPRIMENTOS\\BD_LANÇAMENTOS\\BOT\\BOT - C11\\arquivos para geração da sugestão\\VENDAS VDC\\vendas_vdc22.02.csv\")\n",
"\n",
"\n",
"\n",
"df_vdc['DATA VENDA'] = pd.to_datetime(df_vdc['DATA VENDA'])\n",
"\n",
"# 'Dia' já está em formato datetime, então renomeamos para 'Data' diretamente\n",
"# ou apenas usamos 'Dia' como referência de data\n",
"\n",
"# Ordena o DataFrame para garantir que a cumulativa funcione corretamente\n",
"df_venda_diaria = df_venda_diaria.sort_values(by=['Unidade de Negócio', 'Código do Produto', 'Dia'])\n",
"\n",
"# Calcula a quantidade acumulada até o dia para cada grupo\n",
"df_vdc['Quantidade Acumulada vdc'] = (\n",
" df_vdc\n",
" .groupby(['PDVDEPARA.Practico', 'Código'])['Soma de Quantidade']\n",
" .cumsum()\n",
") # acumulado por grupo até a data da linha"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a827c08",
"metadata": {},
"outputs": [],
"source": [
"df_vdc = pd.merge(left=df_vdc,right=calendario[['Date','Ciclo']],left_on='DATA VENDA',right_on='Date',how='inner')\n",
"\n",
"df_vdc_agrupado = df_vdc.groupby(['PDVDEPARA.Practico',\t'Código','Ciclo'])['Quantidade Acumulada vdc'].max().reset_index()\n",
"\n",
"df_vdc_agrupado = df_vdc_agrupado.rename(columns={'Ciclo':'Ciclo vdc'})\n",
"\n",
"\n",
"df_vdc_agrupado.head()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8ec14143",
"metadata": {},
"outputs": [],
"source": [
"df_final = pd.merge(left=df_final, right = df_vdc_agrupado, right_on=['Ciclo vdc','Código','PDVDEPARA.Practico'],left_on=['CICLO SIMILAR','PRODUTO SIMILAR','PDV'],how='left')\n",
"\n",
"df_final['Quantidade Acumulada vdc'] = df_final['Quantidade Acumulada vdc'].fillna(0)\n",
"\n",
"\n",
"df_final['Vendas Ciclo Lançamento'] = np.where(df_final['Quantidade Acumulada vdc']>0, df_final['Quantidade Acumulada vdc'], df_final['Vendas Ciclo Lançamento'])\n",
"\n",
"df_final = df_final.drop(columns='Quantidade Acumulada vdc')\n",
"\n",
"\n",
"df_final = df_final.drop(columns='Ciclo vdc')\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1a625e69",
"metadata": {},
"outputs": [],
"source": [
"df_final['CRESCIMENTO_FINAL'] = df_final['CRESCIMENTO_GERAL'] + df_final['CRESCIMENTO'] #crescimento do pdv\n",
"\n",
"df_final['CRESCIMENTO_FINAL'] = np.where(df_final['CRESCIMENTO_GERAL'] + df_final['CRESCIMENTO']>0.8,0.8,df_final['CRESCIMENTO_GERAL'] + df_final['CRESCIMENTO'])\n",
"\n",
"df_final['CRESCIMENTO_FINAL'] = np.where(df_final['CRESCIMENTO_GERAL'] + df_final['CRESCIMENTO']<0,0,df_final['CRESCIMENTO_GERAL'] + df_final['CRESCIMENTO'])\n",
"\n",
"df_final['MEDIANA DO HISTÓRICO'] = np.where(df_final['MEDIANA DO HISTÓRICO']==0, df_final['' \\\n",
"'or_canal'],df_final['MEDIANA DO HISTÓRICO'])\n",
"\n",
"# Primeiro cálculo intermediário\n",
"df_final['PV GINSENG'] = np.where(df_final['CRESCIMENTO_FINAL'] * df_final['Vendas Ciclo Lançamento'] + df_final['Vendas Ciclo Lançamento'] < df_final['MEDIANA DO HISTÓRICO'],\n",
" round(df_final['CRESCIMENTO_FINAL'] * df_final['MEDIANA DO HISTÓRICO']+ df_final['MEDIANA DO HISTÓRICO'],0), \n",
" round(df_final['CRESCIMENTO_FINAL']*df_final['Vendas Ciclo Lançamento']+df_final['Vendas Ciclo Lançamento'],0))\n",
"\n",
"df_final['PV GINSENG'] = np.where(df_final['PV GINSENG'].isna(),df_final['med_por_canal'] ,df_final['PV GINSENG'])\n",
"\n",
"df_final.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ad10c069",
"metadata": {},
"outputs": [],
"source": [
"df_final.drop(columns=df_final.columns[29:42],inplace=True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f9bddbb1",
"metadata": {},
"outputs": [],
"source": [
"df_final.columns"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fe73c93e",
"metadata": {},
"outputs": [],
"source": [
"\n",
"df_final.drop(columns=['status', 'Classe', 'SKU', 'Subcategoria', 'Lançamento',\n",
" 'Desativação','Dias sem venda',\n",
" 'Projeção Próximo Ciclo', 'Projeção Próximo Ciclo + 1',\n",
" 'Promoção Próximo Ciclo', 'Promoção Próximo Ciclo + 1', 'Estoque Atual',\n",
" 'Estoque em Transito', 'Pedido Pendente',\n",
" 'Compra inteligente semanal/Sugestão de compra',\n",
" 'Compra inteligente Próximo Ciclo',\n",
" 'Compra inteligente Próximo Ciclo + 1', 'Item Desativado',\n",
" 'Data Prevista Regularização', 'Carteira Bloqueada Para Novos Pedidos',\n",
" 'Planograma', 'Quantidade por caixa', 'Preço Sell In', 'Quantidade',\n",
" 'Item analisado', 'match_x',\n",
" 'CRESCIMENTO', 'Ciclo', 'INICIO CICLO', 'FIM CICLO', 'DURAÇÃO',\n",
" 'match_y', 'dias_ate_inicio', 'INICIO CICLO SIMILAR','med_por_canal', 'CRESCIMENTO_FINAL',\n",
" 'FIM CICLO SIMILAR', 'DURAÇÃO CICLO SIMILAR', 'Código do Produto','CRESCIMENTO_GERAL'],inplace=True)\n",
"df_final.columns"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "66772a9a",
"metadata": {},
"outputs": [],
"source": [
"df_final.columns[23:28]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "15b7149f",
"metadata": {},
"outputs": [],
"source": [
"df_final = df_final.rename(columns={df_final.columns[23]: \"C-4\", df_final.columns[24]: \"C-3\",df_final.columns[25]: \"C-2\",df_final.columns[26]: \"C-1\",df_final.columns[27]:'VENDAS CICLO ATUAL'})\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9333bc77",
"metadata": {},
"outputs": [],
"source": [
"df_final.columns = df_final.columns.str.upper()\n",
"\n",
"df_final.drop(columns=df_final.filter(regex='HISTÓRICO DE VENDAS DO CICLO').columns, inplace=True)\n",
"\n",
"df_final.columns"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5abd4bae",
"metadata": {},
"outputs": [],
"source": [
"df_final = df_final.drop(columns=['DESCRIÇÃO','MEDIANA DO HISTÓRICO'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "62ce5c62",
"metadata": {},
"outputs": [],
"source": [
"df_final.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "25cbff26",
"metadata": {},
"outputs": [],
"source": [
"df_final = df_final.reindex(columns=[\n",
" 'SUPERVISOR',\n",
" 'ANALISTA',\n",
" 'CANAL',\n",
" 'UF',\n",
" 'PDV',\n",
" 'PDV DESC',\n",
" 'PRODUTO LANÇAMENTO',\n",
" 'DESCRIÇÃO DO LANÇAMENTO',\n",
" 'MARCA',\n",
" 'CATEGORIA',\n",
" 'MECANICA CONSUMIDOR',\n",
" '% CONSUMIDOR',\n",
" 'MECANICA REVENDEDOR',\n",
" '% REVENDEDOR',\n",
" 'TIPO DE PRODUTO',\n",
" 'IAF',\n",
" 'FOCO',\n",
" 'SKU',\n",
" 'PRODUTO SIMILAR',\n",
" 'DESCRIÇÃO SIMILAR',\n",
" 'CICLO SIMILAR',\n",
" 'VENDAS CICLO LANÇAMENTO',\n",
" 'C-4',\n",
" 'C-3',\n",
" 'C-2',\n",
" 'C-1',\n",
" 'VENDAS CICLO ATUAL',\n",
" 'PICO VENDAS SIMILAR ULTIMOS 6 CICLOS',\n",
" 'PV GINSENG'])\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a3e80cb4",
"metadata": {},
"outputs": [],
"source": [
"df_final['SUGESTÃO METASELLIN'] = ''\n",
"df_final['SUGESTÃO ABASTECIMENTO'] = ''\n",
"df_final['SUGESTÃO COMERCIAL'] = ''\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2df3e2e9",
"metadata": {},
"outputs": [],
"source": [
"df_final.to_excel(r'C:\\Users\\joao.herculano\\Documents\\sugest.xlsx',index=False)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,221 @@
import smtplib
import ssl
import psycopg2
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 = psycopg2.connect(
host=config['banco']['host'],
port="5432",
database="ginseng",
user=config['banco']['user'],
password=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 = '' 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 = false THEN 'REGULAR' ELSE 'CRITICO' END AS status_item
FROM Draft
WHERE isproductdeactivated = false 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.")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,241 @@
# enviar_email_excel.py
import smtplib
import ssl
import psycopg2
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 = psycopg2.connect(
host=config['banco']['host'], # ou IP do servidor
port="5432", # padrão do PostgreSQL
database="ginseng",
user=config['banco']['user'],
password= config['banco']['password']
)
# Criar um cursor para executar comandos SQL
cur = conn.cursor()
# 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 "public"."draft"
where dayswithoutsales > 40
and deactivation = ''
and stock_actual > 0
and isproductdeactivated is not null
and currentcyclesales = 0
'''
df = pd.read_sql(query, conn)
cur.close()
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

@ -0,0 +1,221 @@
import smtplib
import ssl
import psycopg2
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 = psycopg2.connect(
host=config['banco']['host'],
port="5432",
database="ginseng",
user=config['banco']['user'],
password=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 = '' 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 = false THEN 'REGULAR' ELSE 'CRITICO' END AS status_item
FROM Draft
WHERE isproductdeactivated = false 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.")