From 2879e23f90e18ab9f45dc9315291812f042c728f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Toledo=20Gon=C3=A7alves?= Date: Wed, 15 Oct 2025 14:13:00 -0300 Subject: [PATCH] =?UTF-8?q?Estrutura=20inicial=20do=20projeto=20de=20dashb?= =?UTF-8?q?oard=20de=20invent=C3=A1rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 15 ++ README.md | 32 +++ scripts/inventario.py | 434 +++++++++++++++++++++++++++++++++ scripts/normalizar_planilha.py | 148 +++++++++++ 4 files changed, 629 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 scripts/inventario.py create mode 100644 scripts/normalizar_planilha.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6bb8b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Ignorar pastas de dados e arquivos gerados +dados_entrada/ +relatorios_gerados/ +/dados-normalizados.xlsx + +# Ignorar arquivos temporários do Python +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Ignorar arquivos de ambiente virtual +.venv/ +venv/ +env/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d47cd39 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Dashboard de Inventário e Análise de Risco - Grupo Pralog + +Este projeto automatiza a coleta, normalização e visualização de dados do inventário de colaboradores e equipamentos do Grupo Pralog, gerando um dashboard interativo para análise de conformidade e riscos. + +## Funcionalidades + +- **Normalização de Dados**: Limpa e padroniza informações de múltiplas planilhas de origem. +- **Enriquecimento de Dados**: Adiciona colaboradores ausentes e cruza informações entre as fontes. +- **Geração de Dashboard**: Cria um relatório HTML interativo com KPIs, gráficos e tabelas detalhadas por gestor e equipe. +- **Análise de Risco**: Identifica e classifica riscos como falta de dados, uso de equipamento próprio, inconsistências de cadastro, etc. + +## Estrutura do Projeto + +``` +/ +├── dados_entrada/ # Planilhas Excel de origem (ignoradas pelo Git) +├── relatorios_gerados/ # Onde os dashboards HTML são salvos (ignorados pelo Git) +├── scripts/ # Contém os scripts Python +│ ├── normalizar_planilha.py # Script de preparação e limpeza dos dados +│ └── inventario.py # Script principal que gera o dashboard +├── .gitignore # Arquivos e pastas a serem ignorados pelo Git +└── README.md # Esta documentação +``` + +## Como Executar + +1. **Pré-requisitos**: Certifique-se de ter o Python e o Pandas/Plotly instalados (`pip install pandas openpyxl plotly`). +2. **Dados**: Coloque as planilhas `dados.xlsx` e `Inventario de Equipamentos...` na pasta `dados_entrada/`. +3. **Execução**: + * Primeiro, execute o script de normalização: `python scripts/normalizar_planilha.py` + * Em seguida, execute o script principal para gerar o dashboard: `python scripts/inventario.py` +4. **Resultado**: O arquivo `dashboard_interativo_final.html` será gerado na pasta `relatorios_gerados/`. \ No newline at end of file diff --git a/scripts/inventario.py b/scripts/inventario.py new file mode 100644 index 0000000..3c2f641 --- /dev/null +++ b/scripts/inventario.py @@ -0,0 +1,434 @@ +# inventario_dashboard_final_v22_corrigido.py +import pandas as pd +import re +from pathlib import Path +import plotly.graph_objects as go +import json + +# --- CONFIGURAÇÃO DE COLUNAS --- +COLUNAS = { + "nome": "NOME", "email_pessoal": "Email Pessoal Sendo Usado", "contato_colaborador": "Contato Colaborador", + "contratacao": "Contratação", "status_contrato": "Status do Contrato", "cliente": "CLIENTE", + "base": "BASE", "cargo": "CARGO", "responsavel_direto": "RESPONSÁVEL DIRETO", + "contato_responsavel": "Contato do RESPONSÁVEL", + "contato_1": "Momento do Contato Para Busca de Dados Contato 1", + "contato_2": "Momento do Contato Para Busca de Dados Contato 2", + "contato_3": "Momento do Contato Para Busca de Dados Contato 3", + "subordinado": "Realmente e um Subordinado?", "usa_notebook": "Usa Notebook da Empresa", + "email_corporativo_status": "Tem Email Corporativo?", "email_corporativo_endereco": "Email Corporativo", + "tem_ad": "Tem conta no AD?", "maquina_registrada_status": "Maquina Registrada para o Cliente?", + "maquina_registrada_nome": "Nome da Maquina Registrada", "tag_ativo": "Tag do Ativo", + "numero_serie": "Rótulo de Serviço / Número de Série" +} + +# --- Funções Auxiliares --- +def normalizar_texto(texto): + if isinstance(texto, str): return texto.strip().title() + return texto + +def normalizar_telefone(numero): + if not isinstance(numero, (str, int, float)): return None + numeros_apenas = re.sub(r'\D', '', str(numero)) + if len(numeros_apenas) == 11 and numeros_apenas.startswith('0'): + numeros_apenas = numeros_apenas[1:] + if len(numeros_apenas) in [10, 11]: return f"+55{numeros_apenas}" + if len(numeros_apenas) == 12 and numeros_apenas.startswith('55'): return f"+{numeros_apenas}" + if len(numeros_apenas) == 13 and numeros_apenas.startswith('55'): return f"+{numeros_apenas}" + return None + +def extrair_sim_nao(texto): + if not isinstance(texto, str): return "não informado" + texto_limpo = texto.strip().lower() + if texto_limpo.startswith('sim'): return 'sim' + if texto_limpo.startswith('não') or texto_limpo.startswith('nao'): return 'não' + return 'não informado' + +def formatar_contratacao(texto): + if not isinstance(texto, str) or texto == "Não informado": return texto + texto_lower = texto.lower() + classe_css = "generico" + if "pessoa juridica" in texto_lower or "pj" in texto_lower: classe_css = "pj" + elif "clt" in texto_lower: classe_css = "clt" + elif "estagio" in texto_lower or "estagiario" in texto_lower: classe_css = "estagio" + return f"{texto}" + +def criar_grafico_pizza(valores, legendas, titulo, cores_personalizadas=None): + valores_filtrados, legendas_filtradas, cores_filtradas = [], [], [] + cores_originais = cores_personalizadas if cores_personalizadas else ['#28a745', '#dc3545', '#ffc107', '#6c757d'] + for i, valor in enumerate(valores): + if valor > 0: + valores_filtrados.append(valor) + legendas_filtradas.append(legendas[i]) + cores_filtradas.append(cores_originais[i % len(cores_originais)]) + if not valores_filtrados: + return f"

