AutoRpa/rpa_rebate/tests/test_import.py

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")