From 7ee29c23a53e1bc62c8d464e09fcda3f21b88da8 Mon Sep 17 00:00:00 2001 From: Monezi Date: Fri, 11 Apr 2025 17:01:27 -0300 Subject: [PATCH] Adicionar main.py --- main.py | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..6195273 --- /dev/null +++ b/main.py @@ -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". + ''') + +