{titulo}

Sem dados para exibir.

" + fig = go.Figure(data=[go.Pie(labels=legendas_filtradas, values=valores_filtrados, hole=.4, marker_colors=cores_filtradas, textinfo='percent+value', insidetextfont=dict(color='white', size=14), hovertemplate="%{label}
%{value}
%{percent}")]) + fig.update_layout(title_text=titulo, title_x=0.5, margin=dict(t=60, b=80, l=20, r=20), showlegend=True, legend=dict(orientation="h", yanchor="top", y=-0.1, xanchor="center", x=0.5), font=dict(family="Arial, sans-serif", size=14), paper_bgcolor='rgba(0,0,0,0)') + return fig.to_html(full_html=False, include_plotlyjs=False, config={'displayModeBar': False, 'responsive': True}) + +def gerar_tabela_colaboradores(df_grupo): + tabela_html = "
" + for index, linha in df_grupo.iterrows(): + filtros_da_linha = [key for key, value in linha.items() if key.startswith('filtro_') and value == True] + data_filters_attr = ' '.join(filtros_da_linha) + contato_colab = linha['Telefone Colaborador Normalizado'] + contato_html = f'{contato_colab} ' if contato_colab else "Sem contato" + contratacao_raw = linha.get(COLUNAS['contratacao']) + contratacao = formatar_contratacao(contratacao_raw if pd.notna(contratacao_raw) else "Não informado") + status_contrato = linha.get(COLUNAS['status_contrato'], "Não informado") + status_contrato = status_contrato if pd.notna(status_contrato) else "Não informado" + indicadores_html = "" + info_notebook_original = linha.get(COLUNAS['usa_notebook']) + classe_linha = "" + if (isinstance(info_notebook_original, str) and 'saiu da empresa' in info_notebook_original.lower()) or \ + (isinstance(status_contrato, str) and 'saiu da empresa' in status_contrato.lower()): + classe_linha = " class='terminated'" + if linha['filtro_em_conformidade']: + indicadores_html = " Em conformidade" + else: + if linha.get('filtro_ativo_sem_ad') == True: indicadores_html += " Ativo sem Conta AD" + if linha.get('filtro_ativo_nao_registrado') == True: indicadores_html += " Ativo Não Registrado" + if linha['Usa Notebook (Valor)'] in ['não', 'não informado']: indicadores_html += " Ativo Próprio / Não Declarado" + if pd.notna(linha['Email Pessoal']): indicadores_html += " Drive Externo" + if linha['Tem Email Corp (Valor)'] == 'não': indicadores_html += "" + if str(linha.get(COLUNAS["subordinado"])).lower() == 'não': indicadores_html += " Não Subordinado" + is_cadastro_incompleto = linha['filtro_sem_gestor'] or linha['filtro_sem_contrato'] or linha['filtro_sem_status_contrato'] or linha['filtro_sem_cargo'] + if is_cadastro_incompleto: indicadores_html += " Cadastro Incompleto" + if not indicadores_html: indicadores_html = " Fora de Conformidade" + detalhes_html = "" + if 'terminated' in classe_linha: + detalhes_html += "
Colaborador desligado da empresa.
" + if pd.notna(linha.get(COLUNAS['cargo'])): + detalhes_html += f"
{linha[COLUNAS['cargo']]}
" + if linha['Usa Notebook (Valor)'] == 'sim': + detalhes_html += "
Utiliza equipamento fornecido pela empresa.
" + info_maquina = linha.get(COLUNAS['maquina_registrada_nome']) + if pd.notna(info_maquina): + detalhes_html += f"
Máquina: {info_maquina}
" + elif isinstance(info_notebook_original, str) and 'usa pessoal' in info_notebook_original.lower(): + detalhes_html += "
Colaborador utiliza equipamento pessoal.
" + elif isinstance(info_notebook_original, str) and 'aguardando equipamento' in info_notebook_original.lower(): + detalhes_html += "
Aguardando equipamento da empresa.
" + elif isinstance(info_notebook_original, str) and info_notebook_original.strip().lower() == 'não': + detalhes_html += f"
Informado que não utiliza notebook, sem detalhes adicionais.
" + elif pd.notna(info_notebook_original): + detalhes_html += f"
{info_notebook_original}
" + if pd.notna(linha.get(COLUNAS['tag_ativo'])): + detalhes_html += f"
Tag do Ativo: {linha[COLUNAS['tag_ativo']]}
" + if pd.notna(linha.get(COLUNAS['numero_serie'])): + detalhes_html += f"
N/S: {linha[COLUNAS['numero_serie']]}
" + if pd.notna(linha.get(COLUNAS['email_corporativo_endereco'])): + detalhes_html += f"
{linha[COLUNAS['email_corporativo_endereco']]}
" + if pd.notna(linha['Email Pessoal']): + detalhes_html += f"
{linha['Email Pessoal']}
" + detalhes_final_html = detalhes_html if detalhes_html else "Sem informações adicionais." + tabela_html += f"{linha[COLUNAS['nome']]}{contratacao}{status_contrato}{indicadores_html}{detalhes_final_html}" + tabela_html += "
ColaboradorContrataçãoStatusContatoIndicadores de RiscoDetalhes
{contato_html}
" + return tabela_html + +try: + caminho_do_script = Path(__file__).parent + nome_arquivo_excel = caminho_do_script / 'dados.xlsx' + df = pd.read_excel(nome_arquivo_excel) + + df.replace(['N/A', 'Não informado', 'Sem Informação', 'Sem Informações'], pd.NA, inplace=True) + + for col_key in ["nome", "responsavel_direto"]: + if COLUNAS[col_key] in df.columns: + df[COLUNAS[col_key]] = df[COLUNAS[col_key]].apply(normalizar_texto) + + # --- Pré-Cálculos de Status e Riscos --- + df['Telefone Colaborador Normalizado'] = df.get(COLUNAS["contato_colaborador"], pd.Series(dtype='str')).apply(normalizar_telefone) + df['Usa Notebook (Valor)'] = df.get(COLUNAS["usa_notebook"], pd.Series(dtype='str')).apply(extrair_sim_nao) + df['Tem Email Corp (Valor)'] = df.get(COLUNAS["email_corporativo_status"], pd.Series(dtype='str')).apply(extrair_sim_nao) + df['Tem AD (Valor)'] = df.get(COLUNAS["tem_ad"], pd.Series(dtype='str')).apply(extrair_sim_nao) + df['Maquina Reg (Valor)'] = df.get(COLUNAS["maquina_registrada_status"], pd.Series(dtype='str')).apply(extrair_sim_nao) + + if COLUNAS["email_pessoal"] in df.columns: + df['Email Pessoal'] = df[COLUNAS["email_pessoal"]].apply(lambda x: x if isinstance(x, str) and '@' in x else None) + else: + df['Email Pessoal'] = None + + # --- DEFINIÇÃO DAS CONDIÇÕES DE FILTRO --- + df['filtro_em_conformidade'] = (df['Usa Notebook (Valor)'] == 'sim') & \ + (df.get(COLUNAS['maquina_registrada_nome'], pd.Series(dtype='str')).notna()) & \ + (df['Tem Email Corp (Valor)'] == 'sim') & \ + (df['Telefone Colaborador Normalizado'].notna()) & \ + (df.get(COLUNAS['contratacao'], pd.Series(dtype='str')).notna()) & \ + (df.get(COLUNAS['status_contrato'], pd.Series(dtype='str')).notna()) & \ + (df['Email Pessoal'].isnull()) & \ + (df.get(COLUNAS['responsavel_direto'], pd.Series(dtype='str')).notna()) + + df['filtro_fora_conformidade'] = ~df['filtro_em_conformidade'] + df['filtro_incontactaveis'] = df['Telefone Colaborador Normalizado'].isnull() + df['filtro_equip_proprio'] = df['Usa Notebook (Valor)'].isin(['não', 'não informado']) + df['filtro_drives_externos'] = df['Email Pessoal'].notna() + df['filtro_sem_email_corp'] = df['Tem Email Corp (Valor)'] == 'não' + df['filtro_sem_gestor'] = df.get(COLUNAS['responsavel_direto'], pd.Series(dtype='str')).isnull() + df['filtro_sem_contrato'] = df.get(COLUNAS['contratacao'], pd.Series(dtype='str')).isnull() + df['filtro_sem_status_contrato'] = df.get(COLUNAS['status_contrato'], pd.Series(dtype='str')).isnull() + df['filtro_sem_cargo'] = df.get(COLUNAS['cargo'], pd.Series(dtype='str')).isnull() + df['filtro_ativo_sem_ad'] = (df['Usa Notebook (Valor)'] == 'sim') & (df['Tem AD (Valor)'] == 'não') + df['filtro_ativo_nao_registrado'] = (df['Usa Notebook (Valor)'] == 'sim') & (df['Maquina Reg (Valor)'] == 'não') + + # --- Cálculos para KPIs Globais --- + total_colaboradores = len(df) + total_conformes = df['filtro_em_conformidade'].sum() + total_nao_conformes = total_colaboradores - total_conformes + perc_conformes = (total_conformes / total_colaboradores) * 100 if total_colaboradores > 0 else 0 + perc_nao_conformes = (total_nao_conformes / total_colaboradores) * 100 if total_colaboradores > 0 else 0 + + total_sem_contato_valido = df['filtro_incontactaveis'].sum() + risco_ativos_nao_declarados = df['filtro_equip_proprio'].sum() + risco_drives_externos = df['filtro_drives_externos'].sum() + risco_sem_email_corp = df['filtro_sem_email_corp'].sum() + risco_sem_gestor = df['filtro_sem_gestor'].sum() + risco_sem_contratacao = df['filtro_sem_contrato'].sum() + risco_sem_status_contrato = df['filtro_sem_status_contrato'].sum() + risco_sem_cargo = df['filtro_sem_cargo'].sum() + risco_sem_ad_count = df['filtro_ativo_sem_ad'].sum() + risco_maquina_nao_reg_count = df['filtro_ativo_nao_registrado'].sum() + + cond_risco_alto = df['filtro_incontactaveis'] | df['filtro_equip_proprio'] | df['filtro_drives_externos'] | df['filtro_sem_email_corp'] + total_risco_alto = cond_risco_alto.sum() + cond_risco_medio = df['filtro_sem_gestor'] | df['filtro_sem_contrato'] | df['filtro_sem_status_contrato'] | df['filtro_sem_cargo'] + total_risco_medio = cond_risco_medio.sum() + cond_risco_baixo = df['filtro_ativo_sem_ad'] | df['filtro_ativo_nao_registrado'] + total_risco_baixo = cond_risco_baixo.sum() + + risco_inconsistencia_total = (df['filtro_ativo_sem_ad'] | df['filtro_ativo_nao_registrado']).sum() + if risco_inconsistencia_total > 0: + perc_risco_sem_ad = (risco_sem_ad_count / risco_inconsistencia_total) * 100 + perc_risco_maquina_nao_reg = (risco_maquina_nao_reg_count / risco_inconsistencia_total) * 100 + else: + perc_risco_sem_ad, perc_risco_maquina_nao_reg = 0, 0 + + # --- Geração das Seções por Gestor --- + html_secoes_gestores = "" + if COLUNAS["responsavel_direto"] in df.columns: + lista_gestores = sorted([g for g in df[COLUNAS["responsavel_direto"]].dropna().unique() if g]) + for gestor in lista_gestores: + df_equipe = df[df[COLUNAS["responsavel_direto"]] == gestor].copy() + contato_gestor_raw = df_equipe.iloc[0].get(COLUNAS["contato_responsavel"]) + contato_gestor_norm = normalizar_telefone(contato_gestor_raw) + contato_gestor_html = f'{contato_gestor_norm} ' if contato_gestor_norm else "Sem contato" + tentativas_contato = [] + for col_key in ["contato_1", "contato_2", "contato_3"]: + if COLUNAS[col_key] in df_equipe.columns and pd.notna(df_equipe.iloc[0].get(COLUNAS[col_key])): + tentativas_contato.append(str(df_equipe.iloc[0][COLUNAS[col_key]])) + contatos_realizados_html = f"
Tentativas de Contato ({len(tentativas_contato)}): {' | '.join(tentativas_contato) if tentativas_contato else 'Nenhuma registrada'}
" + html_secoes_gestores += f"

