Adicionar main.py

This commit is contained in:
Monezi 2025-04-11 17:01:27 -03:00
commit 7ee29c23a5

346
main.py Normal file
View File

@ -0,0 +1,346 @@
import pandas as pd
import numpy as np
from openpyxl import Workbook
from datetime import datetime
from openpyxl.utils.dataframe import dataframe_to_rows
import streamlit as st
from io import BytesIO
#----------------Função primária-------------------#
def calcular_transferencias(arquivo, arquivo_lojas, arquivo_lista_negra, cob_corte, cob_max, cob_min, limite_transferencia, estoque_minimo, arquivo_planograma):
###1. Funções de apoio
#Verifica se o estoque está acima do estoque crítico (cobertura acima da cobertura de corte), se sim, calcula excesso
def calcular_exc(row):
if row['ESTOQUE ATUAL'] > row['MV CORTE'] and row['MV CORTE'] != 0:
return row['ESTOQUE ATUAL'] - row['MV MAX']
else:
return 0
#Verifica se o estoque está abaixo do estoque mínimo, se sim, calcula a falta
def calcular_falta(row):
if row['Estoque'] < row['MV MIN']:
return row['MV MIN'] - row['Estoque']
else:
return 0
###2. Importação relatório de estoque
planilha_estoque = arquivo
with st.spinner("Carregando relatório de estoque..."):
try:
df_estoque_bruta = pd.read_excel(planilha_estoque, sheet_name=None)
df_estoque = pd.concat(df_estoque_bruta, ignore_index=True)
colunas_desejadas = ['SKU', 'DESCRICAO', 'CLASSE', 'FASES PRODUTO', 'PDV', 'ESTOQUE ATUAL',
'ESTOQUE EM TRANSITO', 'PEDIDO PENDENTE', 'ESTOQUE DE SEGURANCA', 'DDV PREVISTO', 'COBERTURA ATUAL', 'COBERTURA ATUAL + TRANSITO']
df_estoque = df_estoque[colunas_desejadas]
df_estoque = df_estoque[df_estoque['COBERTURA ATUAL + TRANSITO'] != -1]
df_estoque = df_estoque[df_estoque['FASES PRODUTO'] != 'Descontinuado']
except Exception as e:
st.error(f"Erro: relatório de estoque não importado. {e}")
###3. Adicionando dados dos PDVs e Lista proibida
with st.spinner("Carregando bases auxiliares..."):
try:
planilha_pdvs = arquivo_lojas
planilha_lista_negra = arquivo_lista_negra
planilha_planograma = arquivo_planograma
df_pdv = pd.read_excel(planilha_pdvs, sheet_name ='PDV')
df_sku = pd.read_excel(planilha_pdvs, sheet_name ='SKU')
df_block = pd.read_excel(planilha_lista_negra, sheet_name='Lista proibida')
df_planograma = pd.read_excel(planilha_planograma, sheet_name='Planilha1')
skus_block = pd.merge(df_estoque, df_block, on='SKU', how='inner')['SKU']
df_estoque = df_estoque[~df_estoque['SKU'].isin(skus_block)] #exclui do df os SKUs proibidos
df_estoque['CHAVE'] = df_estoque['PDV'].astype(str) + df_estoque['SKU'].astype(str) # cria uma chave PDVSKU
df_planograma['CHAVE'] = df_estoque['PDV'].astype(str) + df_estoque['SKU'].astype(str) # cria uma chave PDVSKU
de_para_regiao = dict(zip(df_pdv['PDV'], df_pdv['REGIÃO']))
de_para_sku = dict(zip(df_sku['SKU2'], df_sku['SKU1']))
de_para_canal = dict(zip(df_pdv['PDV'], df_pdv['TIPO'])) #depara de canais
de_para_canal_transfer = dict(zip(df_pdv['PDV'], df_pdv['TRANSFER'])) #depara dos canais que podem fazer transferencias entre si
de_para_planograma = dict(zip(df_planograma['CHAVE'], df_planograma['Planograma'])) #depara do planograma para cada PDVSKU
# Adicionar as colunas de região, SKU2 (códigos antigos), canal, canal de transferência e planograma ao DataFrame usando a planilha dos PDVs
df_estoque.loc[:, 'REGIAO'] = df_estoque['PDV'].map(de_para_regiao)
df_estoque.loc[:, 'SKU2'] = df_estoque['SKU'].map(de_para_sku)
df_estoque.loc[:, 'CANAL'] = df_estoque['PDV'].map(de_para_canal)
df_estoque.loc[:, 'TRANSFER'] = df_estoque['PDV'].map(de_para_canal_transfer)
df_estoque.loc[:, 'PLANOGRAMA'] = df_estoque['CHAVE'].map(de_para_planograma)
df_estoque['PLANOGRAMA'].fillna(0, inplace=True) # substitui as linhas vazias por 0 para o cálculo do estoque mínimo
# Adicionar coluna com o estoque mínimo de itens que deve ser deixado nos PDVs de origem
df_estoque['MIN ORIGEM'] = estoque_minimo
except Exception as e:
st.error(f"Erro: não foi possível carregar bases auxiliares. {e}")
###4. Calculando informações para transferências
with st.spinner("Carregando configurações..."):
try:
#Calculando estoque que será utilizado para calcular falta (considera o estoque em trânsito)
df_estoque.loc[:, 'Estoque'] = df_estoque['ESTOQUE ATUAL'] + df_estoque['ESTOQUE EM TRANSITO'] + df_estoque['PEDIDO PENDENTE']
#Calculando o estoque na cobertura de corte
df_estoque.loc[:, 'MV CORTE'] = np.ceil(df_estoque['DDV PREVISTO'] * cob_corte)
#Calculando o estoque na cobertura máxima
df_estoque.loc[:, 'MV MAX'] = np.ceil(df_estoque['DDV PREVISTO'] * cob_max)
#Calculando o estoque na cobertura mínima
df_estoque.loc[:, 'MV MIN'] = np.ceil(df_estoque['DDV PREVISTO'] * cob_min)
#Verificando se há excesso ou falta
df_estoque.loc[:, '#EXC'] = df_estoque.apply(calcular_exc, axis=1)
df_estoque.loc[:, '#FALTA'] = df_estoque.apply(calcular_falta, axis=1)
#Definindo o estoque mínimo para verificação da viabilidade de transferências nos PDVs de origem
df_estoque.loc[:, 'EST MIN'] = df_estoque[['ESTOQUE DE SEGURANCA', 'PLANOGRAMA', 'MIN ORIGEM']].max(axis=1)
except:
st.erro("Erro de configuração")
###5. Verificando transferências
with st.spinner("Calculando transferências..."):
#Criando lista com as regiões e canais
#Obs: o algoritmo calcula as transferências priorizando os PDVs com maiores faltas e excessos, a ideia é garantir a reposição para atingir cobertura mínima
try:
regioes = df_estoque['REGIAO'].unique() #lista de regioes
canais = df_estoque['TRANSFER'].unique() #lista de canais que podem realizar transferencias entre si
colunas_sugestao = ['REGIAO', 'SKU', 'SKU2', 'DESCRICAO', 'PDV ORIGEM', 'CANAL ORIGEM', 'PDV DESTINO', 'CANAL DESTINO', 'QTD']
sugestao_transferencia = pd.DataFrame(columns=colunas_sugestao)
for canal in canais:
for regiao in regioes:
df_regiao = df_estoque[(df_estoque['REGIAO'] == regiao) & (df_estoque['TRANSFER'] == canal)] #quebra do df por regiao
df_excedentes = df_regiao.loc[df_regiao['#EXC'] > 0] #df apenas com SKUs em excesso
# Filtrando as linhas onde #Falta é maior que zero
df_falta = df_regiao.loc[df_regiao['#FALTA'] > 0] #df apenas com SKUs em falta
skus = df_regiao['SKU'].unique()
for sku in skus:
qtd_falta = 0 #aux para verificacao da qtd em falta
qtd_excesso = 0 #aux para verificacao da qtd em excesso
qtd_resultante = 0 #aux para verificacao do excesso resultante
filtro_sku = df_falta['SKU'] == sku #aux para filtrar o SKU no df
filtro_sku_exc = df_excedentes['SKU'] == sku #aux para filtrar o SKU no df de excedentes
df_falta_filtrado = df_falta.loc[filtro_sku]
df_falta_filtrado = df_falta_filtrado.sort_values(by='#FALTA', ascending=False) #Ordenando o df com base em '#Falta' em ordem decrescente
df_excedentes_filtrado = df_excedentes.loc[filtro_sku_exc]
df_excedentes_filtrado = df_excedentes_filtrado.sort_values(by='#EXC', ascending=False) #Ordenando o df com base em '#EXC' em ordem decrescente
pdvs_destino = df_falta_filtrado['PDV'].unique() #lista de possíveis PDVs destino (SKU em falta)
pdvs_origem = df_excedentes_filtrado['PDV'].unique() #lista de possíveis PDVs origem (SKU em excesso)
for pdv_falta in pdvs_destino:
filtro_pdv = df_falta['PDV'] == pdv_falta #aux para filtrar o PDV com sku em falta
df_filtrado = df_falta.loc[filtro_pdv & filtro_sku, '#FALTA'] #df_falta filtrado por PDV e SKU
if not df_filtrado.empty:
qtd_falta = df_filtrado.iloc[0]
#Verificacao da necessidade de transferencia
if qtd_falta != 0:
for pdv_excesso in pdvs_origem:
if (pdv_excesso != pdv_falta) and (qtd_falta != 0):
df_filtrado2 = df_regiao.loc[(df_regiao['SKU'] == sku) & (df_regiao['PDV'] == pdv_excesso), '#EXC']
if not df_filtrado2.empty:
qtd_excesso = df_filtrado2.iloc[0]
if qtd_excesso >= qtd_falta:
exc_resultante = qtd_excesso - qtd_falta
qtd_resultante = df_regiao.loc[(df_regiao['SKU'] == sku) & (df_regiao['PDV'] == pdv_excesso), 'MV MAX'] + exc_resultante #calcula o estoque resultante pós transferência
estoque_min = df_regiao.loc[(df_regiao['SKU'] == sku) & (df_regiao['PDV'] == pdv_excesso), 'EST MIN']
if qtd_resultante.sum() >= estoque_min.sum(): #verifica se o estoque resultante após a transferência é maior ou igual ao estoque mínimo
df_regiao.loc[(df_regiao['SKU'] == sku) & (df_regiao['PDV'] == pdv_excesso), '#EXC'] = exc_resultante
sugestao_transferencia = pd.concat([sugestao_transferencia, pd.DataFrame({
'REGIAO': [regiao],
'SKU2': [sku],
'SKU': df_falta.loc[filtro_sku, 'SKU2'].values[0],
'DESCRICAO': df_falta.loc[filtro_sku, 'DESCRICAO'].values[0],
'PDV ORIGEM': [pdv_excesso],
'CANAL ORIGEM': df_regiao.loc[df_regiao['PDV'] == pdv_excesso, 'CANAL'].values[0],
'PDV DESTINO': [pdv_falta],
'CANAL DESTINO': df_regiao.loc[df_regiao['PDV'] == pdv_falta, 'CANAL'].values[0],
'QTD': [qtd_falta],
})], ignore_index=True)
qtd_falta = 0
except:
st.erro("Erro: não foi possível calcular transferências")
####5.1 Verificando limite de transferência
total_transferido = pd.DataFrame(columns=['REGIAO', 'PDV ORIGEM', 'PDV DESTINO', 'TOTAL'])
pdvs = df_estoque['PDV'].unique()
for pdv_origem in pdvs:
for pdv_destino in pdvs:
if pdv_origem != pdv_destino:
filtro_resultado = sugestao_transferencia.loc[(sugestao_transferencia['PDV ORIGEM'] == pdv_origem) & (sugestao_transferencia['PDV DESTINO'] == pdv_destino)]
soma_qtd = filtro_resultado['QTD'].sum()
if soma_qtd < limite_transferencia:
sugestao_transferencia = sugestao_transferencia.drop(sugestao_transferencia[(sugestao_transferencia['PDV ORIGEM'] == pdv_origem) & (sugestao_transferencia['PDV DESTINO'] == pdv_destino)].index)
elif soma_qtd != 0:
total_transferido = pd.concat([total_transferido, pd.DataFrame({
'REGIAO': df_estoque.loc[df_estoque['PDV'] == pdv_origem, 'REGIAO'].values[0],
'PDV ORIGEM': [pdv_origem],
'PDV DESTINO': [pdv_destino],
'TOTAL': [soma_qtd],
})], ignore_index=True)
st.write("Sugestão de transferência concluída!")
st.write("###### Sugestão de transferências:")
st.dataframe(sugestao_transferencia)
return sugestao_transferencia, total_transferido
#----------------Função para exportar resultados-------------------#
def output_resultados(df1, df2):
wb = Workbook()
aba1 = wb.active
aba1.title = "Detalhamento_Transferencias"
aba2 = wb.create_sheet(title="Resumo")
for row_data in dataframe_to_rows(df1, index=False, header=True):
aba1.append(row_data)
for row_data in dataframe_to_rows(df2, index=False, header=True):
aba2.append(row_data)
output = BytesIO()
wb.save(output)
return output.getvalue()
#----------------interface da ferramenta-------------------#
import streamlit as st
st.set_page_config(page_title='Grupo Ginseng', initial_sidebar_state="expanded")
st.title('Grupo Ginseng')
st.header('Planejamento de transferências :outbox_tray:')
tab1, tab2 = st.tabs(["Página Inicial", "Instruções"])
# Upload de arquivo
with tab1:
with st.expander("Configurações"):
input_cob_corte= st.number_input("Cobertura de corte:", value=60)
input_cob_max = st.number_input("Cobertura máxima:", value=45)
input_cob_min = st.number_input("Cobertura mínima:", value=30)
input_limite = st.number_input("Mínimo de itens por transferência:", value=5)
input_estoque_minimo = st.number_input("Estoque mínimo:", value=5)
st.markdown('**Inserção da base de PDVs e SKUs:**')
arquivo_lojas = st.file_uploader("Upload cadastro de PDVs e SKUs",type={"xls","xlsx"})
st.markdown('**Inserção da lista proibida de SKUs:**')
arquivo_lista_negra = st.file_uploader("Upload lista proibida",type={"xls","xlsx"})
st.markdown('**Inserção do planograma do ciclo:**')
arquivo_planograma = st.file_uploader("Upload planograma",type={"xls","xlsx"})
st.markdown('**Inserção da planilha de estoque:**')
arquivo = st.file_uploader("Upload relatório de estoque",type={"xls","xlsx"})
if st.button("Iniciar otimizador"):
with st.spinner("Otimizador iniciado"):
try:
sugestao_transferencia, total_transferido = calcular_transferencias(arquivo, arquivo_lojas, arquivo_lista_negra, input_cob_corte, input_cob_max, input_cob_min, input_limite, input_estoque_minimo, arquivo_planograma)
st.download_button(
label="Download",
data=output_resultados(sugestao_transferencia, total_transferido),
file_name=f"Transferencias_{datetime.now().strftime('%Y-%m-%d')}.xlsx",
mime='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
except:
st.error("Erro!")
with tab2:
st.markdown('**Como usar:**')
st.markdown('''
Essa ferramenta tem como objetivo gerar um relatório de sugestões de transfências baseado
na posição do estoque do MAR e nos parâmetros de cobertura.
Na aba "*Configurações*" é possível alterar esses parâmetros. São eles:
- Cobertura de corte: cobertura crítica para determinar a necessidade
de transferência, isto é, o excedente dos SKUs que apresentam cobertura
acima desse valor estará disponível para transferência
- Cobertura máxima: define o estoque máximo dos SKUs, é utilizada como
referência para calcular o estoque excedente que será disponibilizado
para a transferência
- Cobertura mínima: define o estoque mínimo, abaixo desse valor o SKU
será considerado em falta e disponível para reposição até atingi-la
- Mínimo de itens por transferência: define a quantidade mínima de
itens que devem ser transferidos entre os PDVs (se não limite o
valor deve continuar igual a 0)
Para iniciar o otimizador, é necessário fazer o uploud da base de PDVs,
lista proibida com os SKUs que não devem ser disponibilizados para
transferências, base com o planograma atualizado do ciclo e a base de
estoque do MAR atualizada, contendo as informações de todos os PDVs
independentemente de canal, na aba "Página Inicial". O otimizador será
iniciado ao clicar no botão "Iniciar Otimizador". Ao final do processamento,
será apresentada uma planilha com as sugestões de transferências do otimizador.
Para realizar o download dos resultados em formato .xlsx basta clicar em
"Download resultados".
''')
st.markdown('**Atenção!**')
st.warning('''
1. Em caso de necessidade de alteração nos parâmetros da aba "Configurações",
sugere-se a atualização dos campos antes da inserção das bases para evitar
possíveis erros de leitura pela ferramenta.
2. O otimizador apenas sugere transferências entre PDVs de
mesmo canal. É preciso realizar a alteração manual da coluna "TRANSFER"
na planilha de cadastro de lojas utilizada para esta ferramenta para que
os PDVs que possam realizar transferências entre si estejam classificados
em um mesmo canal.
Exemplo: PDVs híbridos podem realizar transferências para VDs, logo,
a coluna "TRANSFER" desses PDVs deve estar preenchida com "VD".
Caso a transferência entre VDs e lojas seja possível em dada região,
é necessário que todos os PDVs estejam classificados da mesma forma
na coluna "TRANSFER".
''')