worker-automate-hub 0.5.749__py3-none-any.whl → 0.5.912__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- worker_automate_hub/api/client.py +186 -68
- worker_automate_hub/api/rpa_historico_service.py +1 -0
- worker_automate_hub/cli.py +91 -111
- worker_automate_hub/tasks/jobs/abertura_livros_fiscais.py +112 -229
- worker_automate_hub/tasks/jobs/descartes.py +91 -77
- worker_automate_hub/tasks/jobs/devolucao_produtos.py +1386 -0
- worker_automate_hub/tasks/jobs/entrada_de_notas_15.py +3 -46
- worker_automate_hub/tasks/jobs/entrada_de_notas_22.py +833 -0
- worker_automate_hub/tasks/jobs/entrada_de_notas_36.py +29 -9
- worker_automate_hub/tasks/jobs/entrada_de_notas_37.py +619 -0
- worker_automate_hub/tasks/jobs/entrada_de_notas_39.py +1 -1
- worker_automate_hub/tasks/jobs/entrada_de_notas_9.py +63 -16
- worker_automate_hub/tasks/jobs/extracao_dados_nielsen.py +504 -0
- worker_automate_hub/tasks/jobs/extracao_saldo_estoque.py +242 -108
- worker_automate_hub/tasks/jobs/extracao_saldo_estoque_fiscal.py +688 -0
- worker_automate_hub/tasks/jobs/fidc_gerar_nosso_numero.py +2 -2
- worker_automate_hub/tasks/jobs/fidc_remessa_cobranca_cnab240.py +25 -16
- worker_automate_hub/tasks/jobs/geracao_balancetes_filial.py +330 -0
- 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 +540 -326
- worker_automate_hub/tasks/jobs/sped_fiscal.py +8 -8
- worker_automate_hub/tasks/jobs/transferencias.py +52 -41
- worker_automate_hub/tasks/task_definitions.py +46 -1
- worker_automate_hub/tasks/task_executor.py +11 -0
- worker_automate_hub/utils/util.py +252 -215
- worker_automate_hub/utils/utils_nfe_entrada.py +1 -1
- worker_automate_hub/worker.py +1 -9
- {worker_automate_hub-0.5.749.dist-info → worker_automate_hub-0.5.912.dist-info}/METADATA +4 -2
- {worker_automate_hub-0.5.749.dist-info → worker_automate_hub-0.5.912.dist-info}/RECORD +36 -25
- {worker_automate_hub-0.5.749.dist-info → worker_automate_hub-0.5.912.dist-info}/WHEEL +1 -1
- {worker_automate_hub-0.5.749.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
|
+
|