AutoRpa/rpa_rebate/tests/test_import.py

408 lines
14 KiB
Python
Raw 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.

# 返利细单新增:按货品ID搜、已在表则跳过、ACCEPT 成功失败条数。
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,
add_detail_lines_by_goods_id,
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,
goods_rows=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.goods_rows = list(goods_rows) if goods_rows is not None else []
self.last_goods_id = ""
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
# 记下打开过的 URL。
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))
# 记下原生点击,行为同 click_first。
def click_native(self, selectors: list[str]) -> None:
self.click_first(selectors)
# 假正文里没有这段字就抛 TimeoutError。
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 list(self.goods_rows)
if "FILL_GOODS_ID" in text:
self.last_goods_id = str(args[0] if args else "")
return {"ok": True, "label": "货品ID", "value": self.last_goods_id}
if "CONFIRM_DETAIL_CARD" in text:
gid = self.last_goods_id
if gid and not any(gid in row for row in self.goods_rows):
self.goods_rows.append(gid)
return {"ok": True, "via": "确认"}
if "PICK_DETAIL_OPTION" in text:
return {"ok": True, "text": str(args[0] if args else "")}
if "DETAIL_TOAST" in text:
return {"texts": []}
if "DETAIL_FORM_ERRORS" in text:
return []
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 ""
# 假切入 iframe,什么都不做。
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))
# 记下 xform 选择。
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)
# 在下载目录写一份空 xlsx 当模板。
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")
# 构造一条带货品ID和订单号的细单。
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")
# Excel 行字典走现行列:备注=订单号、补偿 0、承付参数 25。
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"
# 货品编码列用 goodsId,不用操作码。
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"
# 同时有货品编码和 goodsId 时搜 goodsId。
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="")]
)
# 填模板时货品编码列写入 goodsId。
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"
# 填模板时备注是订单号,补偿是 0。
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"
# 细单流程写 ACCEPT,带成功失败条数和货品ID。
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]
# 细单和查号共用同一 Chrome 会话和端口。
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")
# 按货品ID构造一条细单,给查重用例用。
def _goods_line(goods_id: str, *, order_no: str = "ZQD1") -> RebateImportLine:
return RebateImportLine(
goods_code=goods_id,
qty="10",
tax_price="6.70",
order_no=order_no,
goods_id=goods_id,
)
# 货品表已有该 ID 则跳过新增。
def test_add_detail_skips_goods_already_in_table():
browser = FakeBrowser(goods_rows=["G001 10 ZQD1"])
added = add_detail_lines_by_goods_id(browser, [_goods_line("G001")])
assert added == 1
assert ["+新增"] not in browser.clicked
assert ["保存"] not in browser.clicked
# 同批已有的跳过、没有的仍新增。
def test_add_detail_skips_existing_and_adds_missing():
browser = FakeBrowser(goods_rows=["G001 10 ZQD1"])
added = add_detail_lines_by_goods_id(
browser,
[_goods_line("G001"), _goods_line("G002", order_no="ZQD2")],
)
assert added == 2
assert browser.clicked.count(["+新增"]) == 1
assert any("G002" in row for row in browser.goods_rows)
assert ["保存"] in browser.clicked