Roudrigus commited on
Commit
d310f8c
·
verified ·
1 Parent(s): 0033c7b

Update outlook_relatorio.py

Browse files
Files changed (1) hide show
  1. outlook_relatorio.py +77 -61
outlook_relatorio.py CHANGED
@@ -1,12 +1,15 @@
1
-
2
  # -*- coding: utf-8 -*-
3
- import streamlit as st
4
- import pandas as pd
5
- from datetime import datetime, timedelta, date
6
  import io
7
  import re
8
  import unicodedata
9
- import pythoncom # COM init/finalize para evitar 'CoInitialize não foi chamado'
 
 
 
 
 
10
 
11
  # (opcional) auditoria, se existir no seu projeto
12
  try:
@@ -15,6 +18,10 @@ try:
15
  except Exception:
16
  _HAS_AUDIT = False
17
 
 
 
 
 
18
  # ==============================
19
  # 🎨 Estilos (UX)
20
  # ==============================
@@ -73,30 +80,33 @@ def _build_downloads(df: pd.DataFrame, base_name: str):
73
 
74
  # Excel (com autoajuste de larguras)
75
  xlsx_buf = io.BytesIO()
76
- with pd.ExcelWriter(xlsx_buf, engine="openpyxl") as writer:
77
- df.to_excel(writer, index=False, sheet_name="Relatorio")
78
- ws = writer.sheets["Relatorio"]
79
-
80
- # 🔎 Ajuste automático de largura das colunas
81
- from openpyxl.utils import get_column_letter
82
- for col_idx, col_cells in enumerate(ws.columns, start=1):
83
- max_len = 0
84
- for cell in col_cells:
85
- try:
86
- v = "" if cell.value is None else str(cell.value)
87
- max_len = max(max_len, len(v))
88
- except Exception:
89
- pass
90
- ws.column_dimensions[get_column_letter(col_idx)].width = min(max_len + 2, 60)
 
91
 
92
- xlsx_buf.seek(0)
93
- st.download_button(
94
- "⬇️ Baixar Excel",
95
- data=xlsx_buf,
96
- file_name=f"{base_name}.xlsx",
97
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
98
- key=f"dl_xlsx_{base_name}"
99
- )
 
 
100
 
101
  # PDF (resumo até 100 linhas)
102
  try:
@@ -174,7 +184,7 @@ def _ensure_client_exploded(df: pd.DataFrame) -> pd.DataFrame:
174
  return df2
175
 
176
  # ==============================
177
- # ✅ NOVO: visão da Tabela (cliente único + Data/Hora + ícones)
178
  # ==============================
179
  def _build_table_view_unique_client(df: pd.DataFrame) -> pd.DataFrame:
180
  """
@@ -250,8 +260,6 @@ def _build_table_view_unique_client(df: pd.DataFrame) -> pd.DataFrame:
250
  # ==============================
251
  # Indicadores (visual + profissional)
252
  # ==============================
253
- import plotly.express as px
254
-
255
  def _render_indicators_custom(df: pd.DataFrame, dt_col_name: str, cols_for_topn: list[str], topn_default: int = 10):
256
  """
257
  Indicadores com:
@@ -623,7 +631,7 @@ def _format_plate(p: str) -> str:
623
  return f"{p0[:3]}-{p0[3:]}"
624
  return p0
625
 
626
- def _extract_plate_from_text(text: str) -> str | None:
627
  """Extrai placa do texto com robustez."""
628
  if not text:
629
  return None
@@ -662,21 +670,21 @@ def _extract_plate_from_text(text: str) -> str | None:
662
  return None
663
 
664
  # ====== Sanitização de CLIENTE ======
665
- def _sanitize_cliente(s: str | None) -> str | None:
666
  if not s:
667
  return s
668
  s2 = re.sub(r"\s*:\s*", " ", s)
669
  s2 = re.sub(r"\s+", " ", s2).strip(" -:|")
670
  return s2.strip()
671
 
672
- def _split_semicolon(s: str | None) -> list[str] | None:
673
  if not s:
674
  return None
675
  parts = [p.strip(" -:|").strip() for p in s.split(";")]
676
  parts = [p for p in parts if p]
677
  return parts or None
678
 
679
- def _cliente_to_list(cliente_raw: str | None) -> list[str] | None:
680
  s = _sanitize_cliente(cliente_raw)
681
  return _split_semicolon(s)
682
 