Gestor: {gestor} ({contato_gestor_html})

{contatos_realizados_html}" + total_equipe = len(df_equipe) + notebook_counts_equipe = df_equipe['Usa Notebook (Valor)'].value_counts() + sim_equipe, nao_equipe, ni_equipe = notebook_counts_equipe.get('sim', 0), notebook_counts_equipe.get('não', 0), notebook_counts_equipe.get('não informado', 0) + grafico_equipe = criar_grafico_pizza([sim_equipe, nao_equipe, ni_equipe], ['Usa Notebook', 'Não Usa', 'Não Informado'], f"Uso de Notebook na Equipe ({total_equipe} Colaboradores)") + + tabela_equipe_html = gerar_tabela_colaboradores(df_equipe) + html_secoes_gestores += f"
{grafico_equipe}{tabela_equipe_html}
" + + # FIX: Seção para colaboradores sem gestor + df_sem_gestor = df[df[COLUNAS["responsavel_direto"]].isnull()].copy() + if not df_sem_gestor.empty: + html_secoes_gestores += "

Colaboradores Sem Gestor Definido

" + tabela_sem_gestor_html = gerar_tabela_colaboradores(df_sem_gestor) + html_secoes_gestores += f"
{tabela_sem_gestor_html}
" + + + html_content = f""" + + Dashboard Consolidado de Gestão e Riscos + + + + + +

