198 lines
7.7 KiB
Python
198 lines
7.7 KiB
Python
# 合同 S11:挂合同正文。仅 YYC 传合并 PDF;提交前无文件则用占位 PDF。
|
||
#
|
||
# dry_run 且没有文件时跳过。
|
||
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
from pathlib import Path
|
||
|
||
from accept import evidence_shot
|
||
from errors import NodeError
|
||
|
||
from contract_common import (
|
||
BrowserAdapter,
|
||
ContractFlowContext,
|
||
_ACCEPT,
|
||
_close_blocking_dialogs,
|
||
)
|
||
|
||
|
||
# 决定要挂的合同正文路径。提交且无文件时用占位 PDF。
|
||
def _resolve_contract_body_path(ctx: ContractFlowContext) -> str:
|
||
path = (ctx.contract_document_path or "").strip()
|
||
if not path and ctx.source_type == "YYC":
|
||
path = (ctx.merged_pdf_path or "").strip()
|
||
if path:
|
||
return path
|
||
if ctx.submit_mode == "dry_run":
|
||
return ""
|
||
from pdf_utils import ensure_placeholder_contract_pdf
|
||
|
||
return ensure_placeholder_contract_pdf()
|
||
|
||
|
||
# 挂合同正文(S11)。dry_run 且没有文件则跳过。
|
||
def upload_contract_document_if_needed(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||
path = _resolve_contract_body_path(ctx)
|
||
if not path:
|
||
evidence_shot(browser, "contract-pdf-upload-skipped")
|
||
_ACCEPT.note("S11", "上传正文", {"skipped": "dry_run_no_file"}, ok=True)
|
||
return
|
||
try:
|
||
_upload_contract_body(browser, path)
|
||
except Exception as exc:
|
||
_ACCEPT.note("S11", "上传正文", {"attached": "no", "error": exc}, ok=False, raise_error=False)
|
||
raise
|
||
evidence_shot(browser, "contract-body-uploaded")
|
||
attached = _contract_body_attached(browser, path)
|
||
_ACCEPT.check(
|
||
"S11",
|
||
"上传正文",
|
||
[("attached", "yes" if attached else "no", "yes"), ("file", Path(path).name, Path(path).name)],
|
||
browser=browser,
|
||
screenshot_on_fail="contract-body-uploaded",
|
||
)
|
||
|
||
|
||
# 打开合同正文区,点添加合同,把本地文件塞进 file input。
|
||
def _upload_contract_body(browser: BrowserAdapter, file_path: str) -> None:
|
||
from pathlib import Path
|
||
|
||
if not Path(file_path).is_file():
|
||
raise NodeError(f"合同正文文件不存在:{file_path}")
|
||
|
||
_close_blocking_dialogs(browser)
|
||
_reveal_contract_body_section(browser)
|
||
evidence_shot(browser, "contract-body-tab")
|
||
if _contract_body_attached(browser, file_path):
|
||
evidence_shot(browser, "contract-body-uploaded")
|
||
return
|
||
|
||
_click_add_contract_body(browser)
|
||
time.sleep(0.8)
|
||
try:
|
||
browser.click_native(["text=打开本地初稿", "text=本地初稿", "text=上传本地文件"])
|
||
time.sleep(0.8)
|
||
except Exception:
|
||
pass
|
||
|
||
browser.upload_file_first(
|
||
[
|
||
'input[name="_apply_filemainOnline"]',
|
||
'input[name="__landray_filemainOnline"]',
|
||
'input[name*="filemainOnline"]',
|
||
'input[name*="fdMainOnline"]',
|
||
'.lui_dialog input[type="file"]',
|
||
'input[type="file"]',
|
||
],
|
||
file_path,
|
||
)
|
||
deadline = time.time() + 12
|
||
while time.time() < deadline:
|
||
if _contract_body_attached(browser, file_path) or _on_body_editor_tab(browser):
|
||
evidence_shot(browser, "contract-body-uploaded")
|
||
return
|
||
time.sleep(0.6)
|
||
evidence_shot(browser, "contract-body-uploaded", failed=True)
|
||
raise NodeError("合同正文未挂上:页面仍是「+添加合同」,提交会被系统拒绝")
|
||
|
||
|
||
# 滚到或点开「合同正文」区域,露出「+添加合同」。
|
||
def _reveal_contract_body_section(browser: BrowserAdapter) -> None:
|
||
browser.execute_script(
|
||
"""
|
||
const labelOf = (el) => (el.textContent || '').replace(/\\s+/g, '').trim();
|
||
const addBtn = Array.from(document.querySelectorAll('a, span, div, td')).find((el) => {
|
||
const text = labelOf(el);
|
||
return text === '+添加合同' || text === '添加合同';
|
||
});
|
||
if (addBtn) {
|
||
addBtn.scrollIntoView({block: 'center'});
|
||
return {ok: true, via: 'add'};
|
||
}
|
||
const hits = Array.from(document.querySelectorAll('div, li, span, a')).filter((el) => labelOf(el) === '合同正文');
|
||
for (const el of hits) {
|
||
const group = (el.parentElement && el.parentElement.parentElement) || el;
|
||
if (labelOf(group).indexOf('签约方信息') >= 0) {
|
||
(el.closest('li') || el).click();
|
||
return {ok: true, via: 'tabbar'};
|
||
}
|
||
}
|
||
return {ok: false};
|
||
"""
|
||
)
|
||
time.sleep(0.6)
|
||
|
||
|
||
# 点「+添加合同」打开本地初稿入口。
|
||
def _click_add_contract_body(browser: BrowserAdapter) -> None:
|
||
clicked = browser.execute_script(
|
||
"""
|
||
const labelOf = (el) => (el.textContent || '').replace(/\\s+/g, '').trim();
|
||
const hit = Array.from(document.querySelectorAll('a, span, div, td, button')).find((el) => {
|
||
const text = labelOf(el);
|
||
return (text === '+添加合同' || text === '添加合同') && el.children.length <= 1;
|
||
});
|
||
if (!hit) return {ok: false};
|
||
hit.scrollIntoView({block: 'center'});
|
||
hit.click();
|
||
return {ok: true};
|
||
"""
|
||
)
|
||
if isinstance(clicked, dict) and clicked.get("ok"):
|
||
return
|
||
browser.click_native(["text=+添加合同", "text=添加合同"])
|
||
|
||
|
||
# 看合同正文是否已经挂上:文件名、office iframe 或 file input 有文件。
|
||
def _contract_body_attached(browser: BrowserAdapter, file_path: str) -> bool:
|
||
from pathlib import Path
|
||
|
||
stem = Path(file_path).name
|
||
result = browser.execute_script(
|
||
"""
|
||
const wanted = String(arguments[0] || '');
|
||
const wantedStem = wanted.replace(/\\.[^.]+$/, '');
|
||
const fileInputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
||
if (fileInputs.some((el) => el.files && el.files.length > 0)) {
|
||
return {ok: true, via: 'input'};
|
||
}
|
||
const frames = Array.from(document.querySelectorAll('iframe, object, embed'));
|
||
const office = frames.find((el) => {
|
||
const hint = ((el.src || '') + ' ' + (el.id || '') + ' ' + (el.className || '')).toLowerCase();
|
||
const box = el.getBoundingClientRect();
|
||
return box.width > 80 && box.height > 80 && /office|ntko|wps|kingsoft|weboffice|filemain|online|pdf/.test(hint);
|
||
});
|
||
if (office) return {ok: true, via: 'iframe'};
|
||
const rows = Array.from(document.querySelectorAll('tr'));
|
||
for (const row of rows) {
|
||
const cells = Array.from(row.querySelectorAll('td, th'));
|
||
if (!cells.length) continue;
|
||
const head = (cells[0].innerText || '').replace(/\\s+/g, '').trim();
|
||
if (head !== '合同正文') continue;
|
||
const body = (row.innerText || '').replace(/\\s+/g, ' ').trim();
|
||
if (wanted && body.indexOf(wanted) >= 0) return {ok: true, via: 'name'};
|
||
if (wantedStem && body.indexOf(wantedStem) >= 0) return {ok: true, via: 'stem'};
|
||
if (/\\.(pdf|doc|docx|wps)/i.test(body)) return {ok: true, via: 'ext'};
|
||
if (body.indexOf('添加合同') >= 0 && body.indexOf('打开本地初稿') < 0) return {ok: false, via: 'empty'};
|
||
}
|
||
const page = (document.body.innerText || '');
|
||
if (wanted && page.indexOf(wanted) >= 0) return {ok: true, via: 'page'};
|
||
return {ok: false, via: 'missing'};
|
||
""",
|
||
stem,
|
||
)
|
||
return isinstance(result, dict) and bool(result.get("ok"))
|
||
|
||
|
||
# 底部「合同正文」在线编辑页:文件进编辑器后不一定回显文件名。
|
||
def _on_body_editor_tab(browser: BrowserAdapter) -> bool:
|
||
result = browser.execute_script(
|
||
"""
|
||
const page = (document.body.innerText || '').replace(/\\s+/g, '');
|
||
return page.indexOf('打开本地初稿') >= 0 && page.indexOf('添加合同') < 0;
|
||
"""
|
||
)
|
||
return result is True
|