@@ -1007,8 +1015,16 @@ def gerar_relatorio_outlook_desktop_multi(
1007
  filtro_remetente: str = "",
1008
  extrair_campos: bool = True
1009
  ) -> pd.DataFrame:
1010
- """Inicializa COM, conecta ao Outlook, lê múltiplas pastas (todas as stores) e finaliza COM."""
 
 
 
 
 
 
 
1011
  try:
 
1012
  import win32com.client
1013
  pythoncom.CoInitialize()
1014
  ns = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
@@ -1391,28 +1407,31 @@ def main():
1391
  st.success("Cache limpo. Gere novamente os dados.")
1392
 
1393
  if submit_test:
1394
- try:
1395
- import win32com.client
1396
- pythoncom.CoInitialize()
1397
- ns = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
1398
- stores = ns.Folders.Count
1399
- inbox_def = ns.GetDefaultFolder(6)
1400
- sent_def = ns.GetDefaultFolder(5)
1401
- st.info(f"Stores detectadas: **{stores}** | Inbox padrão: **{inbox_def.Name}** | Sent: **{sent_def.Name}**")
1402
- if pastas_escolhidas:
1403
- for p in pastas_escolhidas:
1404
- try:
1405
- f = _get_folder_by_path_any_store(ns, p)
1406
- st.write(f"📁 {p} → '{f.Name}' itens: {f.Items.Count}")
1407
- except Exception as e:
1408
- st.warning(f"[Teste] Falha ao acessar '{p}': {e}")
1409
- except Exception as e:
1410
- st.error(f"[Teste] Erro ao conectar: {e}")
1411
- finally:
1412
  try:
1413
- pythoncom.CoUninitialize()
1414
- except Exception:
1415
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1416
 
1417
  if submit_fresh:
1418
  if not pastas_escolhidas:
@@ -1618,7 +1637,4 @@ def main():
1618
  pass
1619
 
1620
  else:
1621
- st.info("👉 Selecione a caixa (Entrada/Saída), opcionalmente uma subpasta, e clique em **🔍 Gerar (com cache)** ou **⚡ Atualizar agora (sem cache)**. Use **🧪 Teste de conexão** se vier vazio.")
1622
-
1623
- # if __name__ == "__main__":
1624
- # main()
 
 
1
  # -*- coding: utf-8 -*-
2
+ import os
3
+ import platform
 
4
  import io
5
  import re
6
  import unicodedata
7
+ from datetime import datetime, timedelta, date
8
+ from typing import Optional
9
+
10
+ import streamlit as st
11
+ import pandas as pd
12
+ import plotly.express as px
13
 
14
  # (opcional) auditoria, se existir no seu projeto
15
  try:
 
18
  except Exception:
19
  _HAS_AUDIT = False
20
 
21
+ # Ambiente / Flags
22
+ _IS_WINDOWS = platform.system().lower() == "windows"
23
+ _DISABLE_OUTLOOK = os.getenv("DISABLE_OUTLOOK", "0") == "1" # permite desativar mesmo no Windows
24
+
25
  # ==============================
26
  # 🎨 Estilos (UX)
27
  # ==============================
 
80
 
81
  # Excel (com autoajuste de larguras)
82
  xlsx_buf = io.BytesIO()
83
+ try:
84
+ with pd.ExcelWriter(xlsx_buf, engine="openpyxl") as writer:
85
+ df.to_excel(writer, index=False, sheet_name="Relatorio")
86
+ ws = writer.sheets["Relatorio"]
87
+
88
+ # 🔎 Ajuste automático de largura das colunas
89
+ from openpyxl.utils import get_column_letter
90
+ for col_idx, col_cells in enumerate(ws.columns, start=1):
91
+ max_len = 0
92
+ for cell in col_cells:
93
+ try:
94
+ v = "" if cell.value is None else str(cell.value)
95
+ max_len = max(max_len, len(v))
96
+ except Exception:
97
+ pass
98
+ ws.column_dimensions[get_column_letter(col_idx)].width = min(max_len + 2, 60)
99
 
100
+ xlsx_buf.seek(0)
101
+ st.download_button(
102
+ "⬇️ Baixar Excel",
103
+ data=xlsx_buf,
104
+ file_name=f"{base_name}.xlsx",
105
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
106
+ key=f"dl_xlsx_{base_name}"
107
+ )
108
+ except Exception as e:
109
+ st.info(f"Excel: não foi possível gerar o arquivo (openpyxl). Detalhe: {e}")
110
 