Dashboard Consolidado de Gestão e Riscos

+
+
+
Total de Colaboradores
+
{total_colaboradores}
+
+
+
Em Conformidade
+
{total_conformes} ({perc_conformes:.1f}%)
+
+
+
Fora de Conformidade
+
{total_nao_conformes} ({perc_nao_conformes:.1f}%)
+
+
+ +
+

Indicadores de Risco Alto ({total_risco_alto} Colaboradores)

+
+
Incontactáveis (TI)
{total_sem_contato_valido}
+
Equip. Próprio / Não Declarado
{risco_ativos_nao_declarados}
+
Drives Externos
{risco_drives_externos}
+
Sem E-mail Corporativo
{risco_sem_email_corp}
+
+ +

Indicadores de Risco Médio ({total_risco_medio} Colaboradores)

+
+
Sem Gestor Definido
{risco_sem_gestor}
+
Sem Tipo de Contrato
{risco_sem_contratacao}
+
Sem Status de Contrato
{risco_sem_status_contrato}
+
Sem Cargo Definido
{risco_sem_cargo}
+
+ +

Indicadores de Risco Baixo ({total_risco_baixo} Colaboradores)

+
+
Ativo sem Conta AD
{risco_sem_ad_count} ({perc_risco_sem_ad:.1f}%)
+
Ativo Não Registrado
{risco_maquina_nao_reg_count} ({perc_risco_maquina_nao_reg:.1f}%)
+
+
+ +
+
+

