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,1386 @@
1
+ # -*- coding: utf-8 -*-
2
+ import asyncio
3
+ import warnings
4
+ from datetime import datetime
5
+ import json
6
+ import ast
7
+ import io
8
+ import pyautogui
9
+ from pywinauto.application import Application
10
+ from pywinauto import keyboard
11
+ from pywinauto import Desktop
12
+ from collections import defaultdict
13
+ from rich.console import Console
14
+ import getpass
15
+ import time
16
+ import re
17
+ import sys
18
+ import os
19
+ import pyperclip
20
+ from unidecode import unidecode
21
+
22
+ sys.path.append(
23
+ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
24
+ )
25
+
26
+ from worker_automate_hub.api.client import (
27
+ get_config_by_name,
28
+ send_file,
29
+ get_notas_produtos,
30
+ )
31
+ from worker_automate_hub.models.dto.rpa_historico_request_dto import (
32
+ RpaHistoricoStatusEnum,
33
+ RpaRetornoProcessoDTO,
34
+ RpaTagDTO,
35
+ RpaTagEnum,
36
+ )
37
+ from worker_automate_hub.models.dto.rpa_processo_entrada_dto import (
38
+ RpaProcessoEntradaDTO,
39
+ )
40
+ from worker_automate_hub.utils.logger import logger
41
+ from pywinauto.keyboard import send_keys
42
+
43
+ # from worker_automate_hub.utils.toast import show_toast
44
+ from worker_automate_hub.utils.util import (
45
+ send_to_webhook,
46
+ extract_nf_number, # permanece importado, mesmo sem uso
47
+ faturar_pre_venda,
48
+ find_element_center,
49
+ find_target_position,
50
+ kill_all_emsys,
51
+ login_emsys,
52
+ set_variable,
53
+ take_screenshot,
54
+ take_target_position,
55
+ type_text_into_field,
56
+ wait_nf_ready,
57
+ wait_window_close,
58
+ worker_sleep,
59
+ )
60
+
61
+ console = Console()
62
+
63
+ ASSETS_BASE_PATH = r"assets\devolucao_produtos"
64
+ # ASSETS_BASE_PATH = r"C:\Users\automatehub\Desktop\img_leo"
65
+ ALMOXARIFADO_DEFAULT = "50"
66
+
67
+ SLEEP_AFTER_COPY = 0.12
68
+ SLEEP_BETWEEN_KEYS = 0.08
69
+
70
+ # =========================
71
+ # Utilidades p/ o diálogo
72
+ # =========================
73
+ def _get_main_window_by_class(timeout_s: int = 5):
74
+ app = Application(backend="win32").connect(class_name="TFrmBuscaGeralDialog", timeout=timeout_s)
75
+ win = app.window(class_name="TFrmBuscaGeralDialog")
76
+ try:
77
+ win.set_focus()
78
+ except Exception:
79
+ pass
80
+ return app, win
81
+
82
+ def _find_grid_descendant(win):
83
+ best, best_area = None, -1
84
+ for d in win.descendants():
85
+ try:
86
+ cls = (d.class_name() or "").lower()
87
+ if "tcxgridsite" in cls or "tcxgrid" in cls:
88
+ r = d.rectangle()
89
+ area = max(0, (r.right - r.left)) * max(0, (r.bottom - r.top))
90
+ if area > best_area:
91
+ best, best_area = d, area
92
+ except:
93
+ pass
94
+ if not best:
95
+ raise RuntimeError("Grid não localizado (TcxGrid/TcxGridSite).")
96
+ return best
97
+
98
+ def _copy_active_row_text(retries: int = 1) -> str:
99
+ pyperclip.copy("")
100
+ send_keys("^c")
101
+ time.sleep(SLEEP_AFTER_COPY)
102
+ txt = pyperclip.paste().strip()
103
+ if txt or retries <= 0:
104
+ return txt
105
+ return _copy_active_row_text(retries - 1)
106
+
107
+ def _norm_str(s: str) -> str:
108
+ return re.sub(r"\s+", " ", unidecode(str(s or "")).strip()).lower()
109
+
110
+ # -------- novo critério: fornecedor (contains, case/acentos-insensitive) --------
111
+ def _linha_tem_fornecedor(txt: str, fornecedor: str) -> bool:
112
+ if not (txt and fornecedor):
113
+ return False
114
+ t = _norm_str(txt)
115
+ f = _norm_str(fornecedor)
116
+ # evita matches ridiculamente curtos que geram falso-positivo
117
+ if len(f) < 3:
118
+ return False
119
+ return f in t
120
+
121
+ # -------- varredura: já inicia na 1ª linha, sem clicar; clica OK ao encontrar --------
122
+ def selecionar_fornecedor_no_grid(fornecedor: str, max_linhas: int = 800) -> bool:
123
+ """
124
+ Pré-condição: o TFrmBuscaGeralDialog está aberto e o foco já está na PRIMEIRA LINHA do grid.
125
+ - Desce com SETA-PARA-BAIXO até encontrar uma linha cujo texto contenha o nome do FORNECEDOR.
126
+ - Quando encontra, clica no botão OK do diálogo e retorna True.
127
+ - Se não encontrar, retorna False.
128
+ """
129
+ app, win = _get_main_window_by_class()
130
+ grid = _find_grid_descendant(win)
131
+ try:
132
+ grid.set_focus()
133
+ except Exception:
134
+ pass
135
+
136
+ ultima = None
137
+ repet = 0
138
+
139
+ for _ in range(max_linhas):
140
+ linha = _copy_active_row_text()
141
+
142
+ if _linha_tem_fornecedor(linha, fornecedor):
143
+ # ✅ linha com o fornecedor encontrada; confirma no OK do diálogo
144
+ # tenta variações de título/classe
145
+ clicked = False
146
+ for title in ("&OK", "&Ok", "OK", "Ok"):
147
+ for cls in ("TBitBtn", "TButton"):
148
+ try:
149
+ btn_ok = win.child_window(title=title, class_name=cls)
150
+ btn_ok.click_input()
151
+ clicked = True
152
+ break
153
+ except Exception:
154
+ pass
155
+ if clicked:
156
+ break
157
+ if not clicked:
158
+ # fallback: ENTER
159
+ try:
160
+ send_keys("{ENTER}")
161
+ clicked = True
162
+ except Exception:
163
+ pass
164
+
165
+ return clicked
166
+
167
+ # fim de grid por repetição da mesma linha
168
+ if linha == ultima:
169
+ repet += 1
170
+ else:
171
+ repet = 0
172
+ ultima = linha
173
+
174
+ send_keys("{DOWN}")
175
+ time.sleep(SLEEP_BETWEEN_KEYS)
176
+
177
+ if repet >= 3:
178
+ break
179
+
180
+ return False
181
+
182
+ # -------- Critério por NOTA (mantido) --------
183
+ def _linha_tem_nota(txt: str, nota: str) -> bool:
184
+ """
185
+ Verdadeiro se a linha copiada contém a nota como sequência numérica inteira
186
+ (delimitada por não-dígitos). Garante que '123' não case com '1234'.
187
+ """
188
+ if not (txt and nota):
189
+ return False
190
+ nota = re.sub(r"\D+", "", str(nota)) # garante só dígitos
191
+ if not nota:
192
+ return False
193
+
194
+ # Normaliza e procura com fronteiras não numéricas
195
+ t = unidecode(txt)
196
+ # fronteiras: antes não dígito, depois não dígito (ou início/fim)
197
+ padrao = rf"(?<!\d){re.escape(nota)}(?!\d)"
198
+ return re.search(padrao, t) is not None
199
+
200
+ def _parse_int_tolerante(val):
201
+ if val is None:
202
+ return 0
203
+ if isinstance(val, int):
204
+ return val
205
+ s = str(val).replace("\u00a0", "").strip() # remove NBSP
206
+ s = s.replace(".", "").replace(",", "") # remove separadores comuns
207
+ return int(float(s or 0))
208
+
209
+ def _coletar_itens_achatados(d: dict):
210
+ descrs, qtds = {}, {}
211
+ for k, v in d.items():
212
+ m = re.match(r"^descricaoProduto(\d+)$", k, flags=re.I)
213
+ if m:
214
+ descrs[m.group(1)] = v
215
+ continue
216
+ m = re.match(r"^qtd(\d+)$", k, flags=re.I)
217
+ if m:
218
+ qtds[m.group(1)] = v
219
+ continue
220
+
221
+ itens = []
222
+ idxs = sorted(set(descrs.keys()) | set(qtds.keys()), key=lambda x: int(x))
223
+ for n in idxs:
224
+ desc = (descrs.get(n) or "").strip()
225
+ if not desc:
226
+ continue
227
+ qtd = _parse_int_tolerante(qtds.get(n, 0))
228
+ if qtd <= 0:
229
+ continue
230
+ itens.append({"descricaoProduto": desc, "qtd": qtd})
231
+ return itens
232
+
233
+ def normalize_config_entrada(cfg: dict) -> dict:
234
+ """
235
+ Se vier com 'itens': mantém e normaliza qtd -> int.
236
+ Se vier como descricaoProdutoN/qtdN: converte para 'itens'.
237
+ Preserva demais campos.
238
+ """
239
+ cfg = dict(cfg or {})
240
+ if isinstance(cfg.get("itens"), list):
241
+ itens_norm = []
242
+ for it in cfg["itens"]:
243
+ if not isinstance(it, dict):
244
+ continue
245
+ desc = str(it.get("descricaoProduto", "")).strip()
246
+ if not desc:
247
+ continue
248
+ qtd = _parse_int_tolerante(it.get("qtd", it.get("quantidade", 0)))
249
+ if qtd <= 0:
250
+ continue
251
+ itens_norm.append({"descricaoProduto": desc, "qtd": qtd})
252
+ cfg["itens"] = itens_norm
253
+ return cfg
254
+
255
+ # formato achatado
256
+ itens = _coletar_itens_achatados(cfg)
257
+ cfg["itens"] = itens
258
+ # remove chaves achatadas da saída (opcional)
259
+ chaves_remover = [
260
+ k for k in cfg.keys() if re.match(r"^(descricaoProduto|qtd)\d+$", k, flags=re.I)
261
+ ]
262
+ for k in chaves_remover:
263
+ cfg.pop(k, None)
264
+ return cfg
265
+
266
+ # --- fim do normalizador ---
267
+
268
+ async def devolucao_produtos(task: RpaProcessoEntradaDTO) -> RpaRetornoProcessoDTO:
269
+ try:
270
+ # Get config from BOF
271
+ config = await get_config_by_name("login_emsys")
272
+ console.print(task)
273
+
274
+ # Seta config entrada na var nota para melhor entendimento (normalizando formatos)
275
+ nota = normalize_config_entrada(task.configEntrada)
276
+ itens = nota.get("itens", [])
277
+
278
+ descricao_filial = task.configEntrada.get("descricaoFilial", "")
279
+ empresa = descricao_filial.split(" - ")[0]
280
+ estado = nota.get("estado", "")
281
+ descricao_fornecedor = nota.get("descricaoFornecedor", "")
282
+ historico_id = task.historico_id
283
+ cod_fornecedor = descricao_fornecedor.split(" - ")[0]
284
+ fornecedor = descricao_fornecedor.split(" - ")[1]
285
+ identificador = nota.get("identificador", "")
286
+ url_retorno = nota.get("urlRetorno", "")
287
+ multiplicador_timeout = int(float(task.sistemas[0].timeout))
288
+ set_variable("timeout_multiplicador", multiplicador_timeout)
289
+
290
+ # ========= CHAMADA AO ENDPOINT E MONTAGEM DO DATA =========
291
+ # res deve retornar {"lista": [...], "por_codigo": {cod: {...}}}
292
+ res = await get_notas_produtos(int(cod_fornecedor), int(empresa), itens)
293
+ by_code = res.get("por_codigo", {}) or {} # dict dinâmico por código
294
+
295
+ # ========= EXTRAÇÃO DE UM CFOP (PRIMEIRO ENCONTRADO) =========
296
+ cfop = None # Apenas um CFOP em variável
297
+ for info in by_code.values():
298
+ notas_raw = info.get("notas") or []
299
+ # garante que seja lista
300
+ if isinstance(notas_raw, (str, int)):
301
+ notas_raw = [notas_raw]
302
+
303
+ achou = False
304
+ for n in notas_raw:
305
+ try:
306
+ # Caso venha como string de dict: "{'nota': '1401414', 'cfop': '1102'}"
307
+ if isinstance(n, str) and n.strip().startswith("{"):
308
+ nota_dict = ast.literal_eval(n)
309
+ elif isinstance(n, dict):
310
+ nota_dict = n
311
+ else:
312
+ nota_dict = None
313
+
314
+ if isinstance(nota_dict, dict) and "cfop" in nota_dict:
315
+ cfop = nota_dict["cfop"]
316
+ achou = True
317
+ break
318
+ except Exception:
319
+ continue
320
+ if achou:
321
+ break
322
+
323
+ # Constrói os itens na estrutura usada no fluxo de UI
324
+ itens_ui = []
325
+ notas_encontradas = [] # acumula apenas os números das notas (strings) para deduplicar depois
326
+
327
+ for it in itens:
328
+ # aceita "descricaoProduto" contendo o código, ou "codigo"/"codigoProduto"
329
+ desc = it.get("descricaoProduto", "") or ""
330
+ nums = re.findall(r"\d+", desc)
331
+ if not nums:
332
+ # fallback: tenta campo "codigo" direto (se existir)
333
+ cod_raw = it.get("codigo") or it.get("codigoProduto")
334
+ if cod_raw is None:
335
+ continue
336
+ try:
337
+ cod = int(re.findall(r"\d+", str(cod_raw))[0])
338
+ except Exception:
339
+ continue
340
+ else:
341
+ cod = int(nums[0])
342
+
343
+ # quantidade (como int > 0)
344
+ qtd_raw = it.get("quantidade", it.get("qtd"))
345
+ try:
346
+ qtd = int(qtd_raw)
347
+ if qtd <= 0:
348
+ continue
349
+ except (TypeError, ValueError):
350
+ continue
351
+
352
+ info = by_code.get(cod) or {}
353
+ valor_unit = float(info.get("valorUnitario", 0) or 0)
354
+
355
+ # Normaliza "notas" para lista SÓ com os números das notas (strings)
356
+ notas_item_raw = info.get("notas") or []
357
+ if isinstance(notas_item_raw, (str, int)):
358
+ notas_item_raw = [notas_item_raw]
359
+
360
+ notas_item_nums = []
361
+ for n in notas_item_raw:
362
+ # 1) se já vier dict
363
+ if isinstance(n, dict):
364
+ nota_num = n.get("nota")
365
+ if nota_num is not None:
366
+ notas_item_nums.append(str(nota_num))
367
+ continue
368
+
369
+ # 2) se vier string de dict "{'nota': '1401414', 'cfop': '1102'}"
370
+ if isinstance(n, str) and n.strip().startswith("{"):
371
+ try:
372
+ d = ast.literal_eval(n)
373
+ if isinstance(d, dict) and d.get("nota") is not None:
374
+ notas_item_nums.append(str(d["nota"]))
375
+ continue
376
+ except Exception:
377
+ pass
378
+
379
+ # 3) fallback: manter como string (pode ser já o número)
380
+ notas_item_nums.append(str(n))
381
+
382
+ # Acumula para a lista geral (será deduplicada depois)
383
+ notas_encontradas.extend(notas_item_nums)
384
+
385
+ itens_ui.append(
386
+ {
387
+ "codigo": cod,
388
+ "quantidade": qtd,
389
+ "valor_unitario": valor_unit,
390
+ "valor_total_item": round(valor_unit * qtd, 2),
391
+ "notas": notas_item_nums, # vínculo item ↔ números das notas
392
+ }
393
+ )
394
+
395
+ # Deduplica notas preservando a ordem de aparição
396
+ nf_ref = list(dict.fromkeys(notas_encontradas))
397
+
398
+ # Índice opcional: itens por cada nota (facilita inclusão na UI)
399
+ itens_por_nota = defaultdict(list)
400
+ for item in itens_ui:
401
+ for n in item["notas"]:
402
+ itens_por_nota[n].append(item)
403
+
404
+ data = {
405
+ "nf_referencia": nf_ref, # ex.: ['1418727', '1410744']
406
+ "itens": itens_ui, # cada item com suas notas (apenas números)
407
+ "totais": {
408
+ "valor_final": round(sum(i["valor_total_item"] for i in itens_ui), 2)
409
+ },
410
+ "itens_por_nota": itens_por_nota, # para uso direto no fluxo de UI
411
+ "cfop": cfop, # <<< CFOP único extraído e disponível no payload
412
+ }
413
+ # ========= FIM DA MONTAGEM DO DATA =========
414
+
415
+ # Fecha a instancia do emsys - caso esteja aberta
416
+ await kill_all_emsys()
417
+
418
+ app = Application(backend="win32").start("C:\\Rezende\\EMSys3\\EMSys3_10.exe")
419
+ warnings.filterwarnings(
420
+ "ignore",
421
+ category=UserWarning,
422
+ message="32-bit application should be automated using 32-bit Python",
423
+ )
424
+ console.print("\nEMSys iniciando...", style="bold green")
425
+ return_login = await login_emsys(config.conConfiguracao, app, task)
426
+
427
+ if return_login.sucesso == True:
428
+ console.print("Pesquisando por: Cadastro Pré Venda")
429
+ type_text_into_field(
430
+ "Cadastro Pre-Venda", app["TFrmMenuPrincipal"]["Edit"], True, "50"
431
+ )
432
+ pyautogui.press("enter")
433
+ await worker_sleep(1)
434
+ pyautogui.press("enter")
435
+ console.print(
436
+ f"\nPesquisa: 'Cadastro Pre Venda' realizada com sucesso",
437
+ style="bold green",
438
+ )
439
+ await worker_sleep(3)
440
+ try:
441
+ app = Application().connect(class_name="TFrmSelecionaTipoPreVenda")
442
+ select_prevenda_type = app["Selecione o Tipo de Pré-Venda"]
443
+
444
+ if select_prevenda_type.exists():
445
+ tipo = select_prevenda_type.child_window(
446
+ class_name="TComboBox", found_index=0
447
+ )
448
+ tipo.select("Orçamento")
449
+ confirm = select_prevenda_type.child_window(
450
+ class_name="TDBIBitBtn", found_index=1
451
+ )
452
+ confirm.click()
453
+ except:
454
+ console.print(
455
+ "Sem tela de selecionar modelo de pre venda", style="bold green"
456
+ )
457
+ else:
458
+ logger.info(f"\nError Message: {return_login.retorno}")
459
+ console.print(f"\nError Message: {return_login.retorno}", style="bold red")
460
+ return return_login
461
+
462
+ await worker_sleep(7)
463
+
464
+ # Condição da Pré-Venda
465
+ console.print("Selecionando a Condição da Pré-Venda\n")
466
+ app = Application().connect(class_name="TFrmPreVenda")
467
+ main_window = app["TFrmPreVenda"]
468
+
469
+ condicao_field = main_window.child_window(
470
+ class_name="TDBIComboBox", found_index=2
471
+ )
472
+ condicao_field.select("21 DIAS")
473
+
474
+ # Código do fornecedor
475
+ input_cod_fornecedor = main_window.child_window(
476
+ class_name="TDBIEditNumber", found_index=2
477
+ )
478
+ input_cod_fornecedor.click_input()
479
+ await worker_sleep(0.2)
480
+ keyboard.send_keys("{END}+{HOME}{DEL}")
481
+ await worker_sleep(0.2)
482
+ input_cod_fornecedor.type_keys(
483
+ str(cod_fornecedor), with_spaces=True, set_foreground=True
484
+ )
485
+ keyboard.send_keys("{TAB}")
486
+ await worker_sleep(5)
487
+
488
+ # Popups
489
+ try:
490
+ app = Application().connect(class_name="TFrmSelecionaEndereco")
491
+ app["TFrmSelecionaEndereco"].close()
492
+ await worker_sleep(3)
493
+ app = Application().connect(class_name="TMessageForm")
494
+ app["TMessageForm"].child_window(
495
+ class_name="TButton", found_index=0
496
+ ).click()
497
+ except:
498
+ pass
499
+
500
+ app = Application().connect(class_name="TFrmPreVenda")
501
+ main_window = app["TFrmPreVenda"]
502
+ console.print("Verificar estado...")
503
+ cfop_dentro = ["5101", "5102", "5103", "5104", "1102"]
504
+ if cfop not in cfop_dentro:
505
+ modelo = "DEVOLUCAO DE COMPRA DE MERCADORIAS SC"
506
+ else:
507
+ modelo = "DEVOLUCAO DE COMPRA DE MERCADORIAS - TRIBUTADO"
508
+
509
+ await worker_sleep(3)
510
+
511
+ # Existe pre venda em aberto
512
+ try:
513
+ app_info = Application().connect(class_name="TMessageForm")
514
+ main_info = app_info["TMessageForm"]
515
+ btn_ok = main_info.child_window(class_name="TButton", found_index=0).click_input()
516
+ except:
517
+ pass
518
+
519
+ # Inserir modelo
520
+ console.print("Inserir modelo...")
521
+ select_modelo = main_window.child_window(
522
+ class_name="TDBIComboBox", found_index=0
523
+ )
524
+ select_modelo.select(modelo)
525
+ await worker_sleep(1)
526
+
527
+ # Abrir guia de itens (por imagem)
528
+ imagem_item = fr"{ASSETS_BASE_PATH}\itens.png"
529
+ for _ in range(10):
530
+ pos = pyautogui.locateCenterOnScreen(imagem_item, confidence=0.9)
531
+ if pos:
532
+ pyautogui.click(pos)
533
+ break
534
+ await worker_sleep(1)
535
+ else:
536
+ print("Imagem do item não encontrada na tela.")
537
+ await worker_sleep(2)
538
+
539
+ # Incluir item
540
+ botao_incluir = main_window.child_window(
541
+ title="Incluir", class_name="TDBIBitBtn"
542
+ ).wrapper_object()
543
+ botao_incluir.click_input()
544
+ await worker_sleep(5)
545
+
546
+ # =============== PREPARO DE DADOS ===============
547
+ almoxarifado = f"{descricao_filial}50"
548
+
549
+ # =============== ALMOXARIFADO ===============
550
+ console.print("Inserir Almoxarifado...")
551
+ # Abre tela de inclusão de item
552
+ app_item = Application().connect(class_name="TFrmIncluiItemPreVenda")
553
+ wnd_item = app_item["TFrmIncluiItemPreVenda"]
554
+ input_almoxarifado = wnd_item.child_window(
555
+ class_name="TDBIEditNumber", found_index=16
556
+ )
557
+ input_almoxarifado.click_input()
558
+ await worker_sleep(1)
559
+ keyboard.send_keys("{END}+{HOME}{DEL}")
560
+ await worker_sleep(1)
561
+ input_almoxarifado.type_keys(
562
+ almoxarifado, with_spaces=True, set_foreground=True
563
+ )
564
+ keyboard.send_keys("{TAB}")
565
+ await worker_sleep(1)
566
+
567
+ # Dicionário para guardar os itens sem saldo / com saldo
568
+ itens_sem_saldo = {}
569
+ itens_com_saldo = {}
570
+
571
+ # >>> NOVO: set para acumular SOMENTE notas dos itens com saldo
572
+ notas_validas_set = set()
573
+ # (opcional) controle de notas ligadas a itens sem saldo, útil para log
574
+ notas_descartadas_set = set()
575
+
576
+ # =============== LOOP POR ITEM (USANDO DADOS DA API) ===============
577
+ for item in data["itens"]:
578
+ await worker_sleep(2)
579
+ # Abre tela de inclusão de item
580
+ app_item = Application().connect(class_name="TFrmIncluiItemPreVenda")
581
+ wnd_item = app_item["TFrmIncluiItemPreVenda"]
582
+ codigo = str(item["codigo"])
583
+ quantidade = str(item["quantidade"])
584
+ val_unitario = f"{float(item['valor_unitario']):.2f}".replace(".", ",")
585
+ item_notas = item.get("notas", [])
586
+
587
+ # --- Código ---
588
+ console.print("Inserir código...")
589
+ input_codigo = wnd_item.child_window(
590
+ class_name="TDBIEditNumber", found_index=15
591
+ )
592
+ input_codigo.click_input()
593
+ await worker_sleep(1)
594
+ keyboard.send_keys("{END}+{HOME}{DEL}")
595
+ await worker_sleep(1)
596
+ input_codigo.type_keys(codigo, with_spaces=True, set_foreground=True)
597
+ keyboard.send_keys("{TAB}")
598
+ await worker_sleep(5)
599
+ try:
600
+ # Verificar item sem saldo
601
+ app_item_ss = Application().connect(class_name="TFrmPesquisaItem")
602
+ item_ss = app_item_ss["TFrmPesquisaItem"]
603
+ btn_ss = item_ss.child_window(
604
+ title="&Cancela", class_name="TDBIBitBtn").click_input()
605
+ # adiciona no dicionário de erros
606
+ itens_sem_saldo[codigo] = {
607
+ "quantidade": quantidade,
608
+ "valor_unitario": val_unitario,
609
+ }
610
+ continue
611
+
612
+ except:
613
+ pass
614
+
615
+ # --- Unidade (UNI) ---
616
+ console.print("Selecionar Unidade...")
617
+ select_uni = wnd_item.child_window(class_name="TDBIComboBox", found_index=1)
618
+ try:
619
+ # tenta selecionar diretamente UNI
620
+ select_uni.select("UNI")
621
+ except Exception:
622
+ try:
623
+ # tenta selecionar UN se UNI não existir
624
+ select_uni.select("UN")
625
+ except Exception as e:
626
+ print(e)
627
+
628
+ await worker_sleep(1)
629
+
630
+ # --- Quantidade ---
631
+ console.print("Inserir quantidade...")
632
+ wnd_item.child_window(
633
+ class_name="TDBIEditNumber", found_index=8
634
+ ).click_input()
635
+ await worker_sleep(1)
636
+ keyboard.send_keys("{END}+{HOME}{DEL}")
637
+ await worker_sleep(1)
638
+ keyboard.send_keys(quantidade)
639
+ keyboard.send_keys("{TAB}")
640
+ await worker_sleep(1)
641
+
642
+ # --- Valor Unitário via popup ---
643
+ console.print("Inserir valor unitário...")
644
+ wnd_item.child_window(
645
+ class_name="TDBIEditNumber", found_index=6
646
+ ).click_input()
647
+ await worker_sleep(1)
648
+ keyboard.send_keys("{TAB}")
649
+ await worker_sleep(1)
650
+ keyboard.send_keys("{ENTER}")
651
+ await worker_sleep(1)
652
+
653
+ app_preco = Application().connect(class_name="TFrmInputBoxNumero")
654
+ wnd_preco = app_preco["TFrmInputBoxNumero"]
655
+
656
+ campo_preco = wnd_preco.child_window(
657
+ class_name="TDBIEditNumber", found_index=0
658
+ )
659
+ campo_preco.click_input()
660
+ await worker_sleep(1)
661
+ keyboard.send_keys("{END}+{HOME}{DEL}")
662
+ await worker_sleep(1)
663
+ campo_preco.type_keys(val_unitario, with_spaces=True, set_foreground=True)
664
+ await worker_sleep(1)
665
+ wnd_preco.child_window(class_name="TBitBtn", found_index=1).click_input()
666
+ await worker_sleep(2)
667
+
668
+ # --- Confirmar Incluir ---
669
+ console.print("Confirmar inclusão do item...")
670
+ app_prevenda = Application().connect(class_name="TFrmIncluiItemPreVenda")
671
+ wnd_prevenda = app_prevenda["TFrmIncluiItemPreVenda"]
672
+ botao_incluir = wnd_prevenda.child_window(
673
+ title="&Incluir", class_name="TDBIBitBtn"
674
+ ).wrapper_object()
675
+ botao_incluir.click_input()
676
+
677
+ await worker_sleep(4)
678
+
679
+ # ================== VERIFICAÇÃO DE SALDO ==================
680
+ had_saldo = True
681
+ try:
682
+ console.print("Verificar mensagem de saldo menor....")
683
+ img_saldo = fr"{ASSETS_BASE_PATH}\saldo_menor.png"
684
+ img_saldo_bool = False
685
+
686
+ for _ in range(10):
687
+ pos = pyautogui.locateCenterOnScreen(img_saldo, confidence=0.9)
688
+ if pos:
689
+ console.print(
690
+ f"Saldo disponível menor para o item {codigo}: {quantidade} x {val_unitario}"
691
+ )
692
+
693
+ # adiciona no dicionário de erros
694
+ itens_sem_saldo[codigo] = {
695
+ "quantidade": quantidade,
696
+ "valor_unitario": val_unitario,
697
+ }
698
+
699
+ # fecha a mensagem
700
+ app = Application().connect(class_name="TMessageForm")
701
+ main_window_msg = app["TMessageForm"]
702
+ btn_no = main_window_msg.child_window(
703
+ title="&No", class_name="TButton"
704
+ )
705
+ btn_no.click_input()
706
+
707
+ # clica em limpar
708
+ app = Application().connect(class_name="TFrmIncluiItemPreVenda")
709
+ main_window_limpa = app["TFrmIncluiItemPreVenda"]
710
+ btn_limpa = main_window_limpa.child_window(
711
+ title="&Limpa", class_name="TDBIBitBtn"
712
+ )
713
+ btn_limpa.click_input()
714
+
715
+ img_saldo_bool = True
716
+ had_saldo = False
717
+ break
718
+ await worker_sleep(1)
719
+
720
+ await worker_sleep(3)
721
+
722
+ if img_saldo_bool:
723
+ # saldo menor que quantidade
724
+ for n in item_notas:
725
+ notas_descartadas_set.add(str(n))
726
+ continue
727
+
728
+ except Exception:
729
+ # Se der algum erro na verificação da imagem, assumimos sucesso (com saldo)
730
+ had_saldo = True
731
+
732
+ # Se teve saldo, registra e marca notas válidas
733
+ if had_saldo:
734
+ console.print(f"Item {codigo} incluído com sucesso.")
735
+ itens_com_saldo[codigo] = {
736
+ "quantidade": quantidade,
737
+ "valor_unitario": val_unitario,
738
+ }
739
+ for n in item_notas:
740
+ notas_validas_set.add(str(n))
741
+ continue
742
+
743
+ # Depois de processar todos os itens:
744
+ if itens_sem_saldo and not itens_com_saldo:
745
+ # Todos os itens ficaram sem saldo → para aqui
746
+ log_msg = "Todos os itens estão com saldo menor que a quantidade:\n" + "\n".join(
747
+ f"- Código: {cod} | Quantidade: {dados['quantidade']} | Valor Unitário: {dados['valor_unitario']}"
748
+ for cod, dados in itens_sem_saldo.items()
749
+ )
750
+ return RpaRetornoProcessoDTO(
751
+ sucesso=False,
752
+ retorno=log_msg.strip(),
753
+ status=RpaHistoricoStatusEnum.Falha,
754
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Negocio)],
755
+ )
756
+
757
+ # Caso contrário, existe pelo menos 1 com saldo → segue o fluxo
758
+ console.print("Há itens com saldo. Continuando o fluxo até o final...")
759
+
760
+ try:
761
+ # Liberação de preço (se aparecer)
762
+ app = Application().connect(class_name="TFrmUsuariosLiberacaoPreco")
763
+ login = app.window(class_name="TFrmUsuariosLiberacaoPreco")
764
+ login.child_window(class_name="TEdit", found_index=0).click_input()
765
+ login.type_keys("rpa.marvin", with_spaces=True, set_foreground=True)
766
+ login.child_window(class_name="TEdit", found_index=1).click_input()
767
+ login.type_keys("cba321", with_spaces=True, set_foreground=True)
768
+ login.child_window(class_name="TBitBtn", found_index=0).click_input()
769
+ except:
770
+ pass
771
+
772
+ # Clicar em fechar
773
+ wnd_prevenda.close()
774
+
775
+ await worker_sleep(3)
776
+
777
+ # Clicar em recebimentos
778
+ console.print("Clicar em recebimentos...")
779
+ imagem_item = fr"{ASSETS_BASE_PATH}\btn_recebimentos.png"
780
+ for _ in range(10):
781
+ pos = pyautogui.locateCenterOnScreen(imagem_item, confidence=0.9)
782
+ if pos:
783
+ pyautogui.doubleClick(pos)
784
+ break
785
+ await worker_sleep(1)
786
+ else:
787
+ print("Imagem do item não encontrada na tela.")
788
+
789
+ await worker_sleep(3)
790
+
791
+ # Clicar em Parcelamento
792
+ console.print("Clicar em parcelamento...")
793
+ imagem_parc = fr"{ASSETS_BASE_PATH}\btn_parcelamento.png"
794
+ for _ in range(10):
795
+ pos = pyautogui.locateCenterOnScreen(imagem_parc, confidence=0.8)
796
+ if pos:
797
+ pyautogui.doubleClick(pos)
798
+ break
799
+ await worker_sleep(1)
800
+ else:
801
+ print("Imagem do item não encontrada na tela.")
802
+
803
+ await worker_sleep(3)
804
+
805
+ # Volta pra janela de pre venda
806
+ app = Application().connect(class_name="TFrmPreVenda")
807
+ main_window = app["TFrmPreVenda"]
808
+
809
+ # Condição de recebimento (boleto)
810
+ console.print("Selecionar boleto...")
811
+ condicao_field = main_window.child_window(
812
+ class_name="TDBIComboBox", found_index=0
813
+ )
814
+ try:
815
+ condicao_field.select("BANCO DO BRASIL BOLETO")
816
+ print("Selecionado: BANCO DO BRASIL BOLETO")
817
+ except Exception as e:
818
+ print(
819
+ f"Não foi possível selecionar 'BANCO DO BRASIL BOLETO' ({e}). Tentando 'BOLETO'..."
820
+ )
821
+ try:
822
+ condicao_field.select("BOLETO")
823
+ print("Selecionado: BOLETO")
824
+ except Exception as e2:
825
+ print(f"❌ Falha também ao selecionar 'BOLETO': {e2}")
826
+
827
+ # Clicar em Incluir
828
+ console.print("Incluir registro...")
829
+ imagem_incluir = fr"{ASSETS_BASE_PATH}\IncluirRegistro.png"
830
+ for _ in range(10):
831
+ pos = pyautogui.locateCenterOnScreen(imagem_incluir, confidence=0.8)
832
+ if pos:
833
+ pyautogui.click(pos)
834
+ break
835
+ await worker_sleep(1)
836
+ else:
837
+ print("Imagem do item não encontrada na tela.")
838
+
839
+ await worker_sleep(3)
840
+
841
+ # Capturar número da pré-venda
842
+ console.print("Capturar número da pré-venda...")
843
+ numero_pre_venda = None
844
+ timeout = 10
845
+ t0 = time.time()
846
+ while time.time() - t0 < timeout:
847
+ try:
848
+ win = Desktop(backend="win32").window(title_re=".*Informa.*")
849
+ if not win.exists(timeout=0.2):
850
+ time.sleep(0.3)
851
+ continue
852
+ win.set_focus()
853
+
854
+ textos = []
855
+ try:
856
+ textos.append(win.window_text())
857
+ except:
858
+ pass
859
+ try:
860
+ textos += [t for t in win.wrapper_object().texts() if t]
861
+ except:
862
+ pass
863
+ try:
864
+ for st in win.children(class_name="Static"):
865
+ textos += [t for t in st.texts() if t]
866
+ except:
867
+ pass
868
+
869
+ texto = "\n".join([t for t in textos if t])
870
+ if ("Venda inclu" in texto) or ("Pré" in texto) or ("Pr" in texto):
871
+ m = re.search(r"\b(\d{3,}-\d{1,})\b", texto)
872
+ if m:
873
+ numero_pre_venda = m.group(1)
874
+
875
+ clicked = False
876
+ for title in ("OK", "&OK"):
877
+ try:
878
+ win.child_window(
879
+ title=title, class_name="TButton"
880
+ ).click_input()
881
+ clicked = True
882
+ break
883
+ except:
884
+ pass
885
+ if not clicked:
886
+ try:
887
+ win.type_keys("{ENTER}")
888
+ except:
889
+ pass
890
+ break
891
+ except:
892
+ time.sleep(0.3)
893
+
894
+ print("Número da pré-venda:", numero_pre_venda)
895
+ await worker_sleep(5)
896
+
897
+ # Confirmar pré-venda (Yes)
898
+ console.print("Confirmar pré-venda...")
899
+ app = Application().connect(class_name="TMessageForm")
900
+ main_window = app["TMessageForm"]
901
+ btn_ok = main_window.child_window(title="&Yes", class_name="TButton")
902
+ btn_ok.click_input()
903
+ await worker_sleep(4)
904
+
905
+ # Botão confirma
906
+ app = Application().connect(class_name="TFrmPreVenda")
907
+ main_window = app["TFrmPreVenda"]
908
+ btn_confirmar = main_window.child_window(
909
+ title="&Confirma", class_name="TBitBtn"
910
+ )
911
+ btn_confirmar.click_input()
912
+ await worker_sleep(4)
913
+
914
+ # Confirmar (Yes)
915
+ app = Application().connect(class_name="TMessageForm")
916
+ main_window = app["TMessageForm"]
917
+ btn_confirmar = main_window.child_window(title="&Yes", class_name="TButton")
918
+ btn_confirmar.click_input()
919
+ await worker_sleep(10)
920
+
921
+ # Fechar "Informação"
922
+ for _ in range(10):
923
+ try:
924
+ dlg = Desktop(backend="win32").window(
925
+ title_re="Informação", class_name="#32770"
926
+ )
927
+ if dlg.exists(timeout=1):
928
+ dlg.child_window(title="OK").click_input()
929
+ print("✅ Fechou janela 'Informação'.")
930
+ break
931
+ except:
932
+ pass
933
+ time.sleep(1)
934
+
935
+ await worker_sleep(3)
936
+
937
+ # Faturar
938
+ console.print("Clicar em faturar...")
939
+ app = Application().connect(class_name="TFrmPreVenda")
940
+ main_window = app["TFrmPreVenda"]
941
+ main_window.set_focus()
942
+ btn_faturar = main_window.child_window(title="&Faturar", class_name="TBitBtn")
943
+ btn_faturar.click_input()
944
+ await worker_sleep(5)
945
+ print("Botão 'Faturar' clicado com sucesso!")
946
+
947
+ # Recalcular Parcelas? (Yes)
948
+ console.print("Clicar em recalcular parcelas...")
949
+ app = Application().connect(class_name="TMessageForm")
950
+ main_window = app["TMessageForm"]
951
+ main_window.set_focus()
952
+ btn_confirmar = main_window.child_window(title="&Yes", class_name="TButton")
953
+ btn_confirmar.click_input()
954
+
955
+ for _ in range(10):
956
+ try:
957
+ dlg = Desktop(backend="win32").window(
958
+ title_re="Parcelas - Nota Fiscal Sa", class_name="#32770"
959
+ )
960
+ if dlg.exists(timeout=1):
961
+ dlg.child_window(title="&Não").click_input()
962
+ print("Clicar em Não")
963
+ break
964
+ except:
965
+ pass
966
+
967
+ # --- Notas referenciadas ---
968
+ console.print("Aguardando imagem 'notas_referenciadas.png' aparecer...")
969
+ imagem_notas_ref = fr"{ASSETS_BASE_PATH}\notas_referenciadas.png"
970
+
971
+ # 1) valida o arquivo
972
+ if not os.path.exists(imagem_notas_ref):
973
+ console.print(f"Arquivo não encontrado: {imagem_notas_ref}")
974
+ else:
975
+ timeout = 600 # segundos
976
+ intervalo = 2.0 # segundos entre tentativas
977
+ inicio = time.monotonic()
978
+ pos = None
979
+ tentativas = 0
980
+
981
+ while True:
982
+ tentativas += 1
983
+ try:
984
+ pos = pyautogui.locateCenterOnScreen(
985
+ imagem_notas_ref, confidence=0.80, grayscale=True
986
+ )
987
+ except Exception as e:
988
+ # Qualquer erro do PyAutoGUI aqui será logado, mas não quebra o loop.
989
+ console.print(f"⚠️ locateCenterOnScreen falhou (tentativa {tentativas}): {e}")
990
+ pos = None
991
+
992
+ if pos is not None:
993
+ decorrido = int(time.monotonic() - inicio)
994
+ console.print(f"Imagem encontrada após {decorrido}s (tentativa {tentativas}). Clicando...")
995
+ pyautogui.click(pos)
996
+ break
997
+
998
+ # ainda não achou — verifica timeout
999
+ decorrido = time.monotonic() - inicio
1000
+ if decorrido >= timeout:
1001
+ console.print(f"Imagem 'notas_referenciadas.png' não encontrada em {timeout} segundos.")
1002
+ # Se você REALMENTE quer esperar até aparecer (sem limite), remova o bloco de timeout acima.
1003
+ break
1004
+
1005
+ # feedback periódico a cada ~10s
1006
+ if int(decorrido) % 10 == 0:
1007
+ console.print(f"Aguardando... {int(decorrido)}s passados (tentativa {tentativas}).")
1008
+
1009
+ time.sleep(intervalo)
1010
+
1011
+ await worker_sleep(2)
1012
+
1013
+ # Faturamento Pré-venda
1014
+ app = Application().connect(class_name="TFrmDadosFaturamentoPreVenda")
1015
+ main_window = app["TFrmDadosFaturamentoPreVenda"]
1016
+ main_window.set_focus()
1017
+
1018
+ # Radio Entrada
1019
+ main_window.child_window(
1020
+ title="Entrada", class_name="TDBIRadioButton"
1021
+ ).click_input()
1022
+ console.print("Clicado em 'Entrada'")
1023
+ await worker_sleep(4)
1024
+
1025
+ # ====== FILTRAR NOTAS VÁLIDAS ======
1026
+ todas_as_notas = [str(n) for n in data.get("nf_referencia", [])]
1027
+ notas_validas_ordenadas = [n for n in todas_as_notas if n in notas_validas_set]
1028
+ notas_descartadas = [n for n in todas_as_notas if n not in notas_validas_set]
1029
+
1030
+ console.print(
1031
+ f"[green]Notas a referenciar (itens com saldo): {notas_validas_ordenadas}[/]"
1032
+ )
1033
+ if notas_descartadas:
1034
+ console.print(
1035
+ f"[yellow]Notas descartadas (apenas itens sem saldo): {notas_descartadas}[/]"
1036
+ )
1037
+
1038
+ # >>> NOVO: nota_arquivo (primeira válida; fallback: pré-venda ou timestamp) - SEM extract_nf_number
1039
+ if notas_validas_ordenadas:
1040
+ nota_arquivo = re.sub(r"\D+", "", str(notas_validas_ordenadas[0])) or str(
1041
+ notas_validas_ordenadas[0]
1042
+ )
1043
+ else:
1044
+ if numero_pre_venda:
1045
+ nota_arquivo = re.sub(r"\D+", "", str(numero_pre_venda)) or str(
1046
+ numero_pre_venda
1047
+ )
1048
+ else:
1049
+ nota_arquivo = datetime.now().strftime("%Y%m%d%H%M%S")
1050
+
1051
+ # === LOOP REFERENCIANDO APENAS NOTAS VÁLIDAS ===
1052
+ for nf_ref_atual in notas_validas_ordenadas:
1053
+ itens_da_nota = data.get("itens_por_nota", {}).get(nf_ref_atual, [])
1054
+ if not itens_da_nota:
1055
+ console.print(
1056
+ f"[amarelo]Nenhum item associado à nota {nf_ref_atual}. Pulando...[/]"
1057
+ )
1058
+ continue
1059
+
1060
+ console.print(f"[cyan]Processando nota {nf_ref_atual}...[/]")
1061
+
1062
+ # 1) Focar e limpar o campo da nota
1063
+ input_num_nota = main_window.child_window(class_name="TDBIEditDescription")
1064
+ input_num_nota.set_focus()
1065
+ try:
1066
+ input_num_nota.select() # alguns campos suportam select()
1067
+ except Exception:
1068
+ pass
1069
+ keyboard.send_keys("^a{DEL}") # Ctrl+A + Delete (fallback)
1070
+ await worker_sleep(0.4)
1071
+
1072
+ # 2) Digitar a nota e confirmar
1073
+ input_num_nota.type_keys(
1074
+ str(nf_ref_atual), with_spaces=True, set_foreground=True
1075
+ )
1076
+ keyboard.send_keys("{ENTER}")
1077
+
1078
+ await worker_sleep(3)
1079
+ try:
1080
+ # Abrir diálogo de busca e SELECIONAR fornecedor (clicando OK ao encontrar)
1081
+ app_notas = Application().connect(class_name="TFrmBuscaGeralDialog")
1082
+ # NÃO clique em botões aqui antes da varredura.
1083
+ ok = selecionar_fornecedor_no_grid(fornecedor)
1084
+ if not ok:
1085
+ console.print(f"[yellow]Fornecedor '{fornecedor}' não encontrado no diálogo de busca.[/]")
1086
+ except:
1087
+ pass
1088
+
1089
+ try:
1090
+ # Clicar em incluir itens (folha de papel com +)
1091
+ app = Application().connect(class_name="TFrmDadosFaturamentoPreVenda")
1092
+ main_window = app["TFrmDadosFaturamentoPreVenda"]
1093
+
1094
+ # Clica no botão identificado como TDBIBitBtn2
1095
+ main_window.child_window(
1096
+ class_name="TDBIBitBtn", found_index=1
1097
+ ).click_input()
1098
+
1099
+ print("Botão clicado com sucesso!")
1100
+ console.print(f"Incluindo itens vinculados à nota {nf_ref_atual}...")
1101
+ except:
1102
+ pass
1103
+
1104
+ # Aba mensagens
1105
+ console.print("Clicar em mensagens...")
1106
+ imagem_notas_ref = fr"{ASSETS_BASE_PATH}\aba_mensagem.png"
1107
+ for _ in range(10):
1108
+ pos = pyautogui.locateCenterOnScreen(imagem_notas_ref, confidence=0.9)
1109
+ if pos:
1110
+ pyautogui.click(pos)
1111
+ break
1112
+ await worker_sleep(1)
1113
+ else:
1114
+ print("Imagem do item não encontrada na tela.")
1115
+ await worker_sleep(5)
1116
+
1117
+ # Mensagem interna
1118
+ imagem_notas_ref = fr"{ASSETS_BASE_PATH}\mensagem_interna.png"
1119
+ for _ in range(10):
1120
+ pos = pyautogui.locateCenterOnScreen(imagem_notas_ref, confidence=0.8)
1121
+ if pos:
1122
+ pyautogui.click(pos)
1123
+ break
1124
+ await worker_sleep(1)
1125
+ else:
1126
+ print("Imagem do item não encontrada na tela.")
1127
+ await worker_sleep(4)
1128
+
1129
+ # Inserir mensagem padrão
1130
+ console.print("Inserir mensagem...")
1131
+ lista_fornecedores = ["Disbal", "Pepsico", "Punta Balena"]
1132
+ mensagem = (
1133
+ "PRODUTOS VENCIDOS"
1134
+ if fornecedor in lista_fornecedores
1135
+ else "ACORDO COMERCIAL"
1136
+ )
1137
+ input_mensagem = main_window.child_window(class_name="TDBIMemo", found_index=0)
1138
+ input_mensagem.type_keys(mensagem, with_spaces=True, set_foreground=True)
1139
+
1140
+ # Aba itens
1141
+ imagem_itens = fr"{ASSETS_BASE_PATH}\aba_itens.png"
1142
+ for _ in range(10):
1143
+ pos = pyautogui.locateCenterOnScreen(imagem_itens, confidence=0.9)
1144
+ if pos:
1145
+ pyautogui.click(pos)
1146
+ break
1147
+ await worker_sleep(1)
1148
+ else:
1149
+ print("Imagem do item não encontrada na tela.")
1150
+
1151
+ await worker_sleep(3)
1152
+
1153
+ # Corrige tributação
1154
+ console.print("Corrigir tributação...")
1155
+ imagem_itens = fr"{ASSETS_BASE_PATH}\corrige_tributacao.png"
1156
+ for _ in range(10):
1157
+ pos = pyautogui.locateCenterOnScreen(imagem_itens, confidence=0.9)
1158
+ if pos:
1159
+ pyautogui.click(pos)
1160
+ break
1161
+ await worker_sleep(1)
1162
+ else:
1163
+ print("Imagem do tributacao não encontrada na tela.")
1164
+
1165
+ await worker_sleep(3)
1166
+
1167
+ # Selecionar tributação
1168
+ console.print("Selecionar tributação...")
1169
+ app = Application().connect(class_name="TFrmDadosTributacaoProdutoPreVenda")
1170
+ trib = app["TFrmDadosTributacaoProdutoPreVenda"]
1171
+
1172
+ if "disbal" in fornecedor.lower():
1173
+ select_trib = trib.child_window(class_name="TDBIComboBox", found_index=4)
1174
+ select_trib.select("020 - 020 - ICMS 12% RED. BASE 41,667")
1175
+
1176
+ elif "punta balena" in fornecedor.lower():
1177
+ select_trib = trib.child_window(class_name="TDBIComboBox", found_index=4)
1178
+ select_trib.select("000 - 000 - ICMS - 12%")
1179
+
1180
+ elif "vitrola" in fornecedor.lower():
1181
+ select_trib = trib.child_window(class_name="TDBIComboBox", found_index=4)
1182
+ select_trib.select("041 - 041 - ICMS - NAO INCIDENTE ")
1183
+
1184
+ elif estado == "RS" and "pepsico" in fornecedor.lower():
1185
+ select_trib = trib.child_window(class_name="TDBIComboBox", found_index=4)
1186
+ select_trib.select("051 - 051 - ICMS 17% RED BC 29,4118% - TRIBUT.CORRETA")
1187
+ print("Selecionado: 051 - 051 - ICMS 17% RED BC 29,4118% - TRIBUT.CORRETA")
1188
+
1189
+ elif estado == "RS":
1190
+ select_trib = trib.child_window(class_name="TDBIComboBox", found_index=4)
1191
+ select_trib.select("051 - 051 - ICMS 17% RED BC 29,4118% - TRIBUT.CORRETA")
1192
+ print("Selecionado: 051 - 051 - ICMS 17% RED BC 29,4118% - TRIBUT.CORRETA")
1193
+
1194
+ elif estado == "SC":
1195
+ select_trib = trib.child_window(class_name="TDBIComboBox", found_index=4)
1196
+ select_trib.select("000 - 000 - ICMS - 12%")
1197
+ print("Selecionado: 000 - 000 - ICMS - 12%")
1198
+
1199
+ else:
1200
+ print("Estado diferente dos mapeados")
1201
+
1202
+ await worker_sleep(2)
1203
+
1204
+ trib.child_window(title="&OK", class_name="TBitBtn").click_input()
1205
+
1206
+ await worker_sleep(3)
1207
+
1208
+ # --- Verifica se abriu a janela "Corrige tributação?" ---
1209
+ try:
1210
+ # Usa busca tolerante a variações ("Corrige tributa??o", "Corrige tributacao", etc)
1211
+ dlg = Desktop(backend="win32").window(title_re=".*Corrige\s+tribut", found_index=0)
1212
+ if dlg.exists(timeout=1):
1213
+ dlg.set_focus()
1214
+ try:
1215
+ # tenta clicar no botão Sim (pode estar com &Sim ou Sim)
1216
+ try:
1217
+ dlg.child_window(title="&Sim").click_input()
1218
+ except Exception:
1219
+ dlg.child_window(title="Sim").click_input()
1220
+ console.print("Clicou em 'Sim' na janela 'Corrige tributação?'")
1221
+ except Exception:
1222
+ # fallback: Alt+S (atalho do &Sim)
1223
+ send_keys("%s")
1224
+ console.print("⚙️ Clicou via Alt+S (fallback) em 'Corrige tributação?'")
1225
+ else:
1226
+ console.print("Nenhuma janela 'Corrige tributação?' encontrada.")
1227
+ except Exception as e:
1228
+ console.print(f"Erro ao tentar clicar em 'Corrige tributação?': {e}")
1229
+
1230
+ await worker_sleep(3)
1231
+ # Aba principal
1232
+ imagem_principal = fr"{ASSETS_BASE_PATH}\aba_principal.png"
1233
+ for _ in range(10):
1234
+ pos = pyautogui.locateCenterOnScreen(imagem_principal, confidence=0.9)
1235
+ if pos:
1236
+ pyautogui.click(pos)
1237
+ break
1238
+ await worker_sleep(1)
1239
+ else:
1240
+ print("Imagem do item não encontrada na tela.")
1241
+
1242
+ await worker_sleep(5)
1243
+
1244
+ # DANFE 077
1245
+ console.print(
1246
+ "Selecionar NFe - NOTA FISCAL ELETRONICA PROPRIA - DANFE SERIE 077..."
1247
+ )
1248
+ app = Application().connect(class_name="TFrmDadosFaturamentoPreVenda")
1249
+ main_window = app["TFrmDadosFaturamentoPreVenda"]
1250
+ select_danfe = main_window.child_window(
1251
+ class_name="TDBIComboBox", found_index=1
1252
+ )
1253
+ select_danfe.select("NFe - NOTA FISCAL ELETRONICA PROPRIA - DANFE SERIE 077")
1254
+
1255
+ await worker_sleep(2)
1256
+
1257
+ # OK
1258
+ main_window.child_window(title="&OK", class_name="TBitBtn").click_input()
1259
+
1260
+ await worker_sleep(10)
1261
+
1262
+ # Faturar pré-venda (Yes)
1263
+ app = Application().connect(class_name="TMessageForm")
1264
+ main_window = app["TMessageForm"]
1265
+ main_window.child_window(class_name="TButton", found_index=1).click()
1266
+
1267
+ await worker_sleep(5)
1268
+
1269
+ # Faturar pré-venda (Yes)
1270
+ app = Application().connect(class_name="TMessageForm")
1271
+ main_window = app["TMessageForm"]
1272
+ main_window.child_window(
1273
+ title="Transmitir e &Imprimir", class_name="TButton"
1274
+ ).click_input()
1275
+
1276
+ await worker_sleep(10)
1277
+
1278
+ # Diálogo impressão
1279
+ console.print("Confirmar impressão...")
1280
+ app = Application().connect(class_name="TppPrintDialog")
1281
+ main_window = app["TppPrintDialog"]
1282
+ main_window.child_window(title="OK", class_name="TButton").click()
1283
+
1284
+ await worker_sleep(5)
1285
+
1286
+ console.print(f"NAVEGANDO NA TELA DE SALVAR RELATORIO\n")
1287
+ # INSERINDO O DIRETORIO E SALVANDO O ARQUIVO
1288
+ try:
1289
+ app = Application().connect(title="Salvar Saída de Impressão como")
1290
+ main_window = app["Dialog"]
1291
+ console.print("Tela 'Salvar' encontrada!")
1292
+
1293
+ console.print("Interagindo com a tela 'Salvar'...\n")
1294
+ username = getpass.getuser()
1295
+
1296
+ # Preenche o nome do arquivo - SOMENTE número da nota
1297
+ path_to_txt = f"C:\\Users\\{username}\\Downloads\\devolucao_nf_{estado}_{nota_arquivo}"
1298
+
1299
+ main_window.type_keys("%n")
1300
+ pyautogui.write(path_to_txt)
1301
+ await worker_sleep(1)
1302
+ main_window.type_keys("%l")
1303
+ console.print("Arquivo salvo com sucesso...\n")
1304
+ await worker_sleep(8)
1305
+ except Exception as e:
1306
+ retorno = f"Não foi salvar o arquivo: {e}"
1307
+ return RpaRetornoProcessoDTO(
1308
+ sucesso=False,
1309
+ retorno=retorno,
1310
+ status=RpaHistoricoStatusEnum.Falha,
1311
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)],
1312
+ )
1313
+
1314
+ with open(f"{path_to_txt}.pdf", "rb") as file:
1315
+ file_bytes = io.BytesIO(file.read())
1316
+
1317
+ desArquivo = f"devolucao_nf_{estado}_{nota_arquivo}.pdf"
1318
+ try:
1319
+ await send_file(
1320
+ historico_id, desArquivo, "pdf", file_bytes, file_extension="pdf"
1321
+ )
1322
+ os.remove(f"{path_to_txt}.pdf")
1323
+ except Exception as e:
1324
+ result = (
1325
+ f"Arquivo gerado com sucesso, porém erro ao enviar para o backoffice: {e} "
1326
+ f"- Arquivo salvo em {path_to_txt}.pdf"
1327
+ )
1328
+ console.print(result, style="bold red")
1329
+ return RpaRetornoProcessoDTO(
1330
+ sucesso=False,
1331
+ retorno=result,
1332
+ status=RpaHistoricoStatusEnum.Falha,
1333
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Tecnico)],
1334
+ )
1335
+
1336
+ except Exception as ex:
1337
+ log_msg = f"Error: {ex}"
1338
+ print(ex)
1339
+ return RpaRetornoProcessoDTO(
1340
+ sucesso=False,
1341
+ retorno=log_msg,
1342
+ status=RpaHistoricoStatusEnum.Falha,
1343
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Negocio)],
1344
+ )
1345
+
1346
+ # ============== RESUMO FINAL ==============
1347
+ def _fmt_linha(cod, dados):
1348
+ return f"- Código: {cod} | Quantidade: {dados.get('quantidade')} | Valor Unitário: {dados.get('valor_unitario')}"
1349
+
1350
+ resumo_partes = []
1351
+
1352
+ if itens_com_saldo:
1353
+ lista_ok = "\n".join(_fmt_linha(c, d) for c, d in list(itens_com_saldo.items()))
1354
+ resumo_partes.append(
1355
+ "✅ Itens incluídos:\n" + (lista_ok if lista_ok else "(vazio)")
1356
+ )
1357
+
1358
+ if itens_sem_saldo:
1359
+ lista_sem = "\n".join(
1360
+ _fmt_linha(c, d) for c, d in list(itens_sem_saldo.items())
1361
+ )
1362
+ resumo_partes.append(
1363
+ "⚠️ Itens sem saldo:\n" + (lista_sem if lista_sem else "(vazio)")
1364
+ )
1365
+
1366
+ # (Opcional) resumo sobre notas válidas/descartadas
1367
+ try:
1368
+ resumo_partes.append(
1369
+ "🧾 Notas referenciadas: " + ", ".join(sorted(list(notas_validas_set)))
1370
+ if notas_validas_set
1371
+ else "🧾 Notas referenciadas: (nenhuma)"
1372
+ )
1373
+ except:
1374
+ pass
1375
+
1376
+ resumo_txt = (
1377
+ "\n\n".join(resumo_partes) if resumo_partes else "Nenhum item processado."
1378
+ )
1379
+
1380
+ return RpaRetornoProcessoDTO(
1381
+ sucesso=True,
1382
+ retorno=f"Processo concluído.\n\n{resumo_txt}",
1383
+ status=RpaHistoricoStatusEnum.Sucesso,
1384
+ tags=[RpaTagDTO(descricao=RpaTagEnum.Negocio)],
1385
+ )
1386
+