AutoRpa/rpa_contract/contract_submit.py

364 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 合同 S12:暂存后提交。成功后不要等详情页。
#
# 测试环境直接提交会落到「该文档不存在」,所以先点暂存再点提交。
# dry_run 不点提交,返回 False。
from __future__ import annotations
import time
from accept import evidence_shot
from contract_common import (
EDIT_PAGE_URL,
BrowserAdapter,
ContractFlowContext,
_ACCEPT,
_click_toolbar_button,
_close_blocking_dialogs,
_current_url,
_lui_toolbar_button_exists,
_read_document_id,
_wait_lui_toolbar_button,
)
_ERROR_MARKS = (
"不能为空",
"请填写",
"校验失败",
"提交失败",
"必填项未填",
"该文档不存在",
"操作失败",
)
_SUCCESS_MARKS = ("您的操作已成功", "操作已成功", "提交成功", "流程已提交", "成功提交")
_CONFIRM_MARKS = ("确定提交", "是否提交", "确认提交", "是否确定", "确定要提交")
# 暂存后提交(S12)。dry_run 不点提交。成功后不要等详情页。
def submit_contract_for_approval(
browser: BrowserAdapter, ctx: ContractFlowContext, *, fd_id: str = ""
) -> bool:
if ctx.submit_mode == "dry_run":
evidence_shot(browser, "contract-dry-run-ready")
_ACCEPT.note("S12", "提交", {"mode": "dry_run", "submitted": "no"}, ok=True)
return False
_close_blocking_dialogs(browser)
fd_id = (fd_id or "").strip() or _read_document_id(browser)
print(f"S12 fdId={fd_id or '-'}", flush=True)
# 测试环境直接提交会落到「该文档不存在」:先暂存落库,再提交。合同号从详情 #_xform_docNumber 读。
try:
saved_click = _click_toolbar_button(browser, "暂存")
except Exception as exc:
print(f"暂存未点到:{exc}", flush=True)
saved_click = False
save_kind, save_text = _wait_submit_outcome(browser, timeout_seconds=15)
if save_kind == "error":
evidence_shot(browser, "contract-submitted", failed=True)
_ACCEPT.note(
"S12",
"提交",
{"submitted": "no", "error": save_text, "at": "暂存", "clickedSave": "yes" if saved_click else "no"},
ok=False,
raise_error=False,
)
return False
if save_kind != "success":
print("暂存后仍在表单页,继续点提交。", flush=True)
fd_id = fd_id or _read_document_id(browser)
if not _lui_toolbar_button_exists(browser, "提交"):
if not fd_id:
evidence_shot(browser, "contract-submitted", failed=True)
_ACCEPT.note(
"S12",
"提交",
{"submitted": "no", "error": "暂存后离开表单且无 fdId,无法再点提交", "clicked": "no"},
ok=False,
raise_error=False,
)
return False
print(f"暂存后打开编辑页 fdId={fd_id}", flush=True)
browser.open_url(EDIT_PAGE_URL.format(fd_id=fd_id))
if not _wait_lui_toolbar_button(browser, "提交", timeout_seconds=12):
evidence_shot(browser, "contract-submitted", failed=True)
_ACCEPT.note(
"S12",
"提交",
{"submitted": "no", "error": "编辑页未见提交按钮 lui_widget_btn_txt", "clicked": "no", "fdId": fd_id},
ok=False,
raise_error=False,
)
return False
try:
clicked = _click_toolbar_button(browser, "提交")
except Exception as exc:
evidence_shot(browser, "contract-submitted", failed=True)
_ACCEPT.note("S12", "提交", {"submitted": "no", "error": str(exc)[:120], "clicked": "no"}, ok=False, raise_error=False)
return False
if not clicked:
evidence_shot(browser, "contract-submitted", failed=True)
_ACCEPT.note("S12", "提交", {"submitted": "no", "error": "未点到提交按钮 lui_widget_btn_txt", "clicked": "no"}, ok=False, raise_error=False)
return False
evidence_shot(browser, "contract-after-submit-click")
kind, dialog = _wait_submit_outcome(browser, timeout_seconds=20, trust_body_success=False)
if kind == "error":
evidence_shot(browser, "contract-submitted", failed=True)
_ACCEPT.note(
"S12",
"提交",
{"submitted": "no", "clicked": "yes", "error": dialog},
ok=False,
raise_error=False,
)
return False
print(f"提交已点 kind={kind} dialog={dialog[:80]!r},S13 从详情读合同号。", flush=True)
_ACCEPT.note(
"S12",
"提交",
{
"submitted": "yes",
"clicked": "yes",
"via": "暂存后提交",
"signal": kind,
"dialog": dialog[:80] or "(none)",
},
ok=True,
)
return True
# 从页面正文抽出提交失败原因。没有则返回空字符串。
def _submit_error_from_body(body: str) -> str:
text = body or ""
if "该文档不存在" in text:
return "该文档不存在"
if "操作失败" in text and "您的请求已提交" in text:
return "操作失败"
return ""
# 把弹窗文案收成 error / success / confirm / none。
def _classify_dialog_text(text: str) -> str:
blob = text or ""
for mark in _ERROR_MARKS:
if mark in blob:
return "error"
for mark in _SUCCESS_MARKS:
if mark in blob:
return "success"
for mark in _CONFIRM_MARKS:
if mark in blob:
return "confirm"
return "none"
# 等到提交弹窗给出成功或失败信号。超时返回 none。
def _wait_submit_outcome(
browser: BrowserAdapter,
timeout_seconds: float,
*,
trust_body_success: bool = True,
) -> tuple[str, str]:
deadline = time.time() + timeout_seconds
last_dialog = ""
saw_confirm = False
while time.time() < deadline:
kind, text, confirm = _read_submit_signal(
browser,
trust_body_success=trust_body_success or saw_confirm,
)
if confirm:
saw_confirm = True
if text:
last_dialog = text
if kind == "error":
return "error", text
if kind == "success":
return "success", text
time.sleep(0.4)
kind, text, _confirm = _read_submit_signal(
browser,
trust_body_success=trust_body_success or saw_confirm,
)
if kind in ("error", "success"):
return kind, text
return "none", text or last_dialog
# 读一次原生 alert 和 LUI 弹窗,返回 kind、文案、是否点过确认。
def _read_submit_signal(
browser: BrowserAdapter,
*,
trust_body_success: bool = True,
) -> tuple[str, str, bool]:
alert_text = _accept_native_alert(browser)
if alert_text:
kind = _classify_dialog_text(alert_text)
if kind == "none":
kind = "confirm"
print(f"S12 native alert kind={kind} text={alert_text[:80]!r}", flush=True)
if kind == "error":
return "error", alert_text, False
if kind == "success":
return "success", alert_text, False
if kind == "confirm":
return _finish_submit_signal(browser, alert_text, True, trust_body_success)
snapshot = _handle_submit_dialogs(browser)
dialog_text = str(snapshot.get("text") or "").strip()
kind = _classify_dialog_text(dialog_text) if dialog_text else "none"
if kind == "none":
kind = str(snapshot.get("kind") or "none")
if kind == "error":
return "error", dialog_text or "弹窗报错", False
if kind == "success":
return "success", dialog_text or "ok", False
return _finish_submit_signal(
browser,
dialog_text,
kind == "confirm" or bool(snapshot.get("clicked")),
trust_body_success,
)
# 弹窗之后再看正文和 URL,把提交结果收成 error / success / none。
def _finish_submit_signal(
browser: BrowserAdapter,
dialog_text: str,
saw_confirm: bool,
trust_body_success: bool,
) -> tuple[str, str, bool]:
body_error = _submit_error_from_body(browser.read_body_text() or "")
if body_error:
return "error", body_error, saw_confirm
if _submit_view_page(browser):
return "success", dialog_text or "view", saw_confirm
if (trust_body_success or saw_confirm) and _submit_success_text(browser):
return "success", dialog_text or "ok", saw_confirm
return "none", dialog_text, saw_confirm
# 有原生 alert 就点确定并返回文案。没有则返回空字符串。
def _accept_native_alert(browser: BrowserAdapter) -> str:
accept = getattr(browser, "accept_alert", None)
if not callable(accept):
return ""
try:
return str(accept() or "").strip()
except Exception:
return ""
# 当前 URL 是否已经落到合同详情 view 页。
def _submit_view_page(browser: BrowserAdapter) -> bool:
url = _current_url(browser)
return "method=view" in url and "kmAgreementApply" in url
# 页面正文是否出现提交成功标记,且没有失败文案。
def _submit_success_text(browser: BrowserAdapter) -> bool:
body = browser.read_body_text() or ""
if _submit_error_from_body(body):
return False
return any(mark in body for mark in _SUCCESS_MARKS)
# 提交是否已经成功:成功文案或已到详情页,且正文没有失败。
def _submit_succeeded(browser: BrowserAdapter) -> bool:
if _submit_error_from_body(browser.read_body_text() or ""):
return False
return _submit_success_text(browser) or _submit_view_page(browser)
# 读当前弹窗/正文里的校验失败文案。没有则返回空字符串。
def _submit_validation_error(browser: BrowserAdapter) -> str:
kind, text, _confirm = _read_submit_signal(browser, trust_body_success=True)
if kind == "error":
return text
return ""
# 扫描 LUI 弹窗:报错原样返回,确认/成功则点确定。
def _handle_submit_dialogs(browser: BrowserAdapter) -> dict:
raw = browser.execute_script(
"""
const errorMarks = ['不能为空','请填写','校验失败','提交失败','必填项未填','该文档不存在','操作失败'];
const successMarks = ['您的操作已成功','操作已成功','提交成功','流程已提交','成功提交'];
const confirmMarks = ['确定提交','是否提交','确认提交','是否确定','确定要提交'];
function classify(text) {
if (errorMarks.some((m) => text.indexOf(m) >= 0)) return 'error';
if (successMarks.some((m) => text.indexOf(m) >= 0)) return 'success';
if (confirmMarks.some((m) => text.indexOf(m) >= 0)) return 'confirm';
return 'none';
}
function dialogText(doc) {
const roots = Array.from(doc.querySelectorAll(
'.lui_dialog, .lui_message, .lui_alert, .lui_dialog_content, .prompt_frame, .lui_dialog_common'
));
return roots.map((el) => (el.innerText || '').trim()).filter(Boolean).join('\\n');
}
function clickLabel(doc, labels) {
const nodes = Array.from(doc.querySelectorAll('div, a, button, span, input'));
for (const label of labels) {
const hit = nodes.find((el) => {
const t = (el.textContent || el.value || '').trim();
return t === label && (!el.children || el.children.length === 0);
});
if (hit) { hit.click(); return label; }
}
return '';
}
function walk(win) {
let found = {kind: 'none', text: '', clicked: ''};
try {
const text = dialogText(win.document);
const kind = classify(text);
if (kind === 'error') return {kind: kind, text: text.slice(0, 200), clicked: ''};
if (kind === 'confirm') {
const clicked = clickLabel(win.document, ['确定', '确认', '是', 'OK']);
return {kind: kind, text: text.slice(0, 200), clicked: clicked};
}
if (kind === 'success') {
const clicked = clickLabel(win.document, ['确定', '关闭', '确认']);
return {kind: kind, text: text.slice(0, 200), clicked: clicked};
}
if (text) {
const clicked = clickLabel(win.document, ['确定', '确认']);
if (clicked) return {kind: 'confirm', text: text.slice(0, 200), clicked: clicked};
}
} catch (e) {}
for (let i = 0; i < win.frames.length; i++) {
try {
const inner = walk(win.frames[i]);
if (inner.kind !== 'none' || inner.clicked) return inner;
} catch (e) {}
}
return found;
}
return walk(window);
"""
)
if not isinstance(raw, dict):
return {"kind": "none", "text": "", "clicked": ""}
return {
"kind": str(raw.get("kind") or "none"),
"text": str(raw.get("text") or "").strip(),
"clicked": str(raw.get("clicked") or ""),
}
# 等到提交确认弹窗消失或被点掉。
def _wait_lui_dialog_gone(browser: BrowserAdapter, timeout_seconds: float = 8) -> None:
deadline = time.time() + timeout_seconds
while time.time() < deadline:
snapshot = _handle_submit_dialogs(browser)
kind = str(snapshot.get("kind") or "none")
text = str(snapshot.get("text") or "").strip()
if kind == "confirm":
return
if kind in ("none", "") and not text:
return
_close_blocking_dialogs(browser)
time.sleep(0.4)
_close_blocking_dialogs(browser)