Files
sisponto/sisponto.py
Marcelo Amorim 574187eaf3 Inicial
2026-02-25 18:00:08 -03:00

441 lines
15 KiB
Python

from playwright.sync_api import sync_playwright
import time
import os
from datetime import datetime
import json
URL = "https://pmu.sisponto.com.br/Sispontoweb/open.do?sys=SPW"
# CREDENCIAIS FIXAS (TESTE)
USER = "04348941688"
PASS = "04348941688"
DEBUG_DIR = "/tmp/sisponto_debug"
def ts():
return datetime.now().strftime("%Y%m%d_%H%M%S")
def snap(page, label):
path = f"{DEBUG_DIR}/{ts()}_{label}.png"
try:
page.screenshot(path=path, full_page=True)
print(f"[SNAP] {path}")
except Exception as e:
print(f"[SNAP-FAIL] {label}: {e}")
def dump_frames(page, title):
print(f"\n==== FRAMES ({title}) ====")
for i, fr in enumerate(page.frames):
print(f"[{i}] name={fr.name!r} url={fr.url}")
print("==== END FRAMES ====\n")
def open_menu_via_barra(inner_frame):
# Abre o menu lateral sem depender de mouseenter/hover
print("[DEBUG] Aguardando elemento #barra_me_nu...")
# Tentar encontrar o elemento com retry
max_attempts = 3
for attempt in range(max_attempts):
try:
inner_frame.wait_for_selector("#barra_me_nu", timeout=20000)
print(f"[OK] Elemento #barra_me_nu encontrado (tentativa {attempt + 1})")
result = inner_frame.evaluate(
"""
() => {
const el = document.getElementById("barra_me_nu");
if (!el) return false;
el.style.marginLeft = "0px";
el.style.opacity = "1";
el.style.visibility = "visible";
return true;
}
"""
)
if result:
print("[OK] Menu aberto via JS")
return
else:
print(f"[WARN] Elemento não encontrado no DOM (tentativa {attempt + 1})")
except Exception as e:
print(f"[WARN] Erro ao abrir menu (tentativa {attempt + 1}): {e}")
if attempt < max_attempts - 1:
time.sleep(2)
else:
raise
def parse_grid_to_list(popup_frame):
"""
Extrai dados da grid a partir do padrão de IDs:
*.layout/data (container)
*.data.item:<row>.item:<col> (células)
e transforma no formato:
[
{"01/12/2025": ["10:05","11:32","12:32"]},
{"02/12/2025": ["10:26","11:27"]},
{"03/12/2025": ["folga"]},
...
]
"""
# (1) opcional: garantir que o container exista
container = popup_frame.locator('div[id*=".layout/data"]')
container.wait_for(state="attached", timeout=20000)
# (4) abordagem mais simples: pegar todas as células por padrão *.data.item:*.item:*
cells = popup_frame.locator('div[id*=".data.item:"][id*=".item:"]')
raw_rows = {}
count = cells.count()
for i in range(count):
el = cells.nth(i)
el_id = el.get_attribute("id") or ""
text = el.inner_text().strip()
# Ex: grid746802.data.item:0.item:1
try:
part = el_id.split(".data.item:", 1)[1] # "0.item:1"
row_str, col_str = part.split(".item:", 1) # "0", "1"
row_idx = int(row_str)
col_idx = int(col_str)
except Exception:
continue
raw_rows.setdefault(row_idx, {})[col_idx] = text
# Montagem final ordenada por índice de linha
result = []
for row_idx in sorted(raw_rows.keys()):
row = raw_rows[row_idx]
date = (row.get(0) or "").strip()
value = (row.get(1) or "").strip()
if not date:
continue
vlow = value.lower().strip()
if not value:
values = []
elif vlow == "folga":
values = ["folga"]
else:
# Ex: "- 10:05 - 11:32 - 12:32" => ["10:05","11:32","12:32"]
# Ex: "Observacao qualquer" => ["observacao qualquer"] (mantém como 1 item)
cleaned = value.replace("-", " ").strip()
parts = [p.strip() for p in cleaned.split() if p.strip()]
# Heurística simples: se tem tokens no formato HH:MM, assume lista de horários
times = [p for p in parts if len(p) == 5 and p[2] == ":" and p.replace(":", "").isdigit()]
if times:
values = times
else:
values = [value.strip()]
result.append({date: values})
return result
def process_timecard_data(data_list):
"""
Processa os dados de registro de ponto com as seguintes regras:
- Batidas pares: subtrai item 0 do item 1, item 2 do item 3, etc.
- Batidas ímpares e data != hoje: ignora
- Strings que não são horários: ignora
- Batidas ímpares e data == hoje: adiciona horário atual como último lançamento
Retorna um dicionário com o total de horas trabalhadas por dia.
"""
today_str = datetime.now().strftime("%d/%m/%Y")
current_time = datetime.now().strftime("%H:%M")
results = {}
for entry in data_list:
# Cada entry é um dict com uma única chave (a data)
if not entry:
continue
date = list(entry.keys())[0]
times = entry[date]
# Filtrar apenas horários válidos (formato HH:MM)
valid_times = []
for t in times:
t_str = str(t).strip()
# Verificar se é horário válido (HH:MM)
if len(t_str) == 5 and t_str[2] == ':' and t_str[:2].isdigit() and t_str[3:].isdigit():
valid_times.append(t_str)
num_times = len(valid_times)
# Se não há horários válidos, pular
if num_times == 0:
continue
# Se número ímpar de batidas
if num_times % 2 != 0:
# Se não é hoje, ignorar
if date != today_str:
continue
# Se é hoje, adicionar horário atual
valid_times.append(current_time)
num_times += 1
# Calcular total de horas trabalhadas (pares)
total_minutes = 0
for i in range(0, num_times, 2):
time_in = valid_times[i]
time_out = valid_times[i + 1]
# Converter para minutos
h_in, m_in = map(int, time_in.split(':'))
h_out, m_out = map(int, time_out.split(':'))
minutes_in = h_in * 60 + m_in
minutes_out = h_out * 60 + m_out
# Calcular diferença
diff = minutes_out - minutes_in
total_minutes += diff
# Converter total de minutos para horas:minutos
hours = total_minutes // 60
minutes = total_minutes % 60
results[date] = {
'total_minutes': total_minutes,
'total_formatted': f"{hours:02d}:{minutes:02d}",
'times': valid_times
}
return results
def main():
os.makedirs(DEBUG_DIR, exist_ok=True)
print(f"[DEBUG] Arquivos em: {DEBUG_DIR}")
with sync_playwright() as p:
browser = p.chromium.launch(
headless=False, # ver acontecendo
slow_mo=120 # desacelerar para debug
)
context = browser.new_context(
viewport={"width": 1500, "height": 950},
)
page = context.new_page()
page.on("console", lambda msg: print(f"[BROWSER-CONSOLE] {msg.type}: {msg.text}"))
page.on("pageerror", lambda exc: print(f"[BROWSER-ERROR] {exc}"))
# ============================================================
# ABERTURA
# ============================================================
print("[1] Abrindo URL...")
page.goto(URL, wait_until="domcontentloaded")
snap(page, "01_abriu_url")
dump_frames(page, "apos_open")
# ============================================================
# LOGIN (iframe mainform)
# ============================================================
print("[2] Login...")
login_frame = page.frame(name="mainform")
if not login_frame:
raise RuntimeError("Iframe mainform (login) não encontrado")
login_frame.locator('input[name="WFRInput744594"]').wait_for(state="visible", timeout=15000)
login_frame.locator('input[name="WFRInput744594"]').fill(USER)
login_frame.locator('input[name="WFRInput744595"]').fill(PASS)
snap(page, "02_login_preenchido")
login_frame.locator("div.HTMLButton_Logar").wait_for(state="visible", timeout=15000)
login_frame.locator("div.HTMLButton_Logar").click()
snap(page, "03_click_logar")
# ============================================================
# ESPERAR TELA PRINCIPAL (openform.do)
# ============================================================
print("[3] Aguardando openform.do pós-login...")
inner_frame = None
t0 = time.time()
timeout = 40 # aumentar timeout para 40 segundos
while time.time() - t0 < timeout:
# após login, o frame mainform muda de URL
for fr in page.frames:
url = fr.url or ""
name = fr.name or ""
# Procurar por frame mainform com openform.do OU frame sem nome com openform.do
if ("openform.do" in url and "formID" in url) and (name == "mainform" or name == ""):
inner_frame = fr
print(f"[DEBUG] Frame encontrado: name={name!r} url={url[:80]}...")
break
if inner_frame:
break
time.sleep(0.3)
# Debug a cada 5 segundos
if int(time.time() - t0) % 5 == 0:
print(f"[DEBUG] Ainda aguardando... ({int(time.time() - t0)}s)")
dump_frames(page, "apos_login")
if not inner_frame:
snap(page, "ERRO_openform_nao_apareceu")
print("[ERRO] Frames disponíveis:")
for i, fr in enumerate(page.frames):
print(f" [{i}] name={fr.name!r} url={fr.url}")
raise RuntimeError("openform.do não apareceu após login")
print(f"[OK] inner_frame: name={inner_frame.name!r} url={inner_frame.url}")
snap(page, "04_openform_ok")
# Aguardar estabilização do frame
print("[DEBUG] Aguardando estabilização do frame...")
time.sleep(2)
# Verificar se o frame ainda está válido
try:
test_url = inner_frame.url
print(f"[DEBUG] Frame URL ainda válida: {test_url[:80]}...")
except Exception as e:
print(f"[WARN] Frame pode ter sido destruído, procurando novamente: {e}")
# Re-procurar o frame
for fr in page.frames:
url = fr.url or ""
if "openform.do" in url and "formID" in url:
inner_frame = fr
print(f"[OK] Frame re-encontrado: {url[:80]}...")
break
# ============================================================
# ABRIR MENU VIA JS (barra_me_nu)
# ============================================================
print('[4] Abrindo menu via JS: #barra_me_nu.style.marginLeft="0px"')
# Aguardar página estar completamente carregada
try:
inner_frame.wait_for_load_state("domcontentloaded", timeout=10000)
inner_frame.wait_for_load_state("networkidle", timeout=15000)
except Exception as e:
print(f"[WARN] Timeout aguardando load_state: {e}")
time.sleep(1.5) # Aguardar um pouco mais antes de abrir o menu
open_menu_via_barra(inner_frame)
time.sleep(0.8)
snap(page, "05_menu_aberto_barra_me_nu")
# ============================================================
# NAVEGAR MENU: Menu do Colaborador -> Consultas -> Consulta do ponto
# ============================================================
print("[5] Menu do Colaborador...")
m1 = inner_frame.locator("text=Menu do Colaborador").first
m1.wait_for(state="visible", timeout=15000)
m1.hover()
time.sleep(0.25)
snap(page, "06_hover_menu_do_colaborador")
print("[6] Consultas...")
m2 = inner_frame.locator("text=Consultas").first
m2.wait_for(state="visible", timeout=15000)
m2.hover()
time.sleep(0.25)
snap(page, "07_hover_consultas")
print("[7] Clique em Consulta do ponto (popup)...")
m3 = inner_frame.locator("text=Consulta do ponto").first
m3.wait_for(state="visible", timeout=15000)
m3.hover()
time.sleep(0.15)
snap(page, "08_hover_consulta_do_ponto")
with context.expect_page(timeout=20000) as popup_info:
m3.click()
popup = popup_info.value
popup.wait_for_load_state("domcontentloaded")
popup.wait_for_load_state("networkidle")
time.sleep(0.5)
snap(popup, "09_popup_aberto")
# ============================================================
# ESCOLHER FRAME DO POPUP (se houver iframe interno)
# ============================================================
print("[8] Detectando frame final no popup...")
for i, fr in enumerate(popup.frames):
print(f"[POPUP-FRAME {i}] name={fr.name!r} url={fr.url}")
popup_frame = popup.main_frame
for fr in popup.frames:
if fr != popup.main_frame and fr.url and "about:blank" not in fr.url:
popup_frame = fr
break
print(f"[OK] popup_frame: name={popup_frame.name!r} url={popup_frame.url}")
snap(popup, "10_popup_frame_ok")
# ============================================================
# EXTRAÇÃO DA GRID E TRANSFORMAÇÃO
# ============================================================
print("[9] Extraindo grid (*.layout/data / *.data.item:*.item:*) ...")
data_list = parse_grid_to_list(popup_frame)
out_json = f"{DEBUG_DIR}/{ts()}_resultado.json"
with open(out_json, "w", encoding="utf-8") as f:
json.dump(data_list, f, ensure_ascii=False, indent=2)
print(f"[OK] Resultado salvo em: {out_json}")
print("[OK] Todos os registros extraídos:")
for item in data_list:
print(" ", item)
# ============================================================
# PROCESSAMENTO DE HORAS TRABALHADAS
# ============================================================
print("\n[10] Processando horas trabalhadas...")
processed_data = process_timecard_data(data_list)
out_processed = f"{DEBUG_DIR}/{ts()}_horas_trabalhadas.json"
with open(out_processed, "w", encoding="utf-8") as f:
json.dump(processed_data, f, ensure_ascii=False, indent=2)
print(f"[OK] Horas trabalhadas salvas em: {out_processed}")
print("[OK] Horas trabalhadas:")
for date, info in processed_data.items():
print(f" {date}: {info['total_formatted']}")
# Também salva o HTML do frame (útil para validar)
html_path = f"{DEBUG_DIR}/{ts()}_popup_frame.html"
with open(html_path, "w", encoding="utf-8") as f:
f.write(popup_frame.content())
print(f"[OK] HTML do popup_frame salvo em: {html_path}")
# ============================================================
# PAUSA PARA INSPEÇÃO
# ============================================================
#print("\n[PAUSE] Inspector aberto. Clique Resume para encerrar.\n")
#page.pause()
context.close()
browser.close()
if __name__ == "__main__":
main()