完成差旅发票录入流程
This commit is contained in:
426
src/bot.py
426
src/bot.py
@@ -4,11 +4,10 @@
|
||||
使用 Playwright 操作财务报销系统,自动完成登录、填单、上传附件等操作。
|
||||
|
||||
对外接口:
|
||||
load_invoice_data(csv_path, config) -> list[dict] 从 CSV 加载并补全默认值
|
||||
run_bot(config, invoices) 启动浏览器并执行填报流程
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -32,59 +31,22 @@ def _format_date(date_str: str) -> str:
|
||||
return date_str
|
||||
|
||||
|
||||
def _safe_float(value: str | None, default: float = 0.0) -> float:
|
||||
"""安全转换为浮点数,空值或转换失败时返回默认值"""
|
||||
if value is None or str(value).strip() == "":
|
||||
return default
|
||||
try:
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CSV 数据加载
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_invoice_data(csv_path: str, config: dict[str, str | Path]) -> list[dict[str, str | float | Any | Path]]:
|
||||
"""从 CSV 加载发票数据,自动补全空白字段的默认值
|
||||
|
||||
CSV 以支付记录为主键,每行包含 _invoices_json 字段(JSON 序列化的发票列表)。
|
||||
本函数还原为发票级别的数据列表。
|
||||
"""
|
||||
import json
|
||||
|
||||
invoices = []
|
||||
with open(csv_path, encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
invoices_json = row.get("_invoices_json", "")
|
||||
if not invoices_json:
|
||||
continue
|
||||
try:
|
||||
inv_list = json.loads(invoices_json)
|
||||
for inv in inv_list:
|
||||
invoices.append(
|
||||
{
|
||||
"seq": row.get("序号", ""),
|
||||
"invoice_no": inv.get("发票号码", ""),
|
||||
"invoice_date": inv.get("开票日期", ""),
|
||||
"item_name": inv.get("项目名称", ""),
|
||||
"spec_model": inv.get("规格型号", ""),
|
||||
"total_amount": _safe_float(inv.get("价税合计")),
|
||||
"seller_name": inv.get("销售方名称", ""),
|
||||
"person_name": inv.get("人员姓名") or config.get("default_name", ""),
|
||||
"card_date": _format_date(row.get("刷卡日期") or ""),
|
||||
"card_no": row.get("公务卡号") or config.get("default_card_no", ""),
|
||||
"card_amount": _safe_float(row.get("刷卡金额")),
|
||||
"remark": row.get("备注") or "",
|
||||
"person_id": row.get("工号") or config.get("default_person_id", ""),
|
||||
}
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
log.warning(f"无法解析发票 JSON: {invoices_json[:50]}...")
|
||||
return invoices
|
||||
def _classify_invoice_batch(
|
||||
invoices: list[dict[str, str]],
|
||||
) -> dict[str, list[dict[str, str]]]:
|
||||
"""按发票类型分组"""
|
||||
travel: list[dict[str, str]] = []
|
||||
general: list[dict[str, str]] = []
|
||||
application: list[dict[str, str]] = []
|
||||
for inv in invoices:
|
||||
inv_type = inv.get("invoice_type", "general")
|
||||
if inv_type == "application":
|
||||
application.append(inv)
|
||||
elif inv_type in ("train", "hotel"):
|
||||
travel.append(inv)
|
||||
else:
|
||||
general.append(inv)
|
||||
return {"travel": travel, "general": general, "application": application}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -95,7 +57,7 @@ def load_invoice_data(csv_path: str, config: dict[str, str | Path]) -> list[dict
|
||||
class ReimburseBot:
|
||||
"""财务报销自动化机器人"""
|
||||
|
||||
def __init__(self, config: dict[str, Any], headless: bool = False):
|
||||
def __init__(self, config: dict[str, Any], headless: bool = False, work_dir: Path | None = None):
|
||||
self.config = config
|
||||
self.headless = headless
|
||||
self.work_dir: Path | None = None
|
||||
@@ -111,7 +73,7 @@ class ReimburseBot:
|
||||
def launch(self) -> None:
|
||||
"""启动浏览器"""
|
||||
self.browser = self.pw.chromium.launch(headless=self.headless)
|
||||
self.context = self.browser.new_context(viewport={"width": 1920, "height": 1080})
|
||||
self.context = self.browser.new_context(viewport={"width": 1360, "height": 768})
|
||||
self.page = self.context.new_page()
|
||||
self.page.set_default_timeout(30000)
|
||||
|
||||
@@ -156,7 +118,7 @@ class ReimburseBot:
|
||||
self._screenshot("portal_timeout")
|
||||
raise TimeoutError("登录超时,未跳转到信息门户")
|
||||
|
||||
def navigate_to_reimburse(self) -> None:
|
||||
def navigate_to_reimburse(self, page_key: str = "reimburse_page") -> None:
|
||||
"""从统一信息平台进入报销系统"""
|
||||
log.info("进入报销系统...")
|
||||
self._wait_for('text="快捷入口"', timeout=5000)
|
||||
@@ -197,17 +159,17 @@ class ReimburseBot:
|
||||
pass
|
||||
|
||||
self._wait_for('text="报销录入"', timeout=5000)
|
||||
common_url = self.config["reimburse_url"] + self.config["reimburse_page"]
|
||||
common_url = self.config["reimburse_url"] + self.config[page_key]
|
||||
self.page.goto(common_url, wait_until="domcontentloaded", timeout=15000)
|
||||
self._wait_for('text="单据状态:"', timeout=5000)
|
||||
|
||||
def open_reimburse_menu(self) -> None:
|
||||
"""点击「新增」创建新报销单"""
|
||||
log.info("创建新报销单...")
|
||||
def create_new_form(self) -> None:
|
||||
"""点击「新增」创建新单据"""
|
||||
log.info("创建新单据...")
|
||||
self.page.wait_for_timeout(2000)
|
||||
|
||||
try:
|
||||
self.page.click('button:has-text("新增")', timeout=5000)
|
||||
self.page.click("#insert", timeout=5000)
|
||||
except Exception:
|
||||
try:
|
||||
self.page.click("text=新增", timeout=3000)
|
||||
@@ -218,6 +180,31 @@ class ReimburseBot:
|
||||
self.page.wait_for_timeout(3000)
|
||||
self._screenshot("after_add_click")
|
||||
|
||||
def fill_travel_info(self, basic_info: dict[str, Any]) -> None:
|
||||
"""填写差旅报销基本信息"""
|
||||
log.info("填写差旅报销信息...")
|
||||
|
||||
try:
|
||||
self.page.fill("#CAUSE", basic_info.get("travel_purpose", ""))
|
||||
self.page.fill("#SITE", basic_info.get("travel_location", ""))
|
||||
|
||||
self.page.click("#PROJECTCODE", timeout=10000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
self.page.wait_for_selector("#promodal .fixed-table-body tbody tr", timeout=10000)
|
||||
first_row = self.page.query_selector("#promodal .fixed-table-body tbody tr")
|
||||
if first_row:
|
||||
first_row.click()
|
||||
self.page.wait_for_timeout(1000)
|
||||
|
||||
self.page.fill("#THEKSRQ", _format_date(basic_info.get("start_date", "")))
|
||||
self.page.fill("#THEJSRQ", _format_date(basic_info.get("end_date", "")))
|
||||
self.page.click("#saveAndNext", timeout=5000)
|
||||
self.page.wait_for_timeout(2000)
|
||||
self._screenshot("travel_basic_done")
|
||||
except Exception:
|
||||
log.error("填写基本信息失败")
|
||||
self._screenshot("travel_basic_error")
|
||||
|
||||
def fill_basic_info(self, description: str = "元器件采购报销") -> None:
|
||||
"""填写基本信息"""
|
||||
log.info("填写基本信息...")
|
||||
@@ -254,6 +241,96 @@ class ReimburseBot:
|
||||
|
||||
self._screenshot("step3_done")
|
||||
|
||||
def add_travel_items(self, travel_items: dict[str, Any]) -> None:
|
||||
"""录入差旅报销明细"""
|
||||
vehicle_map = {
|
||||
"火车": "01",
|
||||
"汽车": "02",
|
||||
"轮船": "03",
|
||||
"自带车": "04",
|
||||
"公务车": "05",
|
||||
"飞机": "06",
|
||||
"租车": "07",
|
||||
"自驾车": "08",
|
||||
}
|
||||
try:
|
||||
traffic_info = travel_items.get("transport_fee") or []
|
||||
for item in traffic_info:
|
||||
self.page.click("#insertDetail", timeout=5000)
|
||||
self._wait_for('text="增加明细"', timeout=5000)
|
||||
self.page.select_option("#cost", "1")
|
||||
self.page.wait_for_timeout(500)
|
||||
vehicle = item.get("vehicle_type", "")
|
||||
if vehicle in vehicle_map:
|
||||
self.page.select_option("#jtgj", vehicle_map[vehicle])
|
||||
|
||||
self.page.fill("#ksdd", item.get("departure_place", ""))
|
||||
self.page.fill("#jsdd", item.get("arrival_place", ""))
|
||||
self.page.fill(
|
||||
'#t1 input[name="expenPwTraveldetail.MONEY"]',
|
||||
str(item.get("amount", "")),
|
||||
)
|
||||
self.page.fill(
|
||||
'#t1 input[name="expenPwTraveldetail.HOWBILL"]',
|
||||
str(item.get("bill_count", "")),
|
||||
)
|
||||
self.page.fill(
|
||||
'#t1 input[name="expenPwTraveldetail.SMARK"]',
|
||||
str(item.get("remark", "")),
|
||||
)
|
||||
self.page.click("#detailAdd", timeout=3000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
|
||||
hotel_info = travel_items.get("hotel_fee") or []
|
||||
for item in hotel_info:
|
||||
self.page.click("#insertDetail", timeout=5000)
|
||||
self._wait_for('text="增加明细"', timeout=5000)
|
||||
self.page.select_option("#cost", "2")
|
||||
self.page.wait_for_timeout(500)
|
||||
self.page.fill("#ksrq2", _format_date(str(item.get("checkin_date", ""))))
|
||||
self.page.fill("#jsrq2", _format_date(str(item.get("checkout_date", ""))))
|
||||
self.page.fill("#ts2", str(item.get("days", "")))
|
||||
self.page.fill("#rs2", str(item.get("person_count", "")))
|
||||
self.page.fill(
|
||||
'#t2 input[name="expenPwTraveldetail.FPMONEY"]',
|
||||
str(item.get("invoice_amount", "")),
|
||||
)
|
||||
self.page.fill(
|
||||
'#t2 input[name="expenPwTraveldetail.MONEY"]',
|
||||
str(item.get("reimburse_amount", "")),
|
||||
)
|
||||
self.page.fill(
|
||||
'#t2 input[name="expenPwTraveldetail.SMARK"]',
|
||||
str(item.get("remark", "")),
|
||||
)
|
||||
self.page.click("#detailAdd", timeout=3000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
|
||||
conference_info = travel_items.get("conference_fee") or []
|
||||
for item in conference_info:
|
||||
self.page.click("#insertDetail", timeout=5000)
|
||||
self._wait_for('text="增加明细"', timeout=5000)
|
||||
self.page.select_option("#cost", "3")
|
||||
self.page.wait_for_timeout(500)
|
||||
self.page.fill(
|
||||
'#t3 input[name="expenPwTraveldetail.HOWBILL"]',
|
||||
str(item.get("bill_count", "")),
|
||||
)
|
||||
self.page.fill(
|
||||
'#t3 input[name="expenPwTraveldetail.MONEY"]',
|
||||
str(item.get("amount", "")),
|
||||
)
|
||||
self.page.fill(
|
||||
'#t3 input[name="expenPwTraveldetail.SMARK"]',
|
||||
str(item.get("remark", "")),
|
||||
)
|
||||
self.page.click("#detailAdd", timeout=3000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
except Exception as e:
|
||||
log.error(f"录入总明细失败: {e}")
|
||||
self._screenshot("item_total_error")
|
||||
raise
|
||||
|
||||
def add_reimburse_items(self, invoices: list[dict[str, Any]]) -> None:
|
||||
"""录入报销明细(一条总明细)"""
|
||||
card_amount = sum(inv["card_amount"] for inv in invoices)
|
||||
@@ -261,7 +338,6 @@ class ReimburseBot:
|
||||
|
||||
try:
|
||||
self.page.click("#insertDetail", timeout=5000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
self._wait_for('text="经济事项名称"', timeout=5000)
|
||||
self.page.click("#economicscode2")
|
||||
self.page.wait_for_timeout(1000)
|
||||
@@ -286,6 +362,33 @@ class ReimburseBot:
|
||||
self._screenshot("item_total_error")
|
||||
raise
|
||||
|
||||
def fill_travel_payment(self, payment_info: list[dict[str, Any]]) -> None:
|
||||
"""录入差旅支付信息"""
|
||||
log.info("录入差旅支付信息...")
|
||||
try:
|
||||
self.page.click('text="下一步(支付方式)"', timeout=5000)
|
||||
self._wait_for('text="下一步(补助清单)"', timeout=5000)
|
||||
|
||||
for info in payment_info:
|
||||
self.page.click("#insertPay", timeout=5000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
self.page.fill("#personid2", self.config["default_name"])
|
||||
self.page.fill("#accountname2", self.config["default_person_id"])
|
||||
self.page.fill("#receiptdate2", _format_date(info["card_date"]))
|
||||
self.page.fill("#localaccount2", self.config["default_card_no"])
|
||||
self.page.fill("#receiptmoney2", str(info["card_amount"]))
|
||||
self.page.fill("#money2", str(info["card_amount"]))
|
||||
self.page.fill("#merchant2", info.get("merchant", ""))
|
||||
self.page.fill("#smark2", info.get("remark", ""))
|
||||
self.page.click("#payAdd", timeout=3000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
except Exception as e:
|
||||
log.error(f"支付方式录入失败: {e}")
|
||||
self._screenshot("step5_error")
|
||||
raise
|
||||
|
||||
self._screenshot("step5_done")
|
||||
|
||||
def fill_payment(self, invoices: list[dict[str, Any]]) -> None:
|
||||
"""录入支付信息"""
|
||||
log.info("录入支付信息...")
|
||||
@@ -314,11 +417,10 @@ class ReimburseBot:
|
||||
self._screenshot("step5_done")
|
||||
|
||||
def upload_attachments(self, invoices: list[dict[str, Any]]) -> None:
|
||||
"""上传发票附件"""
|
||||
"""上传附件"""
|
||||
log.info("上传附件...")
|
||||
|
||||
try:
|
||||
self.page.click("#next3", timeout=5000)
|
||||
self.page.click("#next4", timeout=5000)
|
||||
self._wait_for("#submit2", timeout=5000)
|
||||
|
||||
attachment_files = sorted((self.work_dir or Path(__file__).parent.parent).glob("*.pdf"))
|
||||
@@ -329,8 +431,8 @@ class ReimburseBot:
|
||||
for i, inv in enumerate(invoices):
|
||||
file_path = attachment_files[i] if i < len(attachment_files) else None
|
||||
try:
|
||||
self._wait_for("#insertAcc", timeout=20000) # 等待增加按钮出现
|
||||
self.page.click("#insertAcc", timeout=5000) # 点击增加按钮出现
|
||||
self._wait_for("#insertAcc", timeout=20000)
|
||||
self.page.click("#insertAcc", timeout=5000)
|
||||
self._wait_for("#fjlx", timeout=5000)
|
||||
self.page.select_option("#fjlx", "1")
|
||||
explanation = f"{inv['item_name']} - {inv['invoice_no']}"
|
||||
@@ -359,17 +461,33 @@ class ReimburseBot:
|
||||
|
||||
self._screenshot("step6_done")
|
||||
|
||||
def submit(self) -> None:
|
||||
"""提交报销单"""
|
||||
log.info("提交报销单...")
|
||||
def upload_travel_attachments(self, attachment_info: list[dict[str, Any]]) -> None:
|
||||
"""上传差旅附件"""
|
||||
log.info("上传差旅附件...")
|
||||
try:
|
||||
self.page.click("#submit", timeout=5000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
self._screenshot("submitted")
|
||||
self.page.click("#next4", timeout=5000)
|
||||
self._wait_for("#submit2", timeout=5000)
|
||||
|
||||
for info in attachment_info:
|
||||
attachment_file = self.work_dir / info["filename"]
|
||||
self._wait_for("#insertAcc", timeout=20000)
|
||||
self.page.click("#insertAcc", timeout=5000)
|
||||
self._wait_for("#fjlx", timeout=5000)
|
||||
if info["attachment_type"] == "invoice":
|
||||
self.page.select_option("#fjlx", "1")
|
||||
else:
|
||||
self.page.select_option("#fjlx", "2")
|
||||
self.page.fill("#fpsmxx", info["attachment_desc"])
|
||||
if attachment_file and attachment_file.exists():
|
||||
self.page.set_input_files("#file", str(attachment_file))
|
||||
self.page.wait_for_timeout(1000)
|
||||
self.page.click("#cjtj", timeout=5000)
|
||||
log.info("上传差旅附件完成")
|
||||
except Exception as e:
|
||||
log.error(f"提交失败: {e}")
|
||||
self._screenshot("submit_error")
|
||||
log.error(f"差旅附件上传失败: {e}")
|
||||
self._screenshot("travel_attachment_error")
|
||||
raise
|
||||
self._screenshot("travel_attachment_done")
|
||||
|
||||
def close(self) -> None:
|
||||
"""关闭浏览器"""
|
||||
@@ -382,10 +500,6 @@ class ReimburseBot:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 辅助方法
|
||||
# --------------------------------------------------------
|
||||
|
||||
def _wait_for(self, selector: str, timeout: int | None = None) -> None:
|
||||
self.page.wait_for_selector(selector, timeout=timeout)
|
||||
|
||||
@@ -394,31 +508,131 @@ class ReimburseBot:
|
||||
img_dir.mkdir(exist_ok=True)
|
||||
self.page.screenshot(path=str(img_dir / f"debug_{name}.png"))
|
||||
|
||||
def fill_travel_subsidy(self, subsidy_info: list[dict[str, Any]]) -> None:
|
||||
"""录入差旅补助清单"""
|
||||
log.info("录入差旅补助清单...")
|
||||
try:
|
||||
self.page.click("#next3", timeout=5000)
|
||||
self._wait_for("#next4", timeout=5000)
|
||||
|
||||
for info in subsidy_info:
|
||||
self.page.click("#insertSubsidy", timeout=5000)
|
||||
self._wait_for('text="增加补助清单"', timeout=5000)
|
||||
self.page.click("#jzg3", timeout=5000)
|
||||
self.page.wait_for_timeout(500)
|
||||
if info["person_name"] and info["person_name"] != "":
|
||||
self.page.fill("#seacher", info["person_name"])
|
||||
elif info["person_id"] and info["person_id"] != "":
|
||||
self.page.fill("#seacher", info["person_id"])
|
||||
else:
|
||||
raise ValueError(f"人员编号和人员姓名不能同时为空: {info}")
|
||||
self.page.click("#cx", timeout=5000)
|
||||
self.page.wait_for_selector("div.fixed-table-loading", state="hidden", timeout=10000)
|
||||
self.page.click("#tableEmp tbody tr", timeout=10000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
|
||||
open_bank = self.page.input_value("#openbank1")
|
||||
if not open_bank:
|
||||
log.info("员工开户行未填写,默认填写中国工商银行")
|
||||
self.page.fill("#openbank1", "中国工商银行")
|
||||
|
||||
self.page.fill("#startdate1", _format_date(info["start_date"]))
|
||||
self.page.fill("#enddate1", _format_date(info["end_date"]))
|
||||
self.page.fill("#trafficdays1", str(info["days"]))
|
||||
self.page.fill("#fooddays1", str(info["days"]))
|
||||
self.page.fill("#trafficnorm1", str(80))
|
||||
self.page.fill("#foodnorm1", str(100))
|
||||
trafficmoney = int(info["days"]) * 80
|
||||
foodmoney = int(info["days"]) * 100
|
||||
subsidymoney = trafficmoney + foodmoney
|
||||
self.page.fill("#trafficmoney1", str(trafficmoney))
|
||||
self.page.fill("#foodmoney1", str(foodmoney))
|
||||
self.page.fill("#subsidymoney1", str(subsidymoney))
|
||||
self.page.click("#add", timeout=3000)
|
||||
self.page.wait_for_timeout(1000)
|
||||
except Exception as e:
|
||||
log.error(f"差旅补助清单录入失败: {e}")
|
||||
self._screenshot("subsidy_error")
|
||||
raise
|
||||
|
||||
self._screenshot("subsidy_done")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 对外入口
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def run_bot(
|
||||
config: dict[str, Any], invoices: list[dict[str, Any]], headless: bool = False, work_dir: Path | None = None
|
||||
) -> None:
|
||||
"""执行完整的浏览器填报流程"""
|
||||
def run_bot(config: dict[str, Any], headless: bool = False, work_dir: Path | None = None) -> None:
|
||||
"""执行完整的浏览器填报流程,根据发票类型自动路由"""
|
||||
if not config["username"] or not config["password"]:
|
||||
raise ValueError("缺少用户名或密码")
|
||||
|
||||
bot = ReimburseBot(config, headless=headless)
|
||||
if not work_dir:
|
||||
raise ValueError("缺少工作目录")
|
||||
|
||||
from .doc.llm_extractor import load_cache
|
||||
|
||||
cache_map = load_cache(work_dir)
|
||||
doc_values = [v for k, v in cache_map.items() if k != "travel_info"]
|
||||
invoices = [data for data in doc_values if data.get("invoice_type") not in ("application", "payment")]
|
||||
applications = [data for data in doc_values if data.get("invoice_type") == "application"]
|
||||
|
||||
groups = _classify_invoice_batch(invoices)
|
||||
travel_invoices = groups["travel"]
|
||||
general_invoices = groups["general"]
|
||||
|
||||
log.info(
|
||||
f"发票分类: 差旅 {len(travel_invoices)} 张, 普通 {len(general_invoices)} 张, "
|
||||
f"出差事前申请单 {len(applications)} 份"
|
||||
)
|
||||
|
||||
if travel_invoices and general_invoices:
|
||||
raise ValueError(
|
||||
f"发票类型混合(差旅 {len(travel_invoices)} 张 + 普通 {len(general_invoices)} 张),不支持混报,请分开提交"
|
||||
)
|
||||
|
||||
is_travel = bool(travel_invoices)
|
||||
travel_info: dict[str, Any] = cache_map.get("travel_info", {})
|
||||
if is_travel and not travel_info:
|
||||
from .doc.llm_extractor import CACHE_DIR_NAME, extract_travel_info
|
||||
|
||||
travel_info = extract_travel_info(source_dir=work_dir)
|
||||
cache_dir = work_dir / CACHE_DIR_NAME
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
with open(cache_dir / "travel_info.json", "w", encoding="utf-8") as f:
|
||||
json.dump(travel_info, f, ensure_ascii=False, indent=2)
|
||||
log.info("差旅信息已保存到缓存")
|
||||
|
||||
bot = ReimburseBot(config, headless=headless, work_dir=work_dir)
|
||||
bot.work_dir = work_dir
|
||||
try:
|
||||
bot.launch()
|
||||
bot.login_portal()
|
||||
bot.navigate_to_reimburse()
|
||||
bot.open_reimburse_menu()
|
||||
bot.fill_basic_info()
|
||||
bot.add_reimburse_items(invoices)
|
||||
bot.fill_payment(invoices)
|
||||
bot.upload_attachments(invoices)
|
||||
# bot.submit() # 确认无误后再取消注释
|
||||
|
||||
if is_travel:
|
||||
log.info("处理差旅发票...")
|
||||
bot.navigate_to_reimburse(page_key="travel_page")
|
||||
bot.create_new_form()
|
||||
basic_info = travel_info["basic_info"]
|
||||
bot.fill_travel_info(basic_info)
|
||||
travel_items = travel_info["reimbursement_details"]
|
||||
bot.add_travel_items(travel_items)
|
||||
payment_info = travel_info["payment_methods"]
|
||||
bot.fill_travel_payment(payment_info)
|
||||
subsidy_info = travel_info["subsidy_list"]
|
||||
bot.fill_travel_subsidy(subsidy_info)
|
||||
attachment_info = travel_info["attachments"]
|
||||
bot.upload_travel_attachments(attachment_info)
|
||||
else:
|
||||
log.info("处理普通发票...")
|
||||
bot.navigate_to_reimburse(page_key="reimburse_page")
|
||||
bot.create_new_form()
|
||||
bot.fill_basic_info()
|
||||
bot.add_reimburse_items(general_invoices)
|
||||
bot.fill_payment(general_invoices)
|
||||
bot.upload_attachments(general_invoices)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"操作失败: {e}")
|
||||
try:
|
||||
@@ -430,28 +644,6 @@ def run_bot(
|
||||
bot.close()
|
||||
|
||||
|
||||
def run_bot_web(config: dict[str, Any], invoices: list[dict[str, Any]], work_dir: Path) -> None:
|
||||
def run_bot_web(config: dict[str, Any], work_dir: Path) -> None:
|
||||
"""Web 模式填报 — headless,附件从指定目录读取"""
|
||||
if not config["username"] or not config["password"]:
|
||||
raise ValueError("缺少用户名或密码")
|
||||
|
||||
bot = ReimburseBot(config, headless=True)
|
||||
bot.work_dir = work_dir
|
||||
try:
|
||||
bot.launch()
|
||||
bot.login_portal()
|
||||
bot.navigate_to_reimburse()
|
||||
bot.open_reimburse_menu()
|
||||
bot.fill_basic_info()
|
||||
bot.add_reimburse_items(invoices)
|
||||
bot.fill_payment(invoices)
|
||||
bot.upload_attachments(invoices)
|
||||
except Exception as e:
|
||||
log.error(f"操作失败: {e}")
|
||||
try:
|
||||
bot._screenshot("error")
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
finally:
|
||||
bot.close()
|
||||
run_bot(config, headless=True, work_dir=work_dir)
|
||||
|
||||
Reference in New Issue
Block a user