Análise por Equipe

+ {html_secoes_gestores} +
+
+ + + + + + """ + + with open('dashboard_interativo_final.html', 'w', encoding='utf-8') as f: + f.write(html_content) + print("Dashboard 'dashboard_interativo_final.html' com KPIs interativos e contagens corrigidas foi gerado com sucesso!") + +except (FileNotFoundError, ValueError) as e: + print(f"ERRO: {e}") +except Exception as e: + print(f"Ocorreu um erro inesperado: {e}") \ No newline at end of file diff --git a/scripts/normalizar_planilha.py b/scripts/normalizar_planilha.py new file mode 100644 index 0000000..998b825 --- /dev/null +++ b/scripts/normalizar_planilha.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +# Importa as bibliotecas necessárias +import pandas as pd +import re +import os +import sys + +print("Iniciando o script de normalização e enriquecimento de dados...") + +# --- 1. CONFIGURAÇÃO DOS CAMINHOS --- +base_path = os.path.expanduser('~') +caminho_dados_originais = os.path.join(base_path, 'Documents', 'Relatorio de Inventario GRUPO PRALOG', 'dados.xlsx') +caminho_inventario = os.path.join(base_path, 'Documents', 'Relatorio de Inventario GRUPO PRALOG', 'Inventario de Equipamentos Grupo Pralog - via RH (1).xlsx') +caminho_saida = os.path.join(base_path, 'Desktop', 'dados-normalizados.xlsx') + +# Configuração de Leitura do Excel de Inventário +linha_cabecalho = 0 + +# --- 2. FUNÇÕES DE NORMALIZAÇÃO E ATUALIZAÇÃO --- +def normalizar_email(email): + email_str = str(email).strip() + if '@' in email_str and email_str.lower() not in ['sem informação', 'n/a']: + return email_str.lower() + return email + +def normalizar_telefone(telefone): + if pd.isna(telefone): return telefone + telefone_str = str(telefone) + numeros = re.sub(r'\D', '', telefone_str) + if len(numeros) in [12, 13] and numeros.startswith('55'): + return f"'+{numeros}" + elif len(numeros) in [10, 11]: + return f"'+55{numeros}" + return telefone + +def normalizar_contrato(tipo_contrato): + if pd.isna(tipo_contrato): return tipo_contrato + tipo_str = str(tipo_contrato).strip().upper() + if tipo_str in ['MEI', 'PJ']: return 'Pessoa Juridica' + return str(tipo_contrato).strip() + +# --- 3. EXECUÇÃO DO SCRIPT --- +try: + # Carrega as planilhas, garantindo que tudo seja lido como texto e valores vazios sejam strings vazias + print(f"Lendo o arquivo principal: {caminho_dados_originais}") + df_dados = pd.read_excel(caminho_dados_originais, dtype=str).fillna('') + + print(f"Lendo o arquivo de inventário: {caminho_inventario} (usando a linha {linha_cabecalho + 1} como cabeçalho)") + df_inventario = pd.read_excel(caminho_inventario, header=linha_cabecalho, dtype=str).fillna('') + + # Limpeza automática dos nomes das colunas de AMBOS os arquivos + print("\nLimpando nomes de colunas de ambos os arquivos...") + df_dados.columns = df_dados.columns.str.strip().str.replace(r'\s+', ' ', regex=True) + df_inventario.columns = df_inventario.columns.str.strip().str.replace(r'\s+', ' ', regex=True) + + # Diagnóstico e Validação + colunas_necessarias_inventario = ['Colaborador Registrado', 'Tipo de Contrato', 'Unidade', 'Status'] + colunas_faltando = [col for col in colunas_necessarias_inventario if col not in df_inventario.columns] + if colunas_faltando: + print(f"❌ ERRO CRÍTICO: As colunas a seguir não foram encontradas no inventário: {colunas_faltando}") + sys.exit() + + # TAREFA A: Adicionar colaboradores ausentes + print("\nVerificando e adicionando colaboradores ausentes...") + df_inventario.dropna(subset=['Colaborador Registrado'], inplace=True) + nomes_dados = set(df_dados['NOME'].astype(str).str.strip().str.lower()) + nomes_inventario = set(df_inventario['Colaborador Registrado'].astype(str).str.strip().str.lower()) + nomes_a_adicionar_lower = nomes_inventario - nomes_dados + if nomes_a_adicionar_lower: + nomes_originais = df_inventario[df_inventario['Colaborador Registrado'].str.strip().str.lower().isin(nomes_a_adicionar_lower)]['Colaborador Registrado'].unique() + print(f"Encontrados {len(nomes_originais)} novos colaboradores para adicionar.") + novos_colaboradores_df = pd.DataFrame(nomes_originais, columns=['NOME']) + df_dados = pd.concat([df_dados, novos_colaboradores_df], ignore_index=True).fillna('') + else: + print("Nenhum novo colaborador a ser adicionado.") + + # TAREFA B: Normalizações Padrão + print("\nExecutando normalizações padrão (email e telefone)...") + df_dados['Email Pessoal Sendo Usado'] = df_dados['Email Pessoal Sendo Usado'].apply(normalizar_email) + df_dados['Email Corporativo'] = df_dados['Email Corporativo'].apply(normalizar_email) + df_dados['Contato Colaborador'] = df_dados['Contato Colaborador'].apply(normalizar_telefone) + df_dados['Contato do RESPONSÁVEL'] = df_dados['Contato do RESPONSÁVEL'].apply(normalizar_telefone) + + # TAREFA C: Lógica condicional para E-mails + print("Aplicando lógicas condicionais para preenchimento de e-mails...") + # Regra 1: Se 'Tem Email Corporativo?' for 'Não' E não houver e-mail pessoal, preenche 'Sem Informação' + cond_nao_tem_email = df_dados['Tem Email Corporativo?'].str.strip().str.lower() == 'não' + cond_email_pessoal_nao_existe = ~df_dados['Email Pessoal Sendo Usado'].str.contains('@', na=False) + mascara_regra1 = cond_nao_tem_email & cond_email_pessoal_nao_existe + df_dados.loc[mascara_regra1, 'Email Pessoal Sendo Usado'] = 'Sem Informação' + + # Regra 2: Se 'Tem Email Corporativo?' for 'Sim', preenche 'Email Pessoal Sendo Usado' com 'N/A' + cond_tem_email_sim = df_dados['Tem Email Corporativo?'].str.strip().str.lower() == 'sim' + df_dados.loc[cond_tem_email_sim, 'Email Pessoal Sendo Usado'] = 'N/A' + + # NOVO: Regra 3: Se 'Tem Email Corporativo?' for 'Não', preenche 'Email Corporativo' com 'N/A' + df_dados.loc[cond_nao_tem_email, 'Email Corporativo'] = 'N/A' + + # TAREFA D: Atualizar 'Contratação' e 'BASE' + print("Atualizando dados de 'Contratação' e 'BASE' a partir do inventário...") + df_inventario_sem_duplicatas = df_inventario.drop_duplicates(subset=['Colaborador Registrado'], keep='first').copy() + + df_inventario_sem_duplicatas.loc[:, 'Tipo de Contrato Normalizado'] = df_inventario_sem_duplicatas['Tipo de Contrato'].apply(normalizar_contrato) + mapa_contratacao = df_inventario_sem_duplicatas.set_index(df_inventario_sem_duplicatas['Colaborador Registrado'].str.strip().str.lower())['Tipo de Contrato Normalizado'] + novos_valores_contratacao = df_dados['NOME'].str.strip().str.lower().map(mapa_contratacao) + df_dados['Contratação'] = novos_valores_contratacao.combine_first(df_dados['Contratação']) + + mapa_base = df_inventario_sem_duplicatas.set_index(df_inventario_sem_duplicatas['Colaborador Registrado'].str.strip().str.lower())['Unidade'] + df_dados['nova_base'] = df_dados['NOME'].str.strip().str.lower().map(mapa_base) + def mesclar_base(row): + base_atual = str(row['BASE']).strip() + base_nova = str(row['nova_base']).strip() + if not base_nova: return base_atual + if not base_atual: return base_nova + if base_atual.lower() == base_nova.lower(): return base_atual + return f"{base_atual} / {base_nova}" + df_dados['BASE'] = df_dados.apply(mesclar_base, axis=1) + df_dados.drop(columns=['nova_base'], inplace=True) + + # TAREFA E: Atualizar status 'Usa Notebook da Empresa' + print("Atualizando status de 'Usa Notebook da Empresa'...") + df_em_uso = df_inventario[df_inventario['Status'].str.strip().str.lower() == 'em uso'] + nomes_com_notebook_em_uso = set(df_em_uso['Colaborador Registrado'].str.strip().str.lower()) + mascara_atualizacao_notebook = df_dados['NOME'].str.strip().str.lower().isin(nomes_com_notebook_em_uso) + df_dados.loc[mascara_atualizacao_notebook, 'Usa Notebook da Empresa'] = 'Sim' + + # NOVO: TAREFA F: Preenchimento de valores padrão em colunas vazias + print("Preenchendo valores padrão para colunas vazias...") + colunas_para_preencher = { + 'Usa Notebook da Empresa': 'Sem Informações', + 'Realmente e um Subordinado?': 'Sem Informações' + } + for coluna, valor_padrao in colunas_para_preencher.items(): + # A condição `== ''` funciona porque carregamos os dados com .fillna('') + df_dados.loc[df_dados[coluna] == '', coluna] = valor_padrao + + # --- 4. SALVAR O RESULTADO --- + print(f"\nSalvando o arquivo final em: {caminho_saida}") + df_dados.to_excel(caminho_saida, index=False) + print("\n✅ Script concluído com sucesso!") + +except FileNotFoundError as e: + print(f"❌ ERRO: Arquivo não encontrado! Verifique o caminho: {e.filename}") +except KeyError as e: + print(f"❌ ERRO DE CHAVE: A coluna {e} não foi encontrada. Verifique os nomes das colunas nos arquivos Excel.") +except Exception as e: + print(f"❌ Ocorreu um erro inesperado: {e}") \ No newline at end of file