worker-automate-hub 0.5.820__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 (22) hide show
  1. worker_automate_hub/api/client.py +121 -10
  2. worker_automate_hub/api/rpa_historico_service.py +1 -0
  3. worker_automate_hub/tasks/jobs/abertura_livros_fiscais.py +11 -14
  4. worker_automate_hub/tasks/jobs/descartes.py +8 -8
  5. worker_automate_hub/tasks/jobs/devolucao_produtos.py +1386 -0
  6. worker_automate_hub/tasks/jobs/extracao_dados_nielsen.py +504 -0
  7. worker_automate_hub/tasks/jobs/extracao_saldo_estoque_fiscal.py +90 -11
  8. worker_automate_hub/tasks/jobs/fidc_gerar_nosso_numero.py +2 -2
  9. worker_automate_hub/tasks/jobs/fidc_remessa_cobranca_cnab240.py +24 -15
  10. worker_automate_hub/tasks/jobs/importacao_extratos.py +538 -0
  11. worker_automate_hub/tasks/jobs/importacao_extratos_748.py +800 -0
  12. worker_automate_hub/tasks/jobs/inclusao_pedidos_ipiranga.py +222 -0
  13. worker_automate_hub/tasks/jobs/inclusao_pedidos_raizen.py +174 -0
  14. worker_automate_hub/tasks/jobs/inclusao_pedidos_vibra.py +327 -0
  15. worker_automate_hub/tasks/jobs/notas_faturamento_sap.py +438 -157
  16. worker_automate_hub/tasks/jobs/opex_capex.py +523 -384
  17. worker_automate_hub/tasks/task_definitions.py +30 -2
  18. worker_automate_hub/utils/util.py +20 -10
  19. {worker_automate_hub-0.5.820.dist-info → worker_automate_hub-0.5.912.dist-info}/METADATA +2 -1
  20. {worker_automate_hub-0.5.820.dist-info → worker_automate_hub-0.5.912.dist-info}/RECORD +22 -15
  21. {worker_automate_hub-0.5.820.dist-info → worker_automate_hub-0.5.912.dist-info}/WHEEL +0 -0
  22. {worker_automate_hub-0.5.820.dist-info → worker_automate_hub-0.5.912.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,504 @@
1
+ # IMPORT SELENIUM
2
+ import os
3
+ import sys
4
+ import time
5
+ import json
6
+ import asyncio
7
+ import shutil
8
+ from typing import List
9
+ from datetime import datetime
10
+ from rich.console import Console
11
+
12
+
13
+ from selenium import webdriver
14
+ from selenium.webdriver.common.by import By
15
+ from selenium.webdriver.remote.webelement import WebElement
16
+ from selenium.webdriver.support.ui import WebDriverWait
17
+ from selenium.webdriver.support import expected_conditions as EC
18
+ from selenium.webdriver.chrome.service import Service
19
+ from selenium.webdriver.chrome.options import Options
20
+ from selenium.common.exceptions import TimeoutException
21
+ from webdriver_manager.chrome import ChromeDriverManager
22
+ from os import name
23
+
24
+ # from selenium.webdriver.support.ui import WebDriverWait
25
+ from selenium.webdriver.support.wait import WebDriverWait
26
+ from selenium.webdriver.chrome.webdriver import WebDriver
27
+
28
+ ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
29
+ sys.path.append(ROOT_PATH)
30
+
31
+ from worker_automate_hub.api.datalake_service import send_file_to_datalake
32
+ from worker_automate_hub.api.client import get_config_by_name
33
+ from worker_automate_hub.models.dto.rpa_historico_request_dto import (
34
+ RpaHistoricoStatusEnum,
35
+ RpaRetornoProcessoDTO,
36
+ RpaTagDTO,
37
+ RpaTagEnum,
38
+ )
39
+ from worker_automate_hub.models.dto.rpa_processo_entrada_dto import (
40
+ RpaProcessoEntradaDTO,
41
+ )
42
+
43
+ from worker_automate_hub.utils.util import (
44
+ kill_all_emsys,
45
+ )
46
+ USERNAME = os.environ.get("USERNAME")
47
+ console = Console()
48
+
49
+ HISTORICO_ID = "e8ca47cf-c49b-437c-9028-50bcfa5fe021"
50
+
51
+ # Diretório/bucket no datalake onde os arquivos serão enviados
52
+ DATALAKE_DIRECTORY = "nielsen_arquivos/raw"
53
+
54
+ # Funcões Selenium
55
+
56
+
57
+ def aguardar_elemento_ser_clicavel_xpath(
58
+ driver: WebDriver, xpath: str, tempo: int
59
+ ) -> bool:
60
+ aguardar = WebDriverWait(driver, tempo)
61
+ return aguardar.until(EC.element_to_be_clickable((By.XPATH, xpath)))
62
+
63
+
64
+ def inserir_texto_por_letra_xpath(driver: WebDriver, xpath: str, texto: str):
65
+ for letter in texto:
66
+ driver.find_element(By.XPATH, xpath).send_keys(letter)
67
+ time.sleep(0.1)
68
+
69
+
70
+ def inserir_texto_por_xpath(driver: WebDriver, xpath: str, texto: str):
71
+ driver.find_element(By.XPATH, xpath).send_keys(texto)
72
+
73
+
74
+ def clicar_elemento_por_xpath(driver: WebDriver, xpath: str) -> None:
75
+ driver.find_element(By.XPATH, xpath).click()
76
+
77
+
78
+ def busca_lista_elementos_por_xpath(driver: WebDriver, xpath: str) -> List[WebElement]:
79
+ return driver.find_elements(By.XPATH, xpath)
80
+
81
+
82
+ def limpar_pasta_downloads(caminho: str, remover_pastas: bool = False):
83
+ """
84
+ Limpa a pasta de downloads utilizada pelo robô.
85
+
86
+ Args:
87
+ caminho (str): Caminho da pasta a ser limpa.
88
+ remover_pastas (bool): Se True, remove também subpastas.
89
+ """
90
+ if not os.path.exists(caminho):
91
+ console.print(f"Pasta não existe: {caminho}", style="yellow")
92
+ return
93
+
94
+ try:
95
+ itens = os.listdir(caminho)
96
+
97
+ if not itens:
98
+ console.print("Pasta já estava vazia.", style="green")
99
+ return
100
+
101
+ for item in itens:
102
+ item_path = os.path.join(caminho, item)
103
+
104
+ try:
105
+ # Remover arquivos
106
+ if os.path.isfile(item_path):
107
+ os.remove(item_path)
108
+ console.print(f"Arquivo removido: {item_path}", style="green")
109
+
110
+ # Remover pastas (opcional)
111
+ elif remover_pastas and os.path.isdir(item_path):
112
+ shutil.rmtree(item_path, ignore_errors=True)
113
+ console.print(f"Pasta removida: {item_path}", style="green")
114
+
115
+ except Exception as e:
116
+ console.print(
117
+ f"Não foi possível remover {item_path}: {e}",
118
+ style="bold red",
119
+ )
120
+
121
+ console.print("Limpeza concluída!", style="bold green")
122
+
123
+ except Exception as e:
124
+ console.print(f"Erro ao limpar pasta: {e}", style="bold red")
125
+
126
+
127
+ class ExtracaoDados:
128
+ def __init__(self, driver: webdriver.Chrome):
129
+ self.driver = driver
130
+ self.caminho_downloads = rf"C:\Users\{USERNAME}\Downloads"
131
+ self.handles = []
132
+
133
+ async def limpar_downloads(self):
134
+ limpar_pasta_downloads(self.caminho_downloads, remover_pastas=False)
135
+
136
+ async def abrir_site(self):
137
+ await asyncio.sleep(2)
138
+ try:
139
+ console.print(
140
+ "Iniciando navegador e acessando site da Nielsen...", style="cyan"
141
+ )
142
+ self.driver.get("https://web.na-mft.nielseniq.com/cfcc/bclient/index.jsp#/")
143
+ except Exception as e:
144
+ console.print(f"Erro ao abrir o site: {e}", style="bold red")
145
+ raise
146
+
147
+ # Lê credenciais via API usando RpaConfiguracao.conConfiguracao (JSON)
148
+ async def load_config(self):
149
+ try:
150
+ config = await get_config_by_name("nielsen_credenciais")
151
+
152
+ if config is None:
153
+ raise Exception(
154
+ "get_config_by_name retornou None para 'nielsen_credenciais'."
155
+ )
156
+
157
+ # conConfiguracao é onde fica o conteúdo da configuração
158
+ raw_valor = getattr(config, "conConfiguracao", None)
159
+ if raw_valor is None:
160
+ raise Exception(
161
+ "Objeto RpaConfiguracao não possui conteúdo em 'conConfiguracao'. "
162
+ f"Atributos disponíveis: {dir(config)}"
163
+ )
164
+
165
+ # Se for string JSON, fazer parse
166
+ if isinstance(raw_valor, str):
167
+ try:
168
+ valor_json = json.loads(raw_valor)
169
+ except Exception as e_json:
170
+ raise Exception(
171
+ f"Falha ao fazer json.loads em conConfiguracao: {e_json}. "
172
+ f"valor bruto: {raw_valor}"
173
+ )
174
+ # Se já vier como dict
175
+ elif isinstance(raw_valor, dict):
176
+ valor_json = raw_valor
177
+ else:
178
+ raise Exception(
179
+ f"Tipo inesperado em conConfiguracao: {type(raw_valor)}. "
180
+ "Esperado str (JSON) ou dict."
181
+ )
182
+
183
+ username = valor_json.get("usuario")
184
+ password = valor_json.get("senha")
185
+
186
+ if not username or not password:
187
+ raise Exception(
188
+ "Usuário ou senha não encontrados no JSON da configuração. "
189
+ f"JSON: {valor_json}"
190
+ )
191
+
192
+ return username, password
193
+
194
+ except Exception as e:
195
+ console.print(
196
+ f"Erro ao obter credenciais via get_config_by_name: {e}",
197
+ style="bold red",
198
+ )
199
+ raise
200
+
201
+ async def clicar_diretorio_principal(self, nome_diretorio: str):
202
+ """
203
+ Clica no diretório principal pelo nome, tentando vários seletores
204
+ para não quebrar com mudanças pequenas de HTML.
205
+ """
206
+
207
+ # Possíveis XPaths para localizar o item
208
+ xpaths_possiveis = [
209
+ # 1) span com classe 'file' e texto exato
210
+ f"//span[contains(@class, 'file') and normalize-space(text())='{nome_diretorio}']",
211
+
212
+ # 2) elemento com title igual ao nome (span, a, td, etc.)
213
+ f"//*[@title='{nome_diretorio}' and (self::span or self::a or self::td)]",
214
+
215
+ # 3) tr que tenha aria-label contendo o nome
216
+ f"//tr[contains(@aria-label, '{nome_diretorio}')]",
217
+
218
+ # 4) Qualquer elemento com texto visível igual ao nome
219
+ f"//*[normalize-space(text())='{nome_diretorio}']",
220
+ ]
221
+
222
+ elemento_encontrado = False
223
+ ultimo_erro = None
224
+
225
+ for xpath in xpaths_possiveis:
226
+ try:
227
+ print(f"Tentando localizar diretório com XPath: {xpath}")
228
+ aguardar_elemento_ser_clicavel_xpath(self.driver, xpath, 20)
229
+ clicar_elemento_por_xpath(self.driver, xpath)
230
+ print(f"Diretório '{nome_diretorio}' clicado com sucesso usando XPath: {xpath}.")
231
+ elemento_encontrado = True
232
+ break
233
+ except Exception as e:
234
+ print(f"Não encontrado/clicável com esse XPath. Erro: {e}")
235
+ ultimo_erro = e
236
+
237
+ if not elemento_encontrado:
238
+ raise RuntimeError(
239
+ f"Não foi possível clicar no diretório '{nome_diretorio}' "
240
+ f"com nenhum dos seletores. Último erro: {ultimo_erro}"
241
+ )
242
+
243
+ async def clicar_ultima_pasta(self, timeout: int = 20):
244
+
245
+ wait = WebDriverWait(self.driver, timeout)
246
+
247
+ # 1) Esperar o tbody da tabela ficar presente
248
+ try:
249
+ tbody = wait.until(
250
+ EC.presence_of_element_located(
251
+ # tabela de listagem de arquivos/pastas
252
+ (By.CSS_SELECTOR, "table.cursor-pointer tbody, table.w-full.cursor-pointer.table-auto tbody")
253
+ )
254
+ )
255
+ except TimeoutException:
256
+ raise RuntimeError("Tabela de transfers não encontrada na página.")
257
+
258
+ # 2) Pegar todas as linhas clicáveis
259
+ # No HTML do TIBCO cada linha tem tabindex="0" e aria-label com o nome
260
+ rows = tbody.find_elements(By.CSS_SELECTOR, "tr[tabindex='0'][aria-label]")
261
+
262
+ # Filtra apenas as visíveis (caso tenha linha escondida)
263
+ rows = [r for r in rows if r.is_displayed()]
264
+
265
+ if not rows:
266
+ raise RuntimeError("Nenhuma linha clicável encontrada na tabela.")
267
+
268
+ # 3) Última linha da lista (mais recente)
269
+ last_row = rows[-1]
270
+ print("Última linha encontrada com aria-label:",
271
+ last_row.get_attribute("aria-label"))
272
+
273
+ # 4) Dentro da linha, tentamos clicar no elemento mais específico
274
+ target = None
275
+ try:
276
+ # span com classe 'file' (texto do nome)
277
+ target = last_row.find_element(By.CSS_SELECTOR, "span.file")
278
+ except Exception:
279
+ # fallback: qualquer span da primeira coluna de nome
280
+ try:
281
+ target = last_row.find_element(By.CSS_SELECTOR, "td span")
282
+ except Exception:
283
+ # fallback final: a própria <tr>
284
+ target = last_row
285
+
286
+ # 5) Garantir que o elemento está visível na tela
287
+ self.driver.execute_script(
288
+ "arguments[0].scrollIntoView({block: 'center'});", target
289
+ )
290
+
291
+ # 6) Esperar ficar clicável e clicar
292
+ try:
293
+ wait.until(EC.element_to_be_clickable(target))
294
+ except TimeoutException:
295
+ # se não ficar "clicável" oficialmente, ainda tentamos via JS
296
+ print("⚠️ Elemento não ficou clicável pelo EC, tentando mesmo assim...")
297
+
298
+ try:
299
+ target.click()
300
+ except Exception:
301
+ # fallback robusto: clique via JavaScript
302
+ self.driver.execute_script("arguments[0].click();", target)
303
+
304
+ print("Última pasta/arquivo clicado com sucesso!")
305
+
306
+
307
+
308
+ async def login_e_baixar(self):
309
+ # limpa downloads antes de começar
310
+ await self.limpar_downloads()
311
+
312
+ # pega credenciais via API (async)
313
+ username, password = await self.load_config()
314
+
315
+ console.print("Realizando login na NielsenIQ...", style="cyan")
316
+
317
+ # Inserir usuário
318
+ xpath = "//input[@id='userid']"
319
+ aguardar_elemento_ser_clicavel_xpath(self.driver, xpath, 30)
320
+ inserir_texto_por_letra_xpath(self.driver, xpath, username)
321
+
322
+ # Inserir senha
323
+ xpath = "//input[@id='password']"
324
+ inserir_texto_por_letra_xpath(self.driver, xpath, password)
325
+
326
+ # Clicar em OK
327
+ xpath = "//input[@id='button']"
328
+ clicar_elemento_por_xpath(self.driver, xpath)
329
+
330
+ time.sleep(2)
331
+
332
+ # Clicar no diretório principal LA_BR_REDE_SIM_DO_SUL
333
+ await self.clicar_diretorio_principal("LA_BR_REDE_SIM_DO_SUL")
334
+
335
+
336
+ await asyncio.sleep(5)
337
+
338
+ # ====== CLICAR NA PASTA COM O ÚLTIMO MÊS ======
339
+ await self.clicar_ultima_pasta()
340
+
341
+ await asyncio.sleep(5)
342
+
343
+ # ====== BAIXAR OS ARQUIVOS DENTRO DA PASTA ======
344
+
345
+ # Localiza as linhas de arquivo na tabela atual (TIBCO MFT)
346
+ xpath_linhas_arquivos = (
347
+ "//table[contains(@class,'cursor-pointer') and "
348
+ "contains(@class,'table-auto')]/tbody/tr[@tabindex='0']"
349
+ )
350
+
351
+ # Aguarda até 20 segundos pelo menos 1 linha da tabela aparecer
352
+ WebDriverWait(self.driver, 20).until(
353
+ EC.presence_of_element_located((By.XPATH, xpath_linhas_arquivos))
354
+ )
355
+
356
+ linhas_arquivos = busca_lista_elementos_por_xpath(
357
+ self.driver, xpath_linhas_arquivos
358
+ )
359
+
360
+ qtd_arquivos = len(linhas_arquivos)
361
+ console.print(
362
+ f"Total de linhas (arquivos visíveis): {qtd_arquivos}", style="cyan"
363
+ )
364
+
365
+ # Arquivos já existentes na pasta de download (baseline)
366
+ conhecidos = set(os.listdir(self.caminho_downloads))
367
+
368
+ for r in range(1, qtd_arquivos + 1):
369
+ # Linha r
370
+ xpath_linha = f"{xpath_linhas_arquivos}[{r}]"
371
+
372
+ # Coluna Name: span com classe 'file' (link do arquivo)
373
+ xpath_baixar = rf"{xpath_linha}//span[contains(@class, 'file')]"
374
+ clicar_elemento_por_xpath(self.driver, xpath_baixar)
375
+
376
+ await asyncio.sleep(1)
377
+
378
+ # Esperar o download completar (sem .crdownload/.tmp)
379
+ timeout_segundos = 180
380
+ inicio = time.time()
381
+ novos_arquivos = set()
382
+
383
+ while time.time() - inicio < timeout_segundos:
384
+ arquivos_atual = [
385
+ f
386
+ for f in os.listdir(self.caminho_downloads)
387
+ if not f.endswith(".crdownload") and not f.endswith(".tmp")
388
+ ]
389
+ novos = set(arquivos_atual) - conhecidos
390
+ if novos:
391
+ novos_arquivos = novos
392
+ conhecidos.update(novos)
393
+ break
394
+ await asyncio.sleep(1)
395
+
396
+ if not novos_arquivos:
397
+ console.print(
398
+ "Nenhum arquivo novo foi baixado dentro do tempo limite.",
399
+ style="bold red",
400
+ )
401
+ continue
402
+
403
+ for arquivo_baixado in sorted(novos_arquivos):
404
+ caminho_arquivo = os.path.join(self.caminho_downloads, arquivo_baixado)
405
+ console.print(f"Arquivo baixado: {caminho_arquivo}", style="bold green")
406
+
407
+ try:
408
+ with open(caminho_arquivo, "rb") as file:
409
+ file_bytes = file.read()
410
+
411
+ nome_arquivo = arquivo_baixado
412
+ ext = "doc" # tipo lógico esperado pelo datalake
413
+
414
+ await send_file_to_datalake(
415
+ directory=DATALAKE_DIRECTORY,
416
+ file=file_bytes,
417
+ filename=nome_arquivo,
418
+ file_extension=ext,
419
+ )
420
+
421
+ os.remove(caminho_arquivo)
422
+ console.print(
423
+ f"Arquivo {nome_arquivo} enviado ao datalake e removido da pasta de download.",
424
+ style="bold green",
425
+ )
426
+
427
+ except Exception as e:
428
+ result = (
429
+ f"Arquivo baixado com sucesso, porém erro ao enviar para o datalake: {e} "
430
+ f"- Arquivo mantido em {caminho_arquivo}"
431
+ )
432
+ console.print(result, style="bold red")
433
+
434
+
435
+ async def extracao_dados_nielsen(task: RpaProcessoEntradaDTO) -> RpaRetornoProcessoDTO:
436
+ """
437
+ Função principal para ser chamada pelo worker.
438
+ """
439
+ console.print("Iniciando processo de extração Nielsen...", style="bold cyan")
440
+ driver = None
441
+ await kill_all_emsys()
442
+ try:
443
+ service = Service(ChromeDriverManager().install())
444
+
445
+ # === CONFIG PARA REMOVER POPUPS ===
446
+ chrome_options = Options()
447
+ chrome_prefs = {
448
+ "profile.default_content_setting_values.notifications": 2, # bloqueia notificações
449
+ "profile.default_content_setting_values.automatic_downloads": 1, # permite múltiplos downloads
450
+ "download.prompt_for_download": False, # não perguntar onde salvar
451
+ "download.directory_upgrade": True,
452
+ }
453
+ chrome_options.add_experimental_option("prefs", chrome_prefs)
454
+ chrome_options.add_argument("--disable-popup-blocking") # evita popups gerais
455
+ chrome_options.add_argument("--no-first-run")
456
+ chrome_options.add_argument("--no-default-browser-check")
457
+ chrome_options.add_argument("--disable-notifications")
458
+ chrome_options.add_argument("--disable-gpu")
459
+ chrome_options.add_argument("--disable-software-rasterizer")
460
+ chrome_options.add_argument("--disable-features=VizDisplayCompositor")
461
+ chrome_options.add_argument("--disable-dev-shm-usage")
462
+
463
+ driver = webdriver.Chrome(service=service, options=chrome_options)
464
+ driver.maximize_window()
465
+ console.print("Driver Chrome inicializado e maximizado.", style="green")
466
+
467
+ extracao = ExtracaoDados(driver)
468
+ await extracao.abrir_site()
469
+ await extracao.login_e_baixar()
470
+
471
+ # Fecha o driver com segurança
472
+ if driver:
473
+ try:
474
+ driver.quit()
475
+ driver = None
476
+ console.print("Driver fechado com sucesso.", style="green")
477
+ except Exception as e:
478
+ console.print(f"Erro ao fechar o driver: {e}", style="yellow")
479
+
480
+ console.print(
481
+ "Processo de extração Nielsen finalizado com sucesso.",
482
+ style="bold green",
483
+ )
484
+ return RpaRetornoProcessoDTO(
485
+ sucesso=True,
486
+ retorno="Processo de extração Nielsen finalizado com sucesso.",
487
+ status=RpaHistoricoStatusEnum.Sucesso,
488
+ )
489
+
490
+ except Exception as ex:
491
+ console.print(f"Erro na automação Nielsen: {ex}", style="bold red")
492
+ if driver:
493
+ try:
494
+ driver.quit()
495
+ except Exception:
496
+ pass
497
+
498
+ return RpaRetornoProcessoDTO(
499
+ sucesso=False,
500
+ retorno=f"Erro na automação Nielsen: {ex}",
501
+ status=RpaHistoricoStatusEnum.Falha,
502
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)],
503
+ )
504
+
@@ -5,6 +5,8 @@ from pywinauto import Application, timings, findwindows, keyboard, Desktop
5
5
  import sys