111
  # PDF (resumo até 100 linhas)
112
  try:
 
184
  return df2
185
 
186
  # ==============================
187
+ # ✅ Visão da Tabela (cliente único + Data/Hora + ícones)
188
  # ==============================
189
  def _build_table_view_unique_client(df: pd.DataFrame) -> pd.DataFrame:
190
  """
 
260
  # ==============================
261
  # Indicadores (visual + profissional)
262
  # ==============================
 
 
263
  def _render_indicators_custom(df: pd.DataFrame, dt_col_name: str, cols_for_topn: list[str], topn_default: int = 10):
264
  """
265
  Indicadores com:
 
631
  return f"{p0[:3]}-{p0[3:]}"
632
  return p0
633
 
634
+ def _extract_plate_from_text(text: str) -> Optional[str]:
635
  """Extrai placa do texto com robustez."""
636
  if not text:
637
  return None
 
670
  return None
671
 
672
  # ====== Sanitização de CLIENTE ======
673
+ def _sanitize_cliente(s: Optional[str]) -> Optional[str]:
674
  if not s:
675
  return s
676
  s2 = re.sub(r"\s*:\s*", " ", s)
677
  s2 = re.sub(r"\s+", " ", s2).strip(" -:|")
678
  return s2.strip()
679
 
680
+ def _split_semicolon(s: Optional[str]) -> Optional[list[str]]:
681
  if not s:
682
  return None
683
  parts = [p.strip(" -:|").strip() for p in s.split(";")]
684
  parts = [p for p in parts if p]
685
  return parts or None
686
 
687
+ def _cliente_to_list(cliente_raw: Optional[str]) -> Optional[list[str]]:
688
  s = _sanitize_cliente(cliente_raw)
689
  return _split_semicolon(s)
690
 
 
1015
  filtro_remetente: str = "",
1016
  extrair_campos: bool = True
1017
  ) -> pd.DataFrame:
1018
+ """
1019
+ Inicializa COM, conecta ao Outlook, lê múltiplas pastas (todas as stores) e finaliza COM.
1020
+ ⚠️ Disponível apenas em Windows (Outlook Desktop via pywin32).
1021
+ """
1022
+ if not _IS_WINDOWS or _DISABLE_OUTLOOK:
1023
+ st.info("📌 Leitura via Outlook Desktop está indisponível neste ambiente.")
1024
+ return pd.DataFrame()
1025
+
1026
  try:
1027
+ import pythoncom
1028
  import win32com.client
1029
  pythoncom.CoInitialize()
1030
  ns = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
 
1407
  st.success("Cache limpo. Gere novamente os dados.")
1408
 
1409
  if submit_test:
1410
+ if not _IS_WINDOWS or _DISABLE_OUTLOOK:
1411
+ st.info("Teste indisponível neste ambiente (Linux/Spaces).")
1412
+ else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1413
  try:
1414
+ import pythoncom, win32com.client
1415
+ pythoncom.CoInitialize()
1416
+ ns = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
1417
+ stores = ns.Folders.Count
1418
+ inbox_def = ns.GetDefaultFolder(6)
1419
+ sent_def = ns.GetDefaultFolder(5)
1420
+ st.info(f"Stores detectadas: **{stores}** | Inbox padrão: **{inbox_def.Name}** | Sent: **{sent_def.Name}**")
1421
+ if pastas_escolhidas:
1422
+ for p in pastas_escolhidas:
1423
+ try:
1424
+ f = _get_folder_by_path_any_store(ns, p)
1425
+ st.write(f"📁 {p} → '{f.Name}' itens: {f.Items.Count}")
1426
+ except Exception as e:
1427
+ st.warning(f"[Teste] Falha ao acessar '{p}': {e}")
1428
+ except Exception as e:
1429
+ st.error(f"[Teste] Erro ao conectar: {e}")
1430
+ finally:
1431
+ try:
1432
+ pythoncom.CoUninitialize()
1433
+ except Exception:
1434
+ pass
1435
 
1436
  if submit_fresh:
1437
  if not pastas_escolhidas:
 
1637
  pass
1638
 
1639
  else:
1640
+ st.info("👉 Selecione a caixa (Entrada/Saída), opcionalmente uma subpasta, e clique em **🔍 Gerar (com cache)** ou **⚡ Atualizar agora (sem cache)**. Use **🧪 Teste de conexão** se vier vazio.")