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