6
6
  import io
7
7
  import win32gui
8
+ import pyperclip
9
+ from unidecode import unidecode
8
10
 
9
11
  # sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
10
12
 
@@ -44,6 +46,88 @@ console = Console()
44
46
  pyautogui.PAUSE = 0.5
45
47
  pyautogui.FAILSAFE = False
46
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
+
47
131
 
48
132
  async def extracao_saldo_estoque_fiscal(
49
133
  task: RpaProcessoEntradaDTO,
@@ -131,6 +215,7 @@ async def extracao_saldo_estoque_fiscal(
131
215
 
132
216
  # Caminho da imagem do botão
133
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"
134
219
 
135
220
  if os.path.exists(imagem_botao):
136
221
  try:
@@ -214,27 +299,19 @@ async def extracao_saldo_estoque_fiscal(
214
299
  if win is None:
215
300
  raise TimeoutError(f"Janela '{CLASS}' não apareceu dentro do timeout.")
216
301
 
217
- # 2) Conecta ao app usando o handle da janela encontrada
218
- app = Application(backend="win32").connect(handle=win.handle)
219
- main_window = app.window(handle=win.handle)
220
-
221
- # Dá o foco na janela
222
- main_window.set_focus()
223
-
224
- await worker_sleep(2)
225
-
226
- main_window.close()
302
+ w.close()
227
303
 
228
304
  await worker_sleep(2)
229
305
 
230
306
  ##### Janela Movimento Livro Fiscal #####
231
307
  # Selecionar primeira linha inventario
232
- pyautogui.click(928, 475)
308
+ selecionar_inventario_por_competencia(periodo)
233
309
 
234
310
  await worker_sleep(2)
235
311
 
236
312
  # Clicar em visualizar livro
237
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"
238
315
  # Verifica se o arquivo existe
239
316
  if os.path.isfile(caminho):
240
317
  print("A imagem existe:", caminho)
@@ -313,6 +390,7 @@ async def extracao_saldo_estoque_fiscal(
313
390
  # 1) Abrir o picker pelo botão (imagem)
314
391
  console.print("Procurando botão de salvar (imagem)...", style="bold cyan")
315
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"
316
394
  if os.path.isfile(caminho_img):
317
395
  pos = pyautogui.locateCenterOnScreen(caminho_img, confidence=0.9)
318
396
  if pos:
@@ -607,3 +685,4 @@ async def extracao_saldo_estoque_fiscal(
607
685
  status=RpaHistoricoStatusEnum.Falha,
608
686
  tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)],
609
687
  )
688
+
@@ -210,7 +210,7 @@ async def gerar_nosso_numero(task: RpaProcessoEntradaDTO) -> RpaRetornoProcessoD
210
210
  return RpaRetornoProcessoDTO(
211
211
  sucesso=False, retorno=log_msg, status=RpaHistoricoStatusEnum.Falha, tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)]
212
212
  )
213
-
213
+ await worker_sleep(60)
214
214
  app = Application().connect(title="Seleciona Cobrança Bancária")
215
215
  main_window = app["Seleciona Cobrança Bancária"]
216
216
  code = main_window.child_window(class_name="TDBIEditCode", found_index=0)
@@ -226,7 +226,7 @@ async def gerar_nosso_numero(task: RpaProcessoEntradaDTO) -> RpaRetornoProcessoD
226
226
  button_ok.click()
227
227
  pyautogui.click(855, 740)
228
228
 
229
- await worker_sleep(80)
229
+ await worker_sleep(120)
230
230
  boleto_argenta = None
231
231
  max_trys = 5
232
232
  trys = 0