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.
- worker_automate_hub/api/client.py +121 -10
- worker_automate_hub/api/rpa_historico_service.py +1 -0
- worker_automate_hub/tasks/jobs/abertura_livros_fiscais.py +11 -14
- worker_automate_hub/tasks/jobs/descartes.py +8 -8
- worker_automate_hub/tasks/jobs/devolucao_produtos.py +1386 -0
- worker_automate_hub/tasks/jobs/extracao_dados_nielsen.py +504 -0
- worker_automate_hub/tasks/jobs/extracao_saldo_estoque_fiscal.py +90 -11
- worker_automate_hub/tasks/jobs/fidc_gerar_nosso_numero.py +2 -2
- worker_automate_hub/tasks/jobs/fidc_remessa_cobranca_cnab240.py +24 -15
- worker_automate_hub/tasks/jobs/importacao_extratos.py +538 -0
- worker_automate_hub/tasks/jobs/importacao_extratos_748.py +800 -0
- worker_automate_hub/tasks/jobs/inclusao_pedidos_ipiranga.py +222 -0
- worker_automate_hub/tasks/jobs/inclusao_pedidos_raizen.py +174 -0
- worker_automate_hub/tasks/jobs/inclusao_pedidos_vibra.py +327 -0
- worker_automate_hub/tasks/jobs/notas_faturamento_sap.py +438 -157
- worker_automate_hub/tasks/jobs/opex_capex.py +523 -384
- worker_automate_hub/tasks/task_definitions.py +30 -2
- worker_automate_hub/utils/util.py +20 -10
- {worker_automate_hub-0.5.820.dist-info → worker_automate_hub-0.5.912.dist-info}/METADATA +2 -1
- {worker_automate_hub-0.5.820.dist-info → worker_automate_hub-0.5.912.dist-info}/RECORD +22 -15
- {worker_automate_hub-0.5.820.dist-info → worker_automate_hub-0.5.912.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
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(
|
|
229
|
+
await worker_sleep(120)
|
|
230
230
|
boleto_argenta = None
|
|
231
231
|
max_trys = 5
|
|
232
232
|
trys = 0
|