Enviar arquivos para "/"
This commit is contained in:
commit
166317fa25
346
ferramenta_transferencias.py
Normal file
346
ferramenta_transferencias.py
Normal 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 há 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 só 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".
|
||||
''')
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user