worker-automate-hub 0.5.749__py3-none-any.whl → 0.5.912__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. worker_automate_hub/api/client.py +186 -68
  2. worker_automate_hub/api/rpa_historico_service.py +1 -0
  3. worker_automate_hub/cli.py +91 -111
  4. worker_automate_hub/tasks/jobs/abertura_livros_fiscais.py +112 -229
  5. worker_automate_hub/tasks/jobs/descartes.py +91 -77
  6. worker_automate_hub/tasks/jobs/devolucao_produtos.py +1386 -0
  7. worker_automate_hub/tasks/jobs/entrada_de_notas_15.py +3 -46
  8. worker_automate_hub/tasks/jobs/entrada_de_notas_22.py +833 -0
  9. worker_automate_hub/tasks/jobs/entrada_de_notas_36.py +29 -9
  10. worker_automate_hub/tasks/jobs/entrada_de_notas_37.py +619 -0
  11. worker_automate_hub/tasks/jobs/entrada_de_notas_39.py +1 -1
  12. worker_automate_hub/tasks/jobs/entrada_de_notas_9.py +63 -16
  13. worker_automate_hub/tasks/jobs/extracao_dados_nielsen.py +504 -0
  14. worker_automate_hub/tasks/jobs/extracao_saldo_estoque.py +242 -108
  15. worker_automate_hub/tasks/jobs/extracao_saldo_estoque_fiscal.py +688 -0
  16. worker_automate_hub/tasks/jobs/fidc_gerar_nosso_numero.py +2 -2
  17. worker_automate_hub/tasks/jobs/fidc_remessa_cobranca_cnab240.py +25 -16
  18. worker_automate_hub/tasks/jobs/geracao_balancetes_filial.py +330 -0
  19. worker_automate_hub/tasks/jobs/importacao_extratos.py +538 -0
  20. worker_automate_hub/tasks/jobs/importacao_extratos_748.py +800 -0
  21. worker_automate_hub/tasks/jobs/inclusao_pedidos_ipiranga.py +222 -0
  22. worker_automate_hub/tasks/jobs/inclusao_pedidos_raizen.py +174 -0
  23. worker_automate_hub/tasks/jobs/inclusao_pedidos_vibra.py +327 -0
  24. worker_automate_hub/tasks/jobs/notas_faturamento_sap.py +438 -157
  25. worker_automate_hub/tasks/jobs/opex_capex.py +540 -326
  26. worker_automate_hub/tasks/jobs/sped_fiscal.py +8 -8
  27. worker_automate_hub/tasks/jobs/transferencias.py +52 -41
  28. worker_automate_hub/tasks/task_definitions.py +46 -1
  29. worker_automate_hub/tasks/task_executor.py +11 -0
  30. worker_automate_hub/utils/util.py +252 -215
  31. worker_automate_hub/utils/utils_nfe_entrada.py +1 -1
  32. worker_automate_hub/worker.py +1 -9
  33. {worker_automate_hub-0.5.749.dist-info → worker_automate_hub-0.5.912.dist-info}/METADATA +4 -2
  34. {worker_automate_hub-0.5.749.dist-info → worker_automate_hub-0.5.912.dist-info}/RECORD +36 -25
  35. {worker_automate_hub-0.5.749.dist-info → worker_automate_hub-0.5.912.dist-info}/WHEEL +1 -1
  36. {worker_automate_hub-0.5.749.dist-info → worker_automate_hub-0.5.912.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,800 @@
1
+ # -*- coding: utf-8 -*-
2
+ import asyncio
3
+ import warnings
4
+ from datetime import datetime, date
5
+ import json
6
+ import io
7
+ import pyautogui
8
+ from pywinauto.application import Application, timings
9
+ from pywinauto import keyboard
10
+ from pywinauto import Desktop
11
+ from collections import defaultdict
12
+ from rich.console import Console
13
+ import getpass
14
+ import time
15
+ import re
16
+ import sys
17
+ import os
18
+ import shutil
19
+ import win32wnet # pywin32
20
+
21
+ sys.path.append(
22
+ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
23
+ )
24
+
25
+ from worker_automate_hub.api.client import (
26
+ get_config_by_name,
27
+ send_file,
28
+ get_notas_produtos,
29
+ )
30
+ from worker_automate_hub.models.dto.rpa_historico_request_dto import (
31
+ RpaHistoricoStatusEnum,
32
+ RpaRetornoProcessoDTO,
33
+ RpaTagDTO,
34
+ RpaTagEnum,
35
+ )
36
+ from worker_automate_hub.models.dto.rpa_processo_entrada_dto import (
37
+ RpaProcessoEntradaDTO,
38
+ )
39
+ from worker_automate_hub.utils.logger import logger
40
+ from pywinauto.keyboard import send_keys
41
+
42
+ from worker_automate_hub.utils.util import (
43
+ kill_all_emsys,
44
+ login_emsys,
45
+ type_text_into_field,
46
+ worker_sleep,
47
+ )
48
+
49
+ console = Console()
50
+ log = console.log
51
+
52
+ ASSETS_BASE_PATH = fr"assets\importacao_extratos"
53
+ # ASSETS_BASE_PATH = r"C:\Users\automatehub\Desktop\img_leo"
54
+ ano_atual = date.today().year
55
+
56
+ # === DESTINOS ===
57
+ DESTINO_BASE = fr"Z:\Nexera\Extrato\{ano_atual}" # tentativa principal (Z:)
58
+ DESTINO_IP_ROOT = r"\\fcaswfs01.ditrento.com.br\compartilhadas$" # root do share
59
+ DESTINO_BASE_IP = fr"{DESTINO_IP_ROOT}\Nexera\\{ano_atual}" # pasta final no UNC
60
+
61
+ EMPRESA = "1" # empresa fixa
62
+
63
+
64
+ async def importacao_extratos_748(task: RpaProcessoEntradaDTO) -> RpaRetornoProcessoDTO:
65
+ try:
66
+ # ======== PARÂMETROS ========
67
+ PASTA = r"Z:\Nexera\Extrato"
68
+ CONTEM = ["EXT_748_"] # padrões obrigatórios no nome do arquivo
69
+ EXT_PERMITIDAS = [".ret", ".txt"] # extensões permitidas (case-insensitive)
70
+ TEXTO_ALVO = "SIM REDE DE POSTOS" # texto a procurar no conteúdo
71
+ BTN_IMPORTAR_IMG = fr"{ASSETS_BASE_PATH}\btn_imp_arq.png"
72
+ BTN_CONCILIAR = fr"{ASSETS_BASE_PATH}\btn_conciliar.png"
73
+ DLG_TIT_RE = ".*Browse.*" # regex de título do diálogo de arquivo
74
+
75
+ IMAGEM_SUCESSO = fr"{ASSETS_BASE_PATH}\conciliados_sucesso.png"
76
+
77
+ EXT_STR = "/".join(EXT_PERMITIDAS).upper()
78
+
79
+ # Credenciais para fallback de rede
80
+ user_folder_login = await get_config_by_name("user_credentials")
81
+ user_folder_cfg = user_folder_login.conConfiguracao or {}
82
+
83
+ log("[cyan]Iniciando importacao_extratos[/] | empresa fixa = %s", EMPRESA)
84
+ log(
85
+ "Parâmetros -> pasta: %s | contem (obrigatório): %s | extensões: %s | texto alvo: %s",
86
+ PASTA,
87
+ CONTEM,
88
+ EXT_STR,
89
+ TEXTO_ALVO,
90
+ )
91
+
92
+ # ======== ACUMULADORES ========
93
+ selecionados = []
94
+ avaliados = 0
95
+ ignorados_nome = 0
96
+ nao_permitido = 0
97
+ lidos = 0
98
+ erros = 0
99
+
100
+ if not os.path.isdir(PASTA):
101
+ msg = f"Pasta não encontrada: {PASTA}"
102
+ log(f"[red]{msg}[/]")
103
+ raise SystemExit(msg)
104
+
105
+ log("Varredura em: %s", PASTA)
106
+
107
+ # ======== SCAN DE ARQUIVOS ========
108
+ for entry in os.scandir(PASTA):
109
+ if not entry.is_file():
110
+ continue
111
+
112
+ nome = entry.name
113
+ caminho = entry.path
114
+ upper_name = nome.upper()
115
+ lower_name = nome.lower()
116
+
117
+ # Seleciona somente arquivos que contenham os padrões em CONTEM
118
+ if not any(p.upper() in upper_name for p in CONTEM):
119
+ ignorados_nome += 1
120
+ log("Ignorado (não contém padrões obrigatórios): %s", nome)
121
+ continue
122
+
123
+ # Considera apenas arquivos com extensões permitidas
124
+ if not any(lower_name.endswith(ext.lower()) for ext in EXT_PERMITIDAS):
125
+ nao_permitido += 1
126
+ continue
127
+
128
+ avaliados += 1
129
+
130
+ # Procura o texto alvo dentro do arquivo (case-insensitive)
131
+ alvo_norm = TEXTO_ALVO.casefold()
132
+ encontrado = False
133
+
134
+ try:
135
+ with open(caminho, "r", encoding="latin1", errors="ignore") as f:
136
+ for linha in f:
137
+ if alvo_norm in linha.casefold():
138
+ encontrado = True
139
+ break
140
+ lidos += 1
141
+ except Exception as e:
142
+ erros += 1
143
+ log("[red]Erro lendo '%s': %s[/red]", nome, e)
144
+ continue
145
+
146
+ # Se encontrou o texto alvo, adiciona à lista de selecionados
147
+ if encontrado:
148
+ selecionados.append(
149
+ {
150
+ "arquivo": nome,
151
+ "caminho": caminho,
152
+ "tamanho_bytes": os.path.getsize(caminho),
153
+ }
154
+ )
155
+ log("[green]Selecionado[/green]: %s", caminho)
156
+
157
+ # ======== SAÍDA NO CONSOLE ========
158
+ log("======== RESULTADO DA VARREDURA ========")
159
+ log(
160
+ "Avaliados (ext permitidas): %s | Ignorados por nome: %s | Ignorados por extensão: %s | Lidos: %s | Erros leitura: %s",
161
+ avaliados,
162
+ ignorados_nome,
163
+ nao_permitido,
164
+ lidos,
165
+ erros,
166
+ )
167
+
168
+ # ======== VERIFICA RESULTADO ========
169
+ if not selecionados:
170
+ if ignorados_nome > 0:
171
+ msg = (
172
+ "Arquivos foram ignorados por nome conforme os padrões definidos: "
173
+ + ", ".join(CONTEM)
174
+ )
175
+ else:
176
+ msg = (
177
+ f"Nenhum arquivo {EXT_STR} contendo '{TEXTO_ALVO}' encontrado."
178
+ )
179
+ log(f"[yellow]{msg}[/yellow]")
180
+ return RpaRetornoProcessoDTO(
181
+ sucesso=False,
182
+ retorno=msg,
183
+ status=RpaHistoricoStatusEnum.Sucesso,
184
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Negocio)],
185
+ )
186
+
187
+ # ======== EMSys: LOGIN ========
188
+ log("Carregando credenciais com get_config_by_name('login_emsys')...")
189
+ config = await get_config_by_name("login_emsys")
190
+ log(
191
+ "[green]Credenciais carregadas[/green]. Tarefa recebida | empresa fixa = %s",
192
+ EMPRESA,
193
+ )
194
+
195
+ log("Verificando instâncias abertas do EMSys (kill_all_emsys)...")
196
+ await kill_all_emsys()
197
+ log("Iniciando EMSys...")
198
+ app = Application(backend="win32").start("C:\\Rezende\\EMSys3\\EMSys3_22.exe")
199
+ warnings.filterwarnings(
200
+ "ignore",
201
+ category=UserWarning,
202
+ message="32-bit application should be automated using 32-bit Python",
203
+ )
204
+ log("[green]EMSys iniciando[/green]...")
205
+
206
+ log("Realizando login no EMSys...")
207
+ return_login = await login_emsys(
208
+ config.conConfiguracao, app, task, filial_origem=EMPRESA
209
+ )
210
+ if not return_login.sucesso:
211
+ logger.info(f"\nError Message: {return_login.retorno}")
212
+ log(f"[red]Erro no login[/red]: {return_login.retorno}")
213
+ return return_login
214
+ log("[green]Login realizado com sucesso[/green].")
215
+
216
+ # ======== ABRE MÓDULO CONCILIADOR ========
217
+ log("Abrindo módulo: 'Conciliador Bancario 2.0'")
218
+ try:
219
+ type_text_into_field(
220
+ "Conciliador Bancario 2.0",
221
+ app["TFrmMenuPrincipal"]["Edit"],
222
+ True,
223
+ "50",
224
+ )
225
+ pyautogui.press("enter")
226
+ await worker_sleep(1)
227
+ pyautogui.press("down", presses=2, interval=0.2)
228
+ await worker_sleep(0.5)
229
+ pyautogui.press("enter")
230
+ log("[green]Módulo acionado[/green]. Aguardando carregamento...")
231
+ await worker_sleep(3)
232
+ except Exception as e:
233
+ log("[red]Falha ao abrir o módulo[/red]: %s", e)
234
+ return RpaRetornoProcessoDTO(
235
+ sucesso=False,
236
+ retorno=f"Falha ao abrir módulo Conciliador Bancario 2.0: {e}",
237
+ status=RpaHistoricoStatusEnum.Falha,
238
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)],
239
+ )
240
+
241
+ # ==== IMPORTAÇÃO: para cada arquivo selecionado ====
242
+ importados = []
243
+ movidos = []
244
+ falhas = []
245
+
246
+ for idx, sel in enumerate(selecionados, 1):
247
+ log(
248
+ "[bold cyan]Iniciando importação (%s/%s)[/bold cyan]: %s",
249
+ idx,
250
+ len(selecionados),
251
+ sel["arquivo"],
252
+ )
253
+
254
+ # Flag para controlar se chegamos na parte do conciliar com sucesso
255
+ conciliacao_ok = False
256
+
257
+ # 1) Clicar no botão "Importar Arquivo"
258
+ log("Procurando botão 'Importar Arquivo' pela imagem: %s", BTN_IMPORTAR_IMG)
259
+ for t in range(20):
260
+ pos = pyautogui.locateCenterOnScreen(
261
+ BTN_IMPORTAR_IMG, confidence=0.9
262
+ )
263
+ if pos:
264
+ pyautogui.click(pos)
265
+ log("[green]Botão encontrado e clicado[/green].")
266
+ break
267
+ await worker_sleep(0.5)
268
+ else:
269
+ msg = "Imagem do botão 'Importar Arquivo' não encontrada na tela."
270
+ log(f"[red]{msg}[/red]")
271
+ falhas.append({"arquivo": sel["arquivo"], "motivo": msg})
272
+ continue
273
+
274
+ await worker_sleep(2)
275
+
276
+ # 2) Conectar na janela de importação
277
+ try:
278
+ log(
279
+ "Conectando na janela 'TFrmImportarArquivoConciliadorBancario2' ..."
280
+ )
281
+ app_imp = Application(backend="win32").connect(
282
+ class_name="TFrmImportarArquivoConciliadorBancario2", timeout=1200
283
+ )
284
+ main_window = app_imp["TFrmImportarArquivoConciliadorBancario2"]
285
+ main_window.set_focus()
286
+ main_window.child_window(
287
+ class_name="TWinControl", found_index=0
288
+ ).click_input()
289
+ log("[green]Janela de importação focada[/green].")
290
+ await worker_sleep(3)
291
+ except Exception:
292
+ log(
293
+ "[yellow]Janela 'TFrmImportarArquivoConciliadorBancario2' não encontrada. Tentando seguir...[/yellow]"
294
+ )
295
+
296
+ # 3) Selecionar tipo de arquivo
297
+ log("Selecionando tipo de arquivo: digitando 'A' e [TAB]...")
298
+ try:
299
+ pyautogui.click(982, 632)
300
+ await worker_sleep(3)
301
+ pyautogui.write("A")
302
+ pyautogui.press("tab")
303
+ await worker_sleep(0.3)
304
+ log("[green]Tipo selecionado[/green].")
305
+ except Exception as e:
306
+ log(
307
+ "[yellow]Não foi possível interagir com o tipo de arquivo: %s[/yellow]",
308
+ e,
309
+ )
310
+
311
+ # 4) Capturar janela de diálogo de arquivo
312
+ log("Aguardando diálogo de arquivo (%s)...", DLG_TIT_RE)
313
+ dlg = None
314
+ for _ in range(30):
315
+ try:
316
+ app_dlg = Application().connect(title_re=DLG_TIT_RE)
317
+ dlg = app_dlg.window(title_re=DLG_TIT_RE)
318
+ if dlg.exists() and dlg.is_enabled():
319
+ break
320
+ except Exception:
321
+ pass
322
+ await worker_sleep(0.5)
323
+
324
+ if dlg is None or not dlg.exists():
325
+ msg = "Diálogo de arquivo (#32770) não apareceu."
326
+ log(f"[red]{msg}[/red]")
327
+ falhas.append({"arquivo": sel["arquivo"], "motivo": msg})
328
+ continue
329
+
330
+ log("[green]Diálogo de arquivo detectado[/green]. Preenchendo caminho...")
331
+ dlg.set_focus()
332
+ await worker_sleep(0.2)
333
+
334
+ # Campo Nome:Edit
335
+ try:
336
+ nome_edit = dlg.child_window(best_match="&Nome:Edit")
337
+ except Exception:
338
+ try:
339
+ nome_edit = dlg.child_window(class_name="Edit", found_index=0)
340
+ except Exception:
341
+ msg = "Campo de nome (Edit) não localizado no diálogo."
342
+ log(f"[red]{msg}[/red]")
343
+ falhas.append({"arquivo": sel["arquivo"], "motivo": msg})
344
+ continue
345
+
346
+ try:
347
+ nome_edit.click_input()
348
+ await worker_sleep(0.2)
349
+ send_keys("^a{BACKSPACE}")
350
+ send_keys(sel["caminho"])
351
+ await worker_sleep(0.2)
352
+ send_keys("{ENTER}")
353
+ log("Arquivo confirmado no diálogo: %s", sel["caminho"])
354
+
355
+ await worker_sleep(2)
356
+ pyautogui.click(1203, 509)
357
+
358
+ await worker_sleep(10)
359
+
360
+ # Aumenta a tolerância global
361
+ timings.after_clickinput_wait = 1
362
+
363
+ # 1) Aguarda a janela de mensagem (TMessageForm) após importação
364
+ try:
365
+ console.print(
366
+ "[yellow]Aguardando a janela de mensagem (TMessageForm) da importação...[/yellow]"
367
+ )
368
+
369
+ msg_form = Desktop(backend="win32").window(class_name="TMessageForm")
370
+ msg_form.wait("exists visible ready", timeout=300)
371
+
372
+ console.print(
373
+ "[green]✅ Janela TMessageForm de importação apareceu.[/green]"
374
+ )
375
+
376
+ try:
377
+ btn_ok = msg_form.child_window(title_re="OK|Ok|ok")
378
+ if btn_ok.exists():
379
+ btn_ok.click()
380
+ else:
381
+ msg_form.type_keys("{ENTER}")
382
+ except Exception:
383
+ pass
384
+
385
+ except Exception as e:
386
+ console.print(
387
+ f"[red]❌ Erro ao aguardar TMessageForm da importação: {e}[/red]"
388
+ )
389
+ falhas.append(
390
+ {
391
+ "arquivo": sel["arquivo"],
392
+ "motivo": f"Erro ao aguardar TMessageForm da importação: {e}",
393
+ }
394
+ )
395
+ continue
396
+
397
+ # 2) Agora conecta na janela do Conciliador
398
+ try:
399
+ console.print(
400
+ "[yellow]Conectando na janela do Conciliador Bancário...[/yellow]"
401
+ )
402
+
403
+ app_conc = Application(backend="win32").connect(
404
+ class_name="TFrmConciliadorBancario2",
405
+ timeout=60,
406
+ )
407
+
408
+ wnd = app_conc.window(class_name="TFrmConciliadorBancario2")
409
+ wnd.wait("visible enabled ready", timeout=60)
410
+
411
+ console.print(
412
+ "[green]✅ Janela do Conciliador carregada com sucesso![/green]"
413
+ )
414
+
415
+ except Exception as e:
416
+ console.print(
417
+ f"[red]❌ Erro ao conectar na janela do Conciliador: {e}[/red]"
418
+ )
419
+ falhas.append(
420
+ {
421
+ "arquivo": sel["arquivo"],
422
+ "motivo": f"Erro ao conectar na janela do Conciliador: {e}",
423
+ }
424
+ )
425
+ continue
426
+
427
+ # Agora sim, busca os campos numéricos
428
+ campos = wnd.descendants(class_name="TDBIEditNumber")
429
+
430
+ # 3) Valida quantidade de campos
431
+ if len(campos) < 5:
432
+ console.print(
433
+ "[red]Não existem campos suficientes (precisa de pelo menos 5).[/red]"
434
+ )
435
+ falhas.append(
436
+ {
437
+ "arquivo": sel["arquivo"],
438
+ "motivo": "Campos numéricos insuficientes no Conciliador.",
439
+ }
440
+ )
441
+ continue
442
+ else:
443
+ # ===== ESPERA CAMPOS ≠ 0,00 =====
444
+ TIMEOUT_CAMPOS = 30 * 60 # 30 minutos
445
+ inicio_espera = time.time()
446
+ valores_preenchidos = False
447
+
448
+ console.print(
449
+ "[cyan]Aguardando até que os campos 0 e 4 deixem de ser '0,00' ou vazios (timeout 30 min)...[/cyan]"
450
+ )
451
+
452
+ while True:
453
+ valor_0 = campos[0].window_text().strip()
454
+ valor_4 = campos[4].window_text().strip()
455
+
456
+ console.print(
457
+ f"[cyan]Leitura atual -> índice 0: '{valor_0}' | índice 4: '{valor_4}'[/cyan]"
458
+ )
459
+
460
+ cond_0 = valor_0 not in ("", "0,00", "0.00")
461
+ cond_4 = valor_4 not in ("", "0,00", "0.00")
462
+
463
+ if cond_0 and cond_4:
464
+ valores_preenchidos = True
465
+ console.print(
466
+ "[green]Campos preenchidos com valores diferentes de 0,00. Prosseguindo para comparação...[/green]"
467
+ )
468
+ break
469
+
470
+ if time.time() - inicio_espera > TIMEOUT_CAMPOS:
471
+ console.print(
472
+ "[red]Timeout de 30 minutos aguardando os campos saírem de 0,00.[/red]"
473
+ )
474
+ falhas.append(
475
+ {
476
+ "arquivo": sel["arquivo"],
477
+ "motivo": "Timeout aguardando campos numéricos saírem de 0,00.",
478
+ }
479
+ )
480
+ valores_preenchidos = False
481
+ break
482
+
483
+ await worker_sleep(5)
484
+
485
+ if not valores_preenchidos:
486
+ continue
487
+
488
+ console.print(f"[cyan]Valor índice 0 final:[/] {valor_0}")
489
+ console.print(f"[cyan]Valor índice 4 final:[/] {valor_4}")
490
+
491
+ if not (valor_0 == valor_4 and valor_0 not in ("", "0,00", "0.00")):
492
+ console.print(
493
+ "[yellow]Valores diferentes. Não será conciliado.[/yellow]"
494
+ )
495
+ falhas.append(
496
+ {
497
+ "arquivo": sel["arquivo"],
498
+ "motivo": f"Valores diferentes no Conciliador (0={valor_0}, 4={valor_4}).",
499
+ }
500
+ )
501
+ continue
502
+
503
+ console.print(
504
+ "[green]Valores iguais e válidos! Tentando clicar no botão 'Conciliar'...[/green]"
505
+ )
506
+ pos_conc = pyautogui.locateCenterOnScreen(
507
+ BTN_CONCILIAR, confidence=0.9
508
+ )
509
+ if not pos_conc:
510
+ console.print(
511
+ "[red]Botão 'Conciliar' não encontrado na tela.[/red]"
512
+ )
513
+ falhas.append(
514
+ {
515
+ "arquivo": sel["arquivo"],
516
+ "motivo": "Botão 'Conciliar' não localizado na tela.",
517
+ }
518
+ )
519
+ continue
520
+
521
+ pyautogui.click(pos_conc)
522
+ console.print(
523
+ "[green]Botão 'Conciliar' clicado com sucesso![/green]"
524
+ )
525
+
526
+ # ===== Aguardar imagem de sucesso + janela de confirmação =====
527
+ timeout = 1800 # 30 min
528
+ inicio = time.time()
529
+
530
+ console.print(
531
+ "[cyan]Aguardando imagem de sucesso aparecer e janela de confirmação da conciliação...[/cyan]"
532
+ )
533
+
534
+ pos_sucesso = None
535
+ janela_ok_encontrada = False
536
+
537
+ while time.time() - inicio < timeout:
538
+ # 1) Verifica imagem
539
+ pos_sucesso = pyautogui.locateCenterOnScreen(
540
+ IMAGEM_SUCESSO, confidence=0.85
541
+ )
542
+
543
+ if pos_sucesso:
544
+ console.print(
545
+ "[green]Imagem 'conciliados_sucesso' encontrada na tela.[/green]"
546
+ )
547
+
548
+ # 2) Agora aguarda a TMessageForm / Information
549
+ try:
550
+ console.print(
551
+ "[yellow]Aguardando janela 'Movimentos selecionados conciliados com sucesso!'...[/yellow]"
552
+ )
553
+ msg_ok = Desktop(backend="win32").window(
554
+ class_name="TMessageForm"
555
+ )
556
+ msg_ok.wait("exists visible ready", timeout=60)
557
+
558
+ console.print(
559
+ "[green]✅ Janela de confirmação da conciliação encontrada.[/green]"
560
+ )
561
+
562
+ try:
563
+ btn_ok = msg_ok.child_window(title_re="OK|Ok|ok")
564
+ if btn_ok.exists():
565
+ btn_ok.click()
566
+ else:
567
+ msg_ok.type_keys("{ENTER}")
568
+ except Exception:
569
+ # se der algo errado no botão, pelo menos a janela existe
570
+ msg_ok.type_keys("{ENTER}")
571
+
572
+ janela_ok_encontrada = True
573
+ except Exception as e:
574
+ console.print(
575
+ f"[red]❌ Não foi possível encontrar/confirmar a janela de conciliação: {e}[/red]"
576
+ )
577
+ janela_ok_encontrada = False
578
+
579
+ # Sai do while (já encontrou imagem, independente da janela ter dado certo ou não)
580
+ break
581
+
582
+ await worker_sleep(1)
583
+
584
+ # Se a imagem nunca apareceu, falha
585
+ if not pos_sucesso:
586
+ console.print(
587
+ "[red]Imagem 'conciliados_sucesso.png' NÃO apareceu dentro do tempo limite.[/red]"
588
+ )
589
+ falhas.append(
590
+ {
591
+ "arquivo": sel["arquivo"],
592
+ "motivo": "Imagem de sucesso da conciliação não apareceu.",
593
+ }
594
+ )
595
+ continue
596
+
597
+ # Se a janela não foi confirmada, considerar falha também
598
+ if not janela_ok_encontrada:
599
+ msg = "Janela de confirmação da conciliação não foi encontrada/confirmada."
600
+ console.print(f"[red]{msg}[/red]")
601
+ falhas.append({"arquivo": sel["arquivo"], "motivo": msg})
602
+ continue
603
+
604
+ # Só aqui marca como conciliado com sucesso
605
+ conciliacao_ok = True
606
+
607
+ # tenta fechar janela de erro, se existir
608
+ try:
609
+ app_err = Application(backend="win32").connect(
610
+ title="Erro", found_index=0
611
+ )
612
+ main_err = app_err["Erro"]
613
+ log("[yellow]Janela 'Erro' detectada. Fechando...[/yellow]")
614
+ main_err.close()
615
+ await worker_sleep(1)
616
+ except Exception:
617
+ pass
618
+
619
+ # ============ CHECAGEM: SÓ MOVE SE CONCILIAR OK ============
620
+ if not conciliacao_ok:
621
+ msg = (
622
+ "Conciliador não foi executado/confirmado com sucesso para o arquivo."
623
+ )
624
+ log(f"[red]{msg}[/red]")
625
+ falhas.append({"arquivo": sel["arquivo"], "motivo": msg})
626
+ continue
627
+
628
+ # ============ MOVER ARQUIVO ============
629
+ origem = sel["caminho"]
630
+ arquivo = sel["arquivo"]
631
+
632
+ try:
633
+ # 1) Tenta mover para o destino padrão (DESTINO_BASE - Z:)
634
+ os.makedirs(DESTINO_BASE, exist_ok=True)
635
+ destino_arquivo = os.path.join(DESTINO_BASE, arquivo)
636
+
637
+ # Sobrescrita segura se já existir
638
+ if os.path.exists(destino_arquivo):
639
+ try:
640
+ os.remove(destino_arquivo)
641
+ except Exception as e_rm:
642
+ log(
643
+ "[yellow]Aviso[/yellow]: não foi possível remover no destino: %s (%s)",
644
+ destino_arquivo,
645
+ e_rm,
646
+ )
647
+
648
+ shutil.move(origem, destino_arquivo)
649
+
650
+ # >>> registrar também quando move pelo Z: <<<
651
+ movidos.append(destino_arquivo)
652
+ importados.append(arquivo)
653
+
654
+ # LOG DE VALIDAÇÃO
655
+ exist_dest = os.path.exists(destino_arquivo)
656
+ try:
657
+ lista = os.listdir(os.path.dirname(destino_arquivo))
658
+ except Exception as e:
659
+ lista = [f"<<erro ao listar pasta: {e}>>"]
660
+
661
+ log(
662
+ "[green]Arquivo movido[/green]: %s -> %s (existe_destino=%s)",
663
+ origem,
664
+ destino_arquivo,
665
+ exist_dest,
666
+ )
667
+ log("Conteúdo da pasta destino após o move: %s", lista)
668
+
669
+ except Exception as e1:
670
+ # 2) Fallback via UNC (compartilhadas$ -> Nexera\{ano})
671
+ try:
672
+ usuario = user_folder_cfg.get("usuario")
673
+ senha = user_folder_cfg.get("senha")
674
+
675
+ if win32wnet is None:
676
+ raise RuntimeError(
677
+ "pywin32 não disponível para mapear caminho de rede (win32wnet)."
678
+ )
679
+
680
+ log(
681
+ "Falha ao mover para Z:. Tentando fallback via UNC em %s ...",
682
+ DESTINO_IP_ROOT,
683
+ )
684
+
685
+ try:
686
+ win32wnet.WNetAddConnection2(
687
+ 0,
688
+ None,
689
+ DESTINO_IP_ROOT,
690
+ None,
691
+ usuario,
692
+ senha,
693
+ )
694
+ except Exception as e_conn:
695
+ # Se der 1219 (conexão já existente), ignoramos
696
+ if getattr(e_conn, "winerror", None) != 1219:
697
+ raise
698
+
699
+ caminho_ip = DESTINO_BASE_IP
700
+ os.makedirs(caminho_ip, exist_ok=True)
701
+
702
+ if os.path.exists(origem):
703
+ destino_arquivo_ip = os.path.join(caminho_ip, arquivo)
704
+
705
+ if os.path.exists(destino_arquivo_ip):
706
+ try:
707
+ os.remove(destino_arquivo_ip)
708
+ except Exception as e_rm_ip:
709
+ log(
710
+ "[yellow]Aviso[/yellow]: não foi possível remover no destino IP: %s (%s)",
711
+ destino_arquivo_ip,
712
+ e_rm_ip,
713
+ )
714
+
715
+ shutil.move(origem, destino_arquivo_ip)
716
+ movidos.append(destino_arquivo_ip)
717
+ importados.append(arquivo)
718
+ log(
719
+ "[green]Arquivo movido (via IP)[/green]: %s -> %s",
720
+ origem,
721
+ destino_arquivo_ip,
722
+ )
723
+ else:
724
+ msg = (
725
+ f"Erro ao mover (via IP): origem não encontrada: {origem} | "
726
+ f"destino: {caminho_ip}"
727
+ )
728
+ log(f"[red]{msg}[/red]")
729
+ falhas.append({"arquivo": arquivo, "motivo": msg})
730
+ continue
731
+
732
+ except Exception as e2:
733
+ msg = f"Falha ao mover '{arquivo}' (fallback IP): {e2}"
734
+ log(f"[red]{msg}[/red]")
735
+ falhas.append({"arquivo": arquivo, "motivo": msg})
736
+ continue
737
+
738
+ except Exception as e:
739
+ msg = f"Falha geral no processamento do arquivo: {e}"
740
+ log(f"[red]{msg}[/red]")
741
+ falhas.append({"arquivo": sel["arquivo"], "motivo": msg})
742
+ continue
743
+
744
+ await worker_sleep(2)
745
+
746
+ # ======== RESUMO FINAL ========
747
+ log("======== RESUMO FINAL ========")
748
+ log("Importados (OK): %s", len(importados))
749
+ for n in importados:
750
+ log(" • %s", n)
751
+ if falhas:
752
+ log("[yellow]Falhas (%s):[/yellow]", len(falhas))
753
+ for f in falhas:
754
+ log(" • %s -> %s", f.get("arquivo"), f.get("motivo"))
755
+
756
+ # Se algum arquivo chegou até a conciliação/movimentação, sucesso
757
+ if importados:
758
+ retorno_payload = {
759
+ "empresa": EMPRESA,
760
+ "importados_count": len(importados),
761
+ "importados": importados,
762
+ "movidos_destino": movidos,
763
+ "falhas": falhas,
764
+ }
765
+ retorno_str = json.dumps(retorno_payload, ensure_ascii=False, indent=2)
766
+ return RpaRetornoProcessoDTO(
767
+ sucesso=True,
768
+ retorno=retorno_str,
769
+ status=RpaHistoricoStatusEnum.Sucesso,
770
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)],
771
+ )
772
+ else:
773
+ retorno_payload = {
774
+ "empresa": EMPRESA,
775
+ "importados_count": 0,
776
+ "falhas": falhas,
777
+ "mensagem": (
778
+ f"Nenhum arquivo foi conciliado/movido com sucesso. "
779
+ f"Verificar falhas detalhadas. (extensões permitidas: {EXT_STR})."
780
+ ),
781
+ }
782
+ retorno_str = json.dumps(retorno_payload, ensure_ascii=False, indent=2)
783
+ return RpaRetornoProcessoDTO(
784
+ sucesso=False,
785
+ retorno=retorno_str,
786
+ status=RpaHistoricoStatusEnum.Falha,
787
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Negocio)],
788
+ )
789
+
790
+ except Exception as ex:
791
+ log("[red]Exceção geral[/red]: %s", ex)
792
+ log(ex)
793
+ log("Traceback pode ser consultado no logger caso configurado.")
794
+ return RpaRetornoProcessoDTO(
795
+ sucesso=False,
796
+ retorno=f"Error: {ex}",
797
+ status=RpaHistoricoStatusEnum.Falha,
798
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Negocio)],
799
+ )
800
+