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