326 lines
11 KiB
Python
326 lines
11 KiB
Python
import pytest
|
|
from accept import accept_log_path
|
|
from apa_browser_adapter import chrome_debug_port, chrome_session_flow, resolve_chrome_profile_dir
|
|
from errors import NodeError
|
|
from rebate_flow_steps import (
|
|
COMPENSATION_DEFAULT,
|
|
LOGIN,
|
|
PAY_MODE_CODE,
|
|
PAY_PARAM_DEFAULT,
|
|
SETTLE_CODE,
|
|
TAX_RATE_DEFAULT,
|
|
RebateFlowContext,
|
|
RebateImportLine,
|
|
build_import_draft,
|
|
build_import_rows,
|
|
fill_import_excel,
|
|
run_import_flow,
|
|
write_canonical_template,
|
|
)
|
|
|
|
|
|
class FakeBrowser:
|
|
def __init__(self, *, table_text="", body_text="", login_body="", import_result=None):
|
|
self.table_text = table_text
|
|
self.body_text = body_text
|
|
self.login_body = login_body or body_text
|
|
self.import_result = import_result or {"success": 1, "fail": 0, "reasons": []}
|
|
self.opened: list[str] = []
|
|
self.filled: list[tuple[list[str], str]] = []
|
|
self.selected: list[tuple[list[str], str]] = []
|
|
self.clicked: list[list[str]] = []
|
|
self.uploaded: list[str] = []
|
|
self.download_dir = None
|
|
self.script_count = 0
|
|
|
|
def open_url(self, url: str) -> None:
|
|
self.opened.append(url)
|
|
|
|
def fill_first(self, selectors: list[str], value: str) -> None:
|
|
self.filled.append((list(selectors), value))
|
|
|
|
def click_first(self, selectors: list[str]) -> None:
|
|
self.clicked.append(list(selectors))
|
|
|
|
def click_native(self, selectors: list[str]) -> None:
|
|
self.click_first(selectors)
|
|
|
|
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
|
|
haystack = f"{self.body_text}\n{self.table_text}\n{self.login_body}"
|
|
if text not in haystack:
|
|
raise TimeoutError(text)
|
|
|
|
def wait_for_element(self, selectors: list[str], timeout_seconds: int = 15) -> None:
|
|
return None
|
|
|
|
def element_exists(self, selectors: list[str]) -> bool:
|
|
return True
|
|
|
|
def read_body_text(self) -> str:
|
|
return self.body_text
|
|
|
|
def execute_script(self, script: str, *args):
|
|
self.script_count += 1
|
|
text = script or ""
|
|
if "SELECT_HEADER_ROW" in text:
|
|
return {"ok": True, "text": self.table_text}
|
|
if "GOODS_ADD_VISIBLE" in text:
|
|
return True
|
|
if "CLICK_GOODS_ADD" in text:
|
|
self.clicked.append(["+新增"])
|
|
return {"ok": True, "text": "+新增"}
|
|
if "CLOSE_HEADER_QUERY" in text:
|
|
return {"ok": True}
|
|
if "OPEN_GOODS_PICKER" in text:
|
|
return {"ok": True}
|
|
if "WAIT_GOODS_PICKER" in text:
|
|
return True
|
|
if "GOODS_ID_FIELD" in text:
|
|
return True
|
|
if "OPEN_GOODS_ID_QUERY" in text:
|
|
return {"ok": True, "already": True}
|
|
if "SAVE_DETAIL" in text:
|
|
self.clicked.append(["保存"])
|
|
return {"ok": True}
|
|
if "CONFIRM_SAVE_POPUP" in text:
|
|
return {"ok": True, "asked": False}
|
|
if "OPEN_DETAIL_SELECT" in text:
|
|
return {"ok": True}
|
|
if "FILTER_DETAIL_SELECT" in text:
|
|
return {"ok": True}
|
|
if "CLOSE_DETAIL_CARD" in text:
|
|
return {"ok": True}
|
|
if "CLOSE_HEADER_CARD" in text:
|
|
return {"ok": True}
|
|
if "GOODS_TABLE_ROWS" in text:
|
|
return ["G001 10 ZQD1"]
|
|
if "PICK_DETAIL_OPTION" in text:
|
|
return {"ok": True, "text": str(args[0] if args else "")}
|
|
if "CONFIRM_DETAIL_CARD" in text:
|
|
return {"ok": True, "via": "确认"}
|
|
if "DETAIL_TOAST" in text:
|
|
return {"texts": []}
|
|
if "DETAIL_FORM_ERRORS" in text:
|
|
return []
|
|
if "FILL_GOODS_ID" in text:
|
|
return {"ok": True, "label": "货品ID", "value": str(args[0] if args else "")}
|
|
if "PICK_GOODS_ROW" in text:
|
|
return {"ok": True}
|
|
if "SOURCE_CHOICE" in text:
|
|
return False
|
|
if "PICK_NTH_OVERLAY" in text:
|
|
return {"ok": True, "text": str(args[1] if len(args) > 1 else "")}
|
|
if "TYPE_DETAIL_FIELD" in text:
|
|
return True
|
|
if "FILL_DETAIL_CELLS" in text:
|
|
return {"qty": True, "price": True, "remark": True, "compensation": True, "tax": True, "payParam": True, "inbound": True}
|
|
if "DETAIL_CARD_PAGER" in text:
|
|
return "第 1 / 1 条记录"
|
|
if "OPEN_HEADER_ROW" in text:
|
|
return {"ok": True, "via": "row"}
|
|
if "READ_IMPORT_RESULT" in text:
|
|
return dict(self.import_result)
|
|
if "table-toolbar-btn__text" in text:
|
|
title = str(args[0] if args else "")
|
|
self.clicked.append([title])
|
|
if title in {"下载模板", "下载导入模板", "导出模板", "导入模板", "模板下载"}:
|
|
self._write_template()
|
|
return {"ok": True}
|
|
return self.table_text
|
|
|
|
def screenshot(self, name: str) -> str:
|
|
return name
|
|
|
|
def read_first(self, selectors: list[str], value_name: str = "") -> str:
|
|
return ""
|
|
|
|
def switch_to_frame(self, selectors: list[str]) -> None:
|
|
return None
|
|
|
|
def switch_to_default_content(self) -> None:
|
|
return None
|
|
|
|
def select_first(self, selectors: list[str], value: str) -> None:
|
|
self.selected.append((list(selectors), value))
|
|
|
|
def select_xform(self, name: str, value: str) -> None:
|
|
return None
|
|
|
|
def upload_file_first(self, selectors: list[str], file_path: str) -> None:
|
|
self.uploaded.append(file_path)
|
|
|
|
def set_download_dir(self, path: str) -> None:
|
|
from pathlib import Path
|
|
|
|
self.download_dir = Path(path)
|
|
self.download_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def _write_template(self) -> None:
|
|
if self.download_dir is None:
|
|
return
|
|
write_canonical_template(self.download_dir / "detail_template.xlsx")
|
|
|
|
|
|
def _ctx() -> RebateFlowContext:
|
|
return RebateFlowContext(rebate_url="http://example", username="u", password="p")
|
|
|
|
|
|
def _line() -> RebateImportLine:
|
|
return RebateImportLine(
|
|
goods_code="G001",
|
|
qty="10",
|
|
tax_price="6.70",
|
|
order_no="ZQD1",
|
|
source_line_id="SRC1",
|
|
)
|
|
|
|
|
|
def _draft():
|
|
return build_import_draft(lines=[_line()], today="2026-10-10")
|
|
|
|
|
|
def test_import_rows_follow_mapping():
|
|
row = build_import_rows([_line()], today="2026-10-10")[0]
|
|
assert row["货品编码"] == "G001"
|
|
assert row["数量"] == "10"
|
|
assert row["含税协议单价"] == "6.70"
|
|
assert row["税率"] == TAX_RATE_DEFAULT
|
|
assert row["关税税率"] == TAX_RATE_DEFAULT
|
|
assert row["承付模式"] == PAY_MODE_CODE
|
|
assert row["承付参数"] == PAY_PARAM_DEFAULT
|
|
assert row["采购结算方式"] == SETTLE_CODE
|
|
assert row["细单开始日期"] == "2026-10-10"
|
|
assert row["细单结束日期"] == "2026-12-31"
|
|
assert row["特定补偿预估值"] == COMPENSATION_DEFAULT
|
|
assert row["进货补偿预估值"] == COMPENSATION_DEFAULT
|
|
assert row["细单备注"] == "ZQD1"
|
|
assert row["货源细单ID"] == "SRC1"
|
|
|
|
|
|
def test_import_rows_search_by_goods_id():
|
|
line = RebateImportLine(
|
|
goods_code="FFTMMHTTP",
|
|
qty="10",
|
|
tax_price="6.70",
|
|
order_no="ZQD1",
|
|
goods_id="4000018689",
|
|
)
|
|
row = build_import_rows([line], today="2026-10-10")[0]
|
|
assert row["货品编码"] == "4000018689"
|
|
|
|
|
|
def test_import_line_from_mapping_prefers_goods_id():
|
|
from rebate_flow_steps import _line_from_mapping
|
|
|
|
line = _line_from_mapping(
|
|
{
|
|
"goodsId": "4000018689",
|
|
"goodsCode": "FFTMMHTTP",
|
|
"qty": "10",
|
|
"taxPrice": "6.70",
|
|
"orderNo": "ZQD1",
|
|
}
|
|
)
|
|
row = build_import_rows([line], today="2026-10-10")[0]
|
|
assert line.goods_id == "4000018689"
|
|
assert row["货品编码"] == "4000018689"
|
|
|
|
|
|
def test_import_draft_requires_oms_order_no():
|
|
with pytest.raises(NodeError):
|
|
build_import_draft(
|
|
lines=[RebateImportLine(goods_code="G001", qty="10", tax_price="6.70", order_no="")]
|
|
)
|
|
|
|
|
|
def test_fill_excel_writes_goods_id_into_goods_id_column(tmp_path):
|
|
from openpyxl import Workbook, load_workbook
|
|
|
|
template = tmp_path / "t.xlsx"
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
ws["A1"] = "货品ID"
|
|
ws["B1"] = "数量"
|
|
wb.save(template)
|
|
line = RebateImportLine(
|
|
goods_code="FFTMMHTTP",
|
|
qty="10",
|
|
tax_price="6.70",
|
|
order_no="ZQD1",
|
|
goods_id="4000018689",
|
|
)
|
|
out = fill_import_excel(
|
|
template,
|
|
build_import_rows([line], today="2026-10-10"),
|
|
tmp_path / "out.xlsx",
|
|
)
|
|
ws = load_workbook(out).active
|
|
assert str(ws["A2"].value) == "4000018689"
|
|
|
|
|
|
def test_fill_excel_writes_remark_and_zero_compensation(tmp_path):
|
|
from openpyxl import load_workbook
|
|
|
|
template = tmp_path / "t.xlsx"
|
|
write_canonical_template(template)
|
|
out = fill_import_excel(
|
|
template,
|
|
build_import_rows([_line()], today="2026-10-10"),
|
|
tmp_path / "out.xlsx",
|
|
)
|
|
ws = load_workbook(out).active
|
|
data = {cell.value: value.value for cell, value in zip(ws[1], ws[2])}
|
|
assert str(data["细单备注"]) == "ZQD1"
|
|
assert str(data["特定补偿预估值"]) == "0"
|
|
assert str(data["进货补偿预估值"]) == "0"
|
|
assert str(data["承付模式"]) == "11"
|
|
|
|
|
|
def test_import_flow_writes_accept_with_success_fail(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
|
|
monkeypatch.setattr("accept.SCREENSHOT_ROOT", tmp_path)
|
|
browser = FakeBrowser(
|
|
table_text="HT1 重庆亿昊",
|
|
body_text="采购协议管理 合同编号 查询 返利合约 承付模式 保存 导入导出 成功 1 条 失败 0 条",
|
|
login_body="采购协议管理",
|
|
import_result={"success": 1, "fail": 0, "reasons": []},
|
|
)
|
|
result = run_import_flow(browser, _ctx(), "HT1", _draft(), download_dir=tmp_path / "dl")
|
|
assert result.success_count == 1
|
|
assert result.fail_count == 0
|
|
assert result.parsed is True
|
|
assert browser.uploaded == []
|
|
text = accept_log_path("REBATE_IMPORT").read_text(encoding="utf-8")
|
|
assert "ACCEPT SUMMARY REBATE_IMPORT 8/8 ok" in text
|
|
assert "ACCEPT S06 ok 选中总单" in text
|
|
assert "ACCEPT S07 ok 新增细单" in text
|
|
assert "goodsId=G001" in text
|
|
assert "ACCEPT S08 ok 保存细单" in text
|
|
assert "1条货品已保存" in text
|
|
assert "ACCEPT S10 ok 读导入结果" in text
|
|
assert "remark=ZQD1" in text
|
|
assert "compensation=0" in text
|
|
assert "success=1" in text
|
|
assert "fail=0" in text
|
|
|
|
|
|
def test_import_reuses_logged_in_session(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
|
|
monkeypatch.setattr("accept.SCREENSHOT_ROOT", tmp_path)
|
|
browser = FakeBrowser(
|
|
table_text="HT1 重庆亿昊",
|
|
body_text="采购协议管理 待审批 合同编号 查询 返利合约 承付模式 保存 导入导出 成功 1 条 失败 0 条",
|
|
login_body="采购协议管理",
|
|
import_result={"success": 1, "fail": 0, "reasons": []},
|
|
)
|
|
run_import_flow(browser, _ctx(), "HT1", _draft(), download_dir=tmp_path / "dl")
|
|
login_fills = [value for selectors, value in browser.filled if selectors == LOGIN["user"]]
|
|
assert login_fills == []
|
|
assert "u" not in [value for _, value in browser.filled]
|
|
|
|
|
|
def test_import_chrome_session_reuses_find():
|
|
assert chrome_session_flow("REBATE_IMPORT") == "REBATE_FIND"
|
|
assert resolve_chrome_profile_dir("REBATE_IMPORT") == resolve_chrome_profile_dir("REBATE_FIND")
|
|
assert chrome_debug_port("REBATE_IMPORT") == chrome_debug_port("REBATE_FIND")
|