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,688 @@
1
+ import asyncio
2
+ import os
3
+ from datetime import datetime
4
+ from pywinauto import Application, timings, findwindows, keyboard, Desktop
5
+ import sys
6
+ import io
7
+ import win32gui
8
+ import pyperclip
9
+ from unidecode import unidecode
10
+
11
+ # sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
12
+
13
+ from worker_automate_hub.models.dto.rpa_historico_request_dto import (
14
+ RpaHistoricoStatusEnum,
15
+ RpaRetornoProcessoDTO,
16
+ RpaTagDTO,
17
+ RpaTagEnum,
18
+ )
19
+ from worker_automate_hub.models.dto.rpa_processo_entrada_dto import (
20
+ RpaProcessoEntradaDTO,
21
+ )
22
+ from rich.console import Console
23
+ import re
24
+ import time
25
+ from pywinauto.keyboard import send_keys
26
+ import warnings
27
+ from pywinauto.application import Application
28
+ from worker_automate_hub.api.client import get_config_by_name, send_file
29
+ from worker_automate_hub.utils.util import (
30
+ kill_all_emsys,
31
+ login_emsys_fiscal,
32
+ set_variable,
33
+ type_text_into_field,
34
+ worker_sleep,
35
+ )
36
+ from pywinauto_recorder.player import set_combobox
37
+
38
+ from datetime import timedelta
39
+ import pyautogui
40
+ from worker_automate_hub.utils.logger import logger
41
+ from worker_automate_hub.utils.utils_nfe_entrada import EMSys
42
+
43
+ emsys = EMSys()
44
+
45
+ console = Console()
46
+ pyautogui.PAUSE = 0.5
47
+ pyautogui.FAILSAFE = False
48
+
49
+ # -------- Configs de velocidade --------
50
+ SLEEP_BETWEEN_KEYS = 0.03 # 30ms entre DOWNs
51
+ SLEEP_AFTER_COPY = 0.06 # 60ms após Ctrl+C
52
+
53
+ # -------- Utilidades --------
54
+ def _get_main_window_by_class(timeout_s: int = 5):
55
+ app = Application(backend="win32").connect(class_name="TFrmMovtoLivroFiscal", timeout=timeout_s)
56
+ win = app.window(class_name="TFrmMovtoLivroFiscal")
57
+ try: win.set_focus()
58
+ except Exception: pass
59
+ return app, win
60
+
61
+ def _find_grid_descendant(win):
62
+ # pega o maior TcxGridSite/TcxGrid
63
+ best, best_area = None, -1
64
+ for d in win.descendants():
65
+ try:
66
+ cls = (d.class_name() or "").lower()
67
+ if "tcxgridsite" in cls or "tcxgrid" in cls:
68
+ r = d.rectangle()
69
+ area = max(0,(r.right-r.left)) * max(0,(r.bottom-r.top))
70
+ if area > best_area:
71
+ best, best_area = d, area
72
+ except:
73
+ pass
74
+ if not best:
75
+ raise RuntimeError("Grid não localizado (TcxGrid/TcxGridSite).")
76
+ return best
77
+
78
+ def _copy_active_row_text(retries: int = 1) -> str:
79
+ pyperclip.copy("")
80
+ send_keys("^c")
81
+ time.sleep(SLEEP_AFTER_COPY)
82
+ txt = pyperclip.paste().strip()
83
+ if txt or retries <= 0:
84
+ return txt
85
+ # 1 tentativa extra (às vezes a 1ª vem vazia)
86
+ return _copy_active_row_text(retries-1)
87
+
88
+ def _linha_bate_criterio(txt: str, mes: str, ano: str) -> bool:
89
+ if not txt:
90
+ return False
91
+ t = unidecode(re.sub(r"\s+"," ", txt)).lower()
92
+ # dd/MM/AAAA do mês/ano desejado
93
+ if not re.search(rf"\b(0[1-9]|[12]\d|3[01])/{mes}/{ano}\b", t):
94
+ return False
95
+ return "livro - inventario" in t
96
+
97
+ # -------- Varredura assumindo que JÁ ESTÁ na 1ª linha --------
98
+ def selecionar_inventario_por_competencia(competencia_mm_aaaa: str, max_linhas: int = 800) -> bool:
99
+ """
100
+ Pré-condição: o foco já está na PRIMEIRA LINHA do grid (você clicou por coordenada).
101
+ A função só navega com SETA-PARA-BAIXO até encontrar a linha alvo e PARA.
102
+ """
103
+ # Selecionar primeira linha inventario
104
+ pyautogui.click(928, 475)
105
+ time.sleep(1)
106
+ m = re.fullmatch(r"(\d{2})/(\d{4})", competencia_mm_aaaa.strip())
107
+ if not m:
108
+ raise ValueError("Competência deve ser MM/AAAA (ex.: '09/2025').")
109
+ mes, ano = m.groups()
110
+
111
+ # garantir foco na janela/grid (não move o cursor de linha)
112
+ _, win = _get_main_window_by_class()
113
+ grid = _find_grid_descendant(win)
114
+ try:
115
+ grid.set_focus()
116
+ except Exception:
117
+ pass
118
+
119
+ for i in range(max_linhas):
120
+ linha = _copy_active_row_text()
121
+ if _linha_bate_criterio(linha, mes, ano):
122
+ # ✅ Linha correta já está selecionada (sem cliques)
123
+ # print(f"[OK] Encontrado em {i+1} passos: {linha}")
124
+ return True
125
+ send_keys("{DOWN}")
126
+ time.sleep(SLEEP_BETWEEN_KEYS)
127
+
128
+ # print("[WARN] Não encontrado dentro do limite de linhas.")
129
+ return False
130
+
131
+
132
+ async def extracao_saldo_estoque_fiscal(
133
+ task: RpaProcessoEntradaDTO,
134
+ ) -> RpaRetornoProcessoDTO:
135
+ try:
136
+ config = await get_config_by_name("login_emsys_fiscal")
137
+ periodo = task.configEntrada["periodo"]
138
+ periodo_format = periodo.replace("/", "")
139
+ filial = task.configEntrada["filialEmpresaOrigem"]
140
+ historico_id = task.historico_id
141
+ await kill_all_emsys()
142
+
143
+ config = await get_config_by_name("login_emsys_fiscal")
144
+
145
+ # Fecha a instancia do emsys - caso esteja aberta
146
+ await kill_all_emsys()
147
+
148
+ app = Application(backend="win32").start("C:\\Rezende\\EMSys3\\EMSysFiscal.exe")
149
+ warnings.filterwarnings(
150
+ "ignore",
151
+ category=UserWarning,
152
+ message="32-bit application should be automated using 32-bit Python",
153
+ )
154
+
155
+ await worker_sleep(5)
156
+
157
+ try:
158
+ app = Application(backend="win32").connect(
159
+ class_name="TFrmLoginModulo", timeout=100
160
+ )
161
+ except:
162
+ return RpaRetornoProcessoDTO(
163
+ sucesso=False,
164
+ retorno="Erro ao abrir o EMSys Fiscal, tela de login não encontrada",
165
+ status=RpaHistoricoStatusEnum.Falha,
166
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)],
167
+ )
168
+ return_login = await login_emsys_fiscal(config.conConfiguracao, app, task)
169
+ if return_login.sucesso:
170
+ await worker_sleep(2)
171
+ type_text_into_field(
172
+ "Livros Fiscais", app["TFrmMenuPrincipal"]["Edit"], True, "50"
173
+ )
174
+ pyautogui.press("enter")
175
+ await worker_sleep(2)
176
+ pyautogui.press("down")
177
+ await worker_sleep(2)
178
+ pyautogui.press("enter")
179
+ console.print(
180
+ "\nPesquisa: 'Livros Fiscais' realizada com sucesso.",
181
+ style="bold green",
182
+ )
183
+
184
+ else:
185
+ logger.info(f"\nError Message: {return_login.retorno}")
186
+ console.print(f"\nError Message: {return_login.retorno}", style="bold red")
187
+ return return_login
188
+
189
+ await worker_sleep(7)
190
+
191
+ ##### Janela Movimento Livros Fiscais #####
192
+ # Conecta na janela principal
193
+ app = Application().connect(class_name="TFrmMovtoLivroFiscal")
194
+ main_window = app.window(class_name="TFrmMovtoLivroFiscal")
195
+ main_window.wait("exists enabled visible ready", timeout=20)
196
+
197
+ # Pegar o wrapper do campo
198
+ campo_data = main_window.child_window(class_name="TDBIEditDate")
199
+ campo_data.wait("exists enabled visible ready", timeout=10)
200
+ campo_data = campo_data.wrapper_object() # agora é o controle de fato
201
+
202
+ # Foco e clique
203
+ campo_data.set_focus()
204
+ campo_data.click_input()
205
+
206
+ # Limpa e digita
207
+ keyboard.send_keys("^a{BACKSPACE}" + periodo)
208
+
209
+ # Seleciona inventário
210
+ chk_inventario = main_window.child_window(
211
+ class_name="TcxCheckBox", found_index=6
212
+ ).click_input()
213
+
214
+ await worker_sleep(2)
215
+
216
+ # Caminho da imagem do botão
217
+ imagem_botao = r"assets\\extracao_relatorios\\btn_incluir_livro.png"
218
+ # imagem_botao = r"C:\Users\automatehub\Documents\GitHub\worker-automate-hub\assets\extracao_relatorios\btn_incluir_livro.png"
219
+
220
+ if os.path.exists(imagem_botao):
221
+ try:
222
+ # Localiza a imagem na tela
223
+ botao = pyautogui.locateCenterOnScreen(
224
+ imagem_botao, confidence=0.9
225
+ ) # confidence precisa do opencv instalado
226
+ if botao:
227
+ pyautogui.click(botao)
228
+ print("Botão clicado com sucesso!")
229
+ else:
230
+ print("Não encontrou o botão na tela.")
231
+ except Exception as e:
232
+ print(f"Erro ao localizar/clicar na imagem: {e}")
233
+ else:
234
+ print("Caminho da imagem não existe.")
235
+
236
+ ##### Janela Perguntas da Geração Livros Fiscais #####
237
+ app = Application().connect(class_name="TPerguntasLivrosFiscaisForm")
238
+ main_window = app.window(class_name="TPerguntasLivrosFiscaisForm")
239
+ main_window.wait("exists enabled visible ready", timeout=20)
240
+
241
+ respostas = ["Não", "Sim", "Não", "Não"]
242
+
243
+ for i, resposta in enumerate(respostas):
244
+ combo = main_window.child_window(
245
+ class_name="TDBIComboBoxValues", found_index=i
246
+ ).wrapper_object()
247
+ combo.set_focus()
248
+ combo.click_input()
249
+ await worker_sleep(0.1)
250
+ keyboard.send_keys(resposta + "{ENTER}")
251
+ await worker_sleep(0.2)
252
+ # Clicar em confirmar
253
+ main_window.child_window(class_name="TButton", found_index=1).click_input()
254
+
255
+ await worker_sleep(2)
256
+
257
+ ##### Janela Gerar Registros #####
258
+ app = Application(backend="win32").connect(title="Gerar Registros")
259
+ main_window = app.window(title="Gerar Registros")
260
+
261
+ # Clicar no botão "Sim"
262
+ main_window.child_window(title="&Sim", class_name="Button").click_input()
263
+
264
+ await worker_sleep(2)
265
+
266
+ ##### Janela Informa Motivo do Inventario #####
267
+ app = Application().connect(class_name="TFrmMotvoMotivoInventario")
268
+ main_window = app.window(class_name="TFrmMotvoMotivoInventario")
269
+ main_window.wait("exists enabled visible ready", timeout=20)
270
+ slc_01 = main_window.child_window(
271
+ class_name="TDBIComboBoxValues", found_index=0
272
+ ).click_input()
273
+ await worker_sleep(1)
274
+ keyboard.send_keys("01" + "{ENTER}")
275
+ await worker_sleep(2)
276
+
277
+ # Clicar em confirmar
278
+ main_window.child_window(class_name="TBitBtn", found_index=0).click_input()
279
+
280
+ await worker_sleep(5)
281
+
282
+ CLASS = "TFrmPreviewRelatorio"
283
+
284
+ # 1) Espera a janela aparecer (até 180s)
285
+ desk = Desktop(backend="win32")
286
+ deadline = time.time() + 180
287
+ win = None
288
+ while time.time() < deadline:
289
+ try:
290
+ w = desk.window(class_name=CLASS)
291
+ if w.exists(timeout=0.5):
292
+ w.wait("visible enabled ready", timeout=30)
293
+ win = w
294
+ break
295
+ except Exception:
296
+ pass
297
+ time.sleep(0.5)
298
+
299
+ if win is None:
300
+ raise TimeoutError(f"Janela '{CLASS}' não apareceu dentro do timeout.")
301
+
302
+ w.close()
303
+
304
+ await worker_sleep(2)
305
+
306
+ ##### Janela Movimento Livro Fiscal #####
307
+ # Selecionar primeira linha inventario
308
+ selecionar_inventario_por_competencia(periodo)
309
+
310
+ await worker_sleep(2)
311
+
312
+ # Clicar em visualizar livro
313
+ caminho = r"assets\\extracao_relatorios\\btn_visu_livros.png"
314
+ # caminho =r"C:\Users\automatehub\Documents\GitHub\worker-automate-hub\assets\extracao_relatorios\btn_visu_livros.png"
315
+ # Verifica se o arquivo existe
316
+ if os.path.isfile(caminho):
317
+ print("A imagem existe:", caminho)
318
+
319
+ # Procura a imagem na tela
320
+ pos = pyautogui.locateCenterOnScreen(
321
+ caminho, confidence=0.9
322
+ ) # ajuste o confidence se necessário
323
+ if pos:
324
+ pyautogui.click(pos) # clica no centro da imagem
325
+ print("Clique realizado na imagem.")
326
+ else:
327
+ print("Imagem encontrada no disco, mas não está visível na tela.")
328
+ else:
329
+ print("A imagem NÃO existe:", caminho)
330
+
331
+ await worker_sleep(5)
332
+
333
+ ##### Janela Movimento Livro Fiscal - Livro - Inventario para Competencia #####
334
+ app = Application().connect(class_name="TFrmMovtoLivroFiscal")
335
+ main_window = app.window(class_name="TFrmMovtoLivroFiscal")
336
+ main_window.wait("exists enabled visible ready", timeout=20)
337
+ input_7 = main_window.child_window(
338
+ class_name="TDBIEditCode", found_index=0
339
+ ).click_input()
340
+ await worker_sleep(0.1)
341
+ keyboard.send_keys("7" + "{TAB}")
342
+ await worker_sleep(0.2)
343
+ # Clicar em imprimir
344
+ btn_imprimir = main_window.child_window(
345
+ class_name="TBitBtn", found_index=0
346
+ ).click_input()
347
+
348
+ await worker_sleep(2)
349
+
350
+ ##### Janela Selecion o Template Desejado #####
351
+ app = Application().connect(class_name="TFrmFRVisualizaTemplateMenuNew")
352
+ main_window = app.window(class_name="TFrmFRVisualizaTemplateMenuNew")
353
+ main_window.wait("exists enabled visible ready", timeout=20)
354
+ btn_gerar_rel = main_window.child_window(
355
+ class_name="TBitBtn", found_index=1
356
+ ).click_input()
357
+
358
+ await worker_sleep(2)
359
+
360
+ ##### Janela Parametros #####
361
+ app = Application().connect(class_name="TFrmFRParametroRelatorio")
362
+ main_window = app.window(class_name="TFrmFRParametroRelatorio")
363
+ main_window.wait("exists enabled visible ready", timeout=20)
364
+ slc_nao = main_window.child_window(
365
+ class_name="TComboBox", found_index=0
366
+ ).click_input()
367
+ await worker_sleep(0.1)
368
+ keyboard.send_keys("NAO" + "{ENTER}")
369
+ await worker_sleep(0.2)
370
+
371
+ # Clicar BOTAO OK
372
+ slc_nao = main_window.child_window(
373
+ class_name="TBitBtn", found_index=1
374
+ ).click_input()
375
+
376
+ await worker_sleep(2)
377
+
378
+ max_tentativas = 5
379
+ tentativa = 1
380
+ sucesso = False
381
+
382
+ # defina caminho_arquivo ANTES para não ficar indefinido
383
+ caminho_arquivo = rf"C:\Users\automatehub\Downloads\saldo_estoque_fiscal_{periodo_format}_{filial}.xlsx"
384
+
385
+ while tentativa <= max_tentativas and not sucesso:
386
+ console.print(
387
+ f"Tentativa {tentativa} de {max_tentativas}", style="bold cyan"
388
+ )
389
+
390
+ # 1) Abrir o picker pelo botão (imagem)
391
+ console.print("Procurando botão de salvar (imagem)...", style="bold cyan")
392
+ caminho_img = r"assets\\extracao_relatorios\btn_salvar.png"
393
+ # caminho_img = r"C:\Users\automatehub\Documents\GitHub\worker-automate-hub\assets\extracao_relatorios\btn_salvar.png"
394
+ if os.path.isfile(caminho_img):
395
+ pos = pyautogui.locateCenterOnScreen(caminho_img, confidence=0.9)
396
+ if pos:
397
+ pyautogui.click(pos)
398
+ console.print(
399
+ "Clique realizado no botão salvar", style="bold green"
400
+ )
401
+ else:
402
+ console.print(
403
+ "Imagem encontrada mas não está visível na tela",
404
+ style="bold yellow",
405
+ )
406
+ else:
407
+ console.print("Imagem do botão salvar NÃO existe", style="bold red")
408
+
409
+ await worker_sleep(8)
410
+
411
+ # 2) Selecionar formato Excel (desambiguando múltiplas TFrmRelatorioFormato)
412
+ console.print("Selecionando formato Excel...", style="bold cyan")
413
+ try:
414
+ desktop = Desktop(backend="win32")
415
+
416
+ # Liste todas as visíveis
417
+ wins_visiveis = desktop.windows(
418
+ class_name="TFrmRelatorioFormato", visible_only=True
419
+ )
420
+ if not wins_visiveis:
421
+ raise RuntimeError("Janela de formato não apareceu.")
422
+
423
+ # 2.1) Tente a janela em foco (foreground)
424
+ h_fore = win32gui.GetForegroundWindow()
425
+ alvo = None
426
+ for w in wins_visiveis:
427
+ if w.handle == h_fore:
428
+ alvo = w
429
+ break
430
+
431
+ # 2.2) Se não estiver em foco, pegue a que contém um TComboBox (a 'Configuração para Salvar arq...')
432
+ if alvo is None:
433
+ candidatos = []
434
+ for w in wins_visiveis:
435
+ try:
436
+ if w.child_window(class_name="TComboBox").exists(
437
+ timeout=0.8
438
+ ):
439
+ candidatos.append(w)
440
+ except Exception:
441
+ pass
442
+ if candidatos:
443
+ alvo = candidatos[-1] # a mais recente
444
+ else:
445
+ alvo = wins_visiveis[-1] # fallback
446
+
447
+ # Trabalhe via WindowSpecification
448
+ spec_fmt = desktop.window(handle=alvo.handle)
449
+ spec_fmt.wait("visible", timeout=10)
450
+ win_fmt = spec_fmt.wrapper_object()
451
+ win_fmt.set_focus()
452
+
453
+ # Acessar o ComboBox
454
+ try:
455
+ combo_spec = spec_fmt.child_window(class_name="TComboBox")
456
+ except Exception:
457
+ combo_spec = spec_fmt.child_window(control_type="ComboBox")
458
+ combo_spec.wait("exists enabled", timeout=10)
459
+ combo = combo_spec.wrapper_object()
460
+
461
+ textos = combo.texts()
462
+ console.print(f"Itens do ComboBox: {textos}", style="bold yellow")
463
+
464
+ # Seleção por índice conhecido; fallback por texto
465
+ try:
466
+ combo.select(8)
467
+ except Exception:
468
+ alvo_idx = None
469
+ for i, t in enumerate(textos):
470
+ if "EXCEL" in str(t).upper() or "XLSX" in str(t).upper():
471
+ alvo_idx = i
472
+ break
473
+ if alvo_idx is None:
474
+ console.print(
475
+ "Não foi possível localizar a opção de Excel no ComboBox.",
476
+ style="bold red",
477
+ )
478
+ tentativa += 1
479
+ await worker_sleep(2)
480
+ continue
481
+ combo.select(alvo_idx)
482
+
483
+ await worker_sleep(1)
484
+
485
+ # Clique em OK
486
+ btn_ok_spec = spec_fmt.child_window(class_name="TBitBtn", found_index=1)
487
+ btn_ok_spec.wait("enabled", timeout=5)
488
+ btn_ok_spec.click_input()
489
+
490
+ # Aguarde a janela de formato desaparecer
491
+ try:
492
+ spec_fmt.wait_not("visible", timeout=10)
493
+ except Exception:
494
+ pass
495
+
496
+ # Feche possíveis duplicatas remanescentes (defensivo)
497
+ for w in desktop.windows(
498
+ class_name="TFrmRelatorioFormato", visible_only=True
499
+ ):
500
+ if w.handle != alvo.handle:
501
+ try:
502
+ w.close()
503
+ except Exception:
504
+ pass
505
+
506
+ except Exception as e:
507
+ console.print(f"Falha ao selecionar formato: {e}", style="bold red")
508
+ tentativa += 1
509
+ await worker_sleep(3)
510
+ continue
511
+
512
+ await worker_sleep(5)
513
+
514
+ # 3) Janela "Salvar para arquivo"
515
+ console.print("Abrindo janela de salvar arquivo...", style="bold cyan")
516
+ try:
517
+ app_save = Application(backend="win32").connect(
518
+ title_re="Salvar para arquivo|Salvar como|Save As", timeout=30
519
+ )
520
+ spec_save = app_save.window(
521
+ title_re="Salvar para arquivo|Salvar como|Save As"
522
+ )
523
+ spec_save.wait("visible", timeout=30)
524
+ win_save = spec_save.wrapper_object()
525
+ except Exception as e:
526
+ console.print(
527
+ f"Não achou a janela 'Salvar para arquivo': {e}", style="bold red"
528
+ )
529
+ tentativa += 1
530
+ await worker_sleep(3)
531
+ continue
532
+
533
+ # 3.1) Remover arquivo pré-existente
534
+ if os.path.exists(caminho_arquivo):
535
+ try:
536
+ os.remove(caminho_arquivo)
537
+ console.print(
538
+ "Arquivo existente removido para evitar prompt de sobrescrita.",
539
+ style="bold yellow",
540
+ )
541
+ except Exception as e:
542
+ console.print(
543
+ f"Não foi possível remover o arquivo existente: {e}",
544
+ style="bold red",
545
+ )
546
+
547
+ # 3.2) Preencher nome e salvar
548
+ try:
549
+ campo_spec = spec_save.child_window(class_name="Edit", control_id=1148)
550
+ campo_spec.wait("exists enabled visible", timeout=10)
551
+ campo_nome = campo_spec.wrapper_object()
552
+ campo_nome.set_focus()
553
+ try:
554
+ campo_nome.set_edit_text("")
555
+ except Exception:
556
+ campo_nome.type_keys("^a{DELETE}", pause=0.02)
557
+
558
+ campo_nome.type_keys(caminho_arquivo, with_spaces=True, pause=0.01)
559
+ console.print(
560
+ f"Arquivo configurado para: {caminho_arquivo}", style="bold green"
561
+ )
562
+
563
+ await worker_sleep(1)
564
+
565
+ btn_salvar_spec = spec_save.child_window(
566
+ class_name="Button", found_index=0
567
+ )
568
+ btn_salvar_spec.wait("enabled", timeout=10)
569
+ btn_salvar_spec.click_input()
570
+
571
+ # Esperar a janela sumir
572
+ try:
573
+ spec_save.wait_not("visible", timeout=15)
574
+ except Exception:
575
+ pass
576
+
577
+ except Exception as e:
578
+ console.print(f"Erro ao confirmar salvar: {e}", style="bold red")
579
+ tentativa += 1
580
+ await worker_sleep(3)
581
+ continue
582
+
583
+ await worker_sleep(2)
584
+
585
+ # 3.3) Confirmar sobrescrita (se houver)
586
+ try:
587
+ app_conf = Application(backend="win32").connect(
588
+ title_re="Confirm(ar)?( )?Salvar( )?Como|Confirm Save As", timeout=3
589
+ )
590
+ spec_conf = app_conf.window(
591
+ title_re="Confirm(ar)?( )?Salvar( )?Como|Confirm Save As"
592
+ )
593
+ spec_conf.wait("visible", timeout=3)
594
+ spec_conf.child_window(class_name="Button", found_index=0).click_input()
595
+ console.print(
596
+ "Confirmação de sobrescrita respondida.", style="bold yellow"
597
+ )
598
+ except Exception:
599
+ pass
600
+
601
+ await worker_sleep(2)
602
+
603
+ # 4) Aguardar 'Printing' (se existir)
604
+ console.print(
605
+ "Aguardando finalização do processo de impressão/salvamento...",
606
+ style="bold cyan",
607
+ )
608
+ try:
609
+ app_print = Application(backend="win32").connect(
610
+ title_re="Printing", timeout=5
611
+ )
612
+ spec_print = app_print.window(title_re="Printing")
613
+ try:
614
+ spec_print.wait_not("visible", timeout=60)
615
+ console.print("Janela 'Printing' fechada.", style="bold green")
616
+ except Exception:
617
+ console.print(
618
+ "Janela 'Printing' não fechou no tempo esperado. Seguindo.",
619
+ style="bold yellow",
620
+ )
621
+ except findwindows.ElementNotFoundError:
622
+ console.print("Janela 'Printing' não apareceu.", style="bold yellow")
623
+ except Exception as e:
624
+ console.print(f"Erro ao aguardar 'Printing': {e}", style="bold yellow")
625
+
626
+ # 5) Validar arquivo salvo
627
+ if os.path.exists(caminho_arquivo):
628
+ console.print(
629
+ f"Arquivo encontrado: {caminho_arquivo}", style="bold green"
630
+ )
631
+ with open(caminho_arquivo, "rb") as f:
632
+ file_bytes = io.BytesIO(f.read())
633
+ sucesso = True
634
+ else:
635
+ console.print(
636
+ "Arquivo não encontrado, tentando novamente...", style="bold red"
637
+ )
638
+ tentativa += 1
639
+ await worker_sleep(3)
640
+
641
+ if not sucesso:
642
+ console.print(
643
+ "Falha após 5 tentativas. Arquivo não foi gerado.", style="bold red"
644
+ )
645
+
646
+ nome_com_extensao = f"saldo_estoque_fiscal_{periodo_format}_{filial}.xlsx"
647
+ # lê o arquivo
648
+ print(caminho_arquivo)
649
+ with open(f"{caminho_arquivo}", "rb") as file:
650
+ file_bytes = io.BytesIO(file.read())
651
+
652
+ console.print("Enviar Excel para o BOF")
653
+ try:
654
+ await send_file(
655
+ historico_id,
656
+ nome_com_extensao,
657
+ "xlsx",
658
+ file_bytes,
659
+ file_extension="xlsx",
660
+ )
661
+ console.print("Removendo arquivo XLS da pasta downloads")
662
+ os.remove(f"{caminho_arquivo}")
663
+ return RpaRetornoProcessoDTO(
664
+ sucesso=True,
665
+ retorno="Relatório gerado com sucesso",
666
+ status=RpaHistoricoStatusEnum.Sucesso,
667
+ )
668
+
669
+ except Exception as e:
670
+ console.print(f"Erro ao enviar o arquivo: {e}", style="bold red")
671
+ return RpaRetornoProcessoDTO(
672
+ sucesso=False,
673
+ retorno=f"Erro ao enviar o arquivo: {e}",
674
+ status=RpaHistoricoStatusEnum.Falha,
675
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)],
676
+ )
677
+
678
+ except Exception as ex:
679
+ retorno = f"Erro Processo Saldo Estoque Fiscal: {str(ex)}"
680
+ logger.error(retorno)
681
+ console.print(retorno, style="bold red")
682
+ return RpaRetornoProcessoDTO(
683
+ sucesso=False,
684
+ retorno=retorno,
685
+ status=RpaHistoricoStatusEnum.Falha,
686
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)],
687
+ )
688
+