项目概要
~80%
操作时间节省
8~10s
优化后每条耗时
1~2min
优化前每条耗时
项目背景
每月需对 SAP 中大量已过账发票的抬头文本和行项目文本进行修改(FB02),以满足集团报表字段要求。手工逐条进入 FB02、填写文本、保存,每条约 1~2 分钟,数量多时极为耗时。
解决方案
三脚本联动:VBScript 自动从 SAP ZFI006 导出当月凭证数据 → Python 清洗处理生成标准修改文本 → VBScript 再次调用 SAP GUI Scripting 批量进入 FB02 逐条修改并保存,全程无需人工干预。
ZFI006导出数据
→
Python清洗处理
→
FB02批量修改
→
结果写回Excel
核心难点
SafeFindById 重试机制:SAP界面加载慢时自动等待重试,最多5次,避免脚本意外中断;waitMultiplier 压缩系数可调节整体速度;进度实时写回 Excel 便于断点续跑;特殊客户追加送达方信息的分支处理。
技术栈
项目文档
一、背景与目标
SAP 中已过账凭证的行项目文本需按集团规范格式修改,包括抬头文本和两个行项目的文本字段。手工操作约 1~2 分钟/条,自动化后约 8~10 秒/条,节省约 80%。
二、三脚本说明
| 脚本 | 功能 | 输入/输出 |
|---|---|---|
| zfi006导出.vbs | 连接SAP,进入ZFI006,设置当月日期范围,导出凭证数据,调用过账.exe | → 发票明细.xlsx |
| 过账.py | 去重、客户名称映射、清除括号、构建抬头/行项目文本,特殊客户追加送达方 | 发票明细.xlsx → 行项目格式.xlsx |
| 更改行项目.vbs | 逐行读取过账1.xlsx,进入FB02批量修改抬头文本和行项目文本,进度写回Excel | 行项目格式.xlsx → SAP FB02 |
三、关键机制
SafeFindById 函数:每次操作前等待SAP元素加载,最多重试5次,超时后弹出提示并退出,防止界面未加载就操作导致脚本崩溃。waitMultiplier 压缩系数(默认0.3)控制所有等待时间,可根据网络状况调整。
效果展示
以下为三脚本联动全流程演示:ZFI006导出 → Python处理 → FB02批量修改。
录屏演示
FB02 批量修改过程
示例文件
以下为项目相关文件截图。(注:此文件中的数据均已进行脱敏及适应性修改,与公司业务无直接关联,仅用于演示或测试用途)
ZFI006 导出的原始数据
Python 处理后的凭证表
SAP FB02 修改凭证界面
状态写回 Excel 结果
源码
006导出.vbs — SAP ZFI006 自动导出
Option Explicit
' === 变量定义 ===
Dim SapGuiAuto, application, connection, session
Dim currentDate, firstDayDate, lastDayDate
Dim firstDayOfMonth, lastDayOfMonth
Dim ws, excelApp, workbook, exePath
' === SAP连接 ===
If Not IsObject(application) Then
Set SapGuiAuto = GetObject("SAPGUI")
Set application = SapGuiAuto.GetScriptingEngine
End If
If Not IsObject(connection) Then
Set connection = application.Children(0)
End If
If Not IsObject(session) Then
Set session = connection.Children(0)
End If
If IsObject(WScript) Then
WScript.ConnectObject session, "on"
WScript.ConnectObject application, "on"
End If
' === 日期计算 - 获取当月第一天和最后一天 ===
currentDate = Date()
firstDayDate = DateSerial(Year(currentDate), Month(currentDate), 1)
lastDayDate = DateAdd("d", -1, DateAdd("m", 1, firstDayDate))
firstDayOfMonth = Year(firstDayDate) & "." & Right("0" & Month(firstDayDate), 2) & "." & Right("0" & Day(firstDayDate), 2)
lastDayOfMonth = Year(lastDayDate) & "." & Right("0" & Month(lastDayDate), 2) & "." & Right("0" & Day(lastDayDate), 2)
' === 执行操作 ===
session.findById("wnd[0]").maximize
' 导航到ZFI006事务码
session.findById("wnd[0]/tbar[0]/okcd").text = "/nzfi006"
session.findById("wnd[0]/tbar[0]/btn[0]").press
session.findById("wnd[0]").sendVKey 4 ' F4键
' 选择特定行(这里选择第3行,您可以根据需要调整)
session.findById("wnd[1]/usr/cntlCUSTOM_CONTAINER/shellcont/shell").setCurrentCell 3,"VTEXT"
session.findById("wnd[1]/usr/cntlCUSTOM_CONTAINER/shellcont/shell").selectedRows = "3"
session.findById("wnd[1]/usr/cntlCUSTOM_CONTAINER/shellcont/shell").doubleClickCurrentCell
' 设置日期范围(使用动态计算的当月第一天和最后一天)
session.findById("wnd[0]/usr/ctxtS_FKDAT-LOW").text = firstDayOfMonth
session.findById("wnd[0]/usr/ctxtS_FKDAT-HIGH").text = lastDayOfMonth
session.findById("wnd[0]/usr/ctxtS_FKDAT-LOW").setFocus
session.findById("wnd[0]/usr/ctxtS_FKDAT-LOW").caretPosition = 10
' 执行查询
session.findById("wnd[0]/tbar[1]/btn[8]").press
' 导出结果
session.findById("wnd[0]/mbar/menu[0]/menu[3]/menu[1]").select
session.findById("wnd[1]/usr/subSUB_CONFIGURATION:SAPLSALV_GUI_CUL_EXPORT_AS:0512/txtGS_EXPORT-FILE_NAME").text = "过账"
session.findById("wnd[1]/usr/subSUB_CONFIGURATION:SAPLSALV_GUI_CUL_EXPORT_AS:0512/txtGS_EXPORT-FILE_NAME").caretPosition = 6
session.findById("wnd[1]/tbar[0]/btn[20]").press
' 设置保存路径
session.findById("wnd[1]/usr/ctxtDY_PATH").text = "C:\Users\DELL\Desktop\"
session.findById("wnd[1]/usr/ctxtDY_PATH").setFocus
session.findById("wnd[1]/usr/ctxtDY_PATH").caretPosition = 0
session.findById("wnd[1]/tbar[0]/btn[11]").press
' === 检查并关闭已打开的"过账.xlsx"文件 ===
On Error Resume Next ' 启用错误处理
Set excelApp = GetObject(, "Excel.Application") ' 尝试获取Excel应用程序对象
If Err.Number = 0 Then ' 如果Excel正在运行
For Each workbook In excelApp.Workbooks
' 检查工作簿名称是否包含"过账"(不区分大小写)
If InStr(1, workbook.Name, "过账", vbTextCompare) > 0 Then
workbook.Close False ' 关闭工作簿,不保存更改
WScript.Echo "已关闭已打开的Excel文件: " & workbook.Name
End If
Next
' 如果没有打开的工作簿,退出Excel
If excelApp.Workbooks.Count = 0 Then
excelApp.Quit
End If
End If
On Error GoTo 0 ' 禁用错误处理
' 释放Excel对象
Set workbook = Nothing
Set excelApp = Nothing
' === 运行shoukuna.exe程序 ===
Set ws = CreateObject("WScript.Shell")
exePath = Chr(34) & "C:\Users\DELL\Desktop\新建文件夹 (2)\新建文件夹\program\过账.exe" & Chr(34)
ws.Run exePath, 1, True ' 等待程序执行完成后再继续下一步
Set ws = Nothing ' 释放资源
' 提示完成
WScript.Echo "所有操作已完成!"过账.py — 数据清洗与文本构建
from csv import excel
import pandas as pd
from datetime import datetime
# 读取数据
lujing = r"C:\Users\DELL\Desktop\过账.xlsx"
ke = pd.read_excel(lujing, sheet_name='Sheet1', dtype=str)
# 去重并合并(保持你的原始逻辑)
jieguo11 = ke.drop_duplicates(subset=['SAP发票号'])
biaoge = pd.merge(jieguo11, ke, how='inner')
zidian = {
'青岛XXXXXX医院(青岛XXXX民医院)': '青岛XXX医院',
'青岛市XX院(青岛X院、青岛X所)': '青市XX院'}
#映射 true精确匹配字符串
biaoge=biaoge.replace(zidian)
# 处理付款方描述中的括号内容
biaoge['付款方描述'] = biaoge['付款方描述'].str.replace(r'\(.*\)', '', regex=True)
# 关键:处理空值(将NaN转为空字符串,避免后续判断报错)
biaoge['付款方描述'] = biaoge['付款方描述'].fillna('') # 防止in判断时出现float
biaoge['金税发票号'] = biaoge['金税发票号'].fillna('')
biaoge['送达方描述'] = biaoge['送达方描述'].fillna('')
# 先定义当天日期(避免重复计算)
today = datetime.now().strftime('%Y%m%d')
# 初始化发票后8位和当天日期(保留你的原始格式)
biaoge['发票后8位'] = biaoge['付款方描述'] + '/' + biaoge['金税发票号'].str[-8:]
biaoge['当天日期'] = biaoge['付款方描述'] + '/' + today
# 定义需要匹配的客户
target_companies = ['烟台XX司', '国XXX公司']
# 用函数处理每行数据(返回两个值,对应两列)
def process_row(row):
# 判断是否是目标客户(先转字符串避免类型问题)
is_target = any(company in row['付款方描述'] for company in target_companies)
if is_target:
# 目标客户:在原有基础上添加送达方描述
new_invoice = f"{row['发票后8位']}/{row['送达方描述']}"
new_date = f"{row['当天日期']}/{row['送达方描述']}"
return [new_invoice, new_date] # 返回两个值,对应两列
else:
# 非目标客户:保持原始值
return [row['发票后8位'], row['当天日期']]
# 应用函数给两列赋值(解决维度不匹配问题)
biaoge[['发票后8位', '当天日期']] = biaoge.apply(process_row, axis=1, result_type='expand')
# 选择需要的列
mycolumn = [
'会计凭证号',
'SAP发票号',
'发票后8位',
'当天日期',
'金税发票号',
'送达方描述',
]
biaoge['发票后8位'] = biaoge['发票后8位'].str.replace(r'\(.*\)', '', regex=True)
biaoge['当天日期'] = biaoge['当天日期'].str.replace(r'\(.*\)', '', regex=True)
biaoge = biaoge[mycolumn].copy()
biaoge=biaoge.dropna(subset='会计凭证号')
# 对DataFrame按'SAP发票号'列升序排序
biaoge = biaoge.sort_values(by='SAP发票号', ascending=True)
lujing2 = r"C:\Users\DELL\Desktop\过账1.xlsx"
biaoge.to_excel(lujing2,sheet_name='凭证表',index=False)
print(biaoge)更改行项目.vbs — SAP FB02 批量修改
Option Explicit
Dim SapGuiAuto, application, connection, session
Dim objExcel, wb, ws
Dim i, lastRow
Dim waitMultiplier
Dim WshShell
Dim belnr, colC, colD, colE
Dim excelPath, sheetName
waitMultiplier = 0.3 ' 等待时间压缩为 30%
Set WshShell = CreateObject("WScript.Shell")
' === 设置Excel路径 ===
excelPath = "C:\Users\DELL\Desktop\过账1.xlsx"
sheetName = "凭证表"
' === SAP连接 ===
If Not IsObject(application) Then
Set SapGuiAuto = GetObject("SAPGUI")
Set application = SapGuiAuto.GetScriptingEngine
End If
If Not IsObject(connection) Then
Set connection = application.Children(0)
End If
If Not IsObject(session) Then
Set session = connection.Children(0)
End If
If IsObject(WScript) Then
WScript.ConnectObject session, "on"
WScript.ConnectObject application, "on"
End If
session.findById("wnd[0]").maximize
WScript.Sleep 1000 * waitMultiplier
' === 打开Excel文件 ===
Set objExcel = CreateObject("Excel.Application")
Set wb = objExcel.Workbooks.Open(excelPath)
Set ws = wb.Sheets(sheetName)
objExcel.Visible = True ' 可选:显示Excel窗口
' 获取最后一行
lastRow = ws.Cells(ws.Rows.Count, 1).End(-4162).Row ' xlUp
' 遍历处理所有凭证
For i = 2 To lastRow
On Error Resume Next
' 从Excel读取数据
belnr = Trim(ws.Cells(i, 1).Value) ' A列凭证号
colC = Trim(ws.Cells(i, 3).Value) ' C列文本
colD = Trim(ws.Cells(i, 4).Value) ' D列文本
colE = Trim(ws.Cells(i, 5).Value) ' E列文本
' 执行SAP操作流程
SafeFindById "wnd[0]/tbar[0]/okcd", 1000
session.findById("wnd[0]/tbar[0]/okcd").text = "/nfb02"
session.findById("wnd[0]/tbar[0]/btn[0]").press
WScript.Sleep 500 * waitMultiplier
SafeFindById "wnd[0]/usr/txtRF05L-BELNR", 1000
session.findById("wnd[0]/usr/txtRF05L-BELNR").text = belnr
session.findById("wnd[0]").sendVKey 0
WScript.Sleep 500 * waitMultiplier
SafeFindById "wnd[0]/tbar[1]/btn[5]", 1000
session.findById("wnd[0]/tbar[1]/btn[5]").press
WScript.Sleep 500 * waitMultiplier
SafeFindById "wnd[1]/usr/txtBKPF-BKTXT", 1000
session.findById("wnd[1]/usr/txtBKPF-BKTXT").text = colC
session.findById("wnd[1]").sendVKey 0
WScript.Sleep 500 * waitMultiplier
SafeFindById "wnd[0]/usr/cntlCTRL_CONTAINERBSEG/shellcont/shell", 1000
session.findById("wnd[0]/usr/cntlCTRL_CONTAINERBSEG/shellcont/shell").currentCellColumn = "KOBEZ"
session.findById("wnd[0]/usr/cntlCTRL_CONTAINERBSEG/shellcont/shell").doubleClickCurrentCell
WScript.Sleep 500 * waitMultiplier
SafeFindById "wnd[0]/usr/ctxtBSEG-SGTXT", 1000
session.findById("wnd[0]/usr/ctxtBSEG-SGTXT").text = colD
session.findById("wnd[0]/usr/ctxtBSEG-SGTXT").setFocus
session.findById("wnd[0]/usr/ctxtBSEG-SGTXT").caretPosition = Len(colD)
WScript.Sleep 300 * waitMultiplier
SafeFindById "wnd[0]/tbar[1]/btn[19]", 1000
session.findById("wnd[0]/tbar[1]/btn[19]").press
WScript.Sleep 500 * waitMultiplier
SafeFindById "wnd[0]/usr/ctxtBSEG-SGTXT", 1000
session.findById("wnd[0]/usr/ctxtBSEG-SGTXT").text = colE
session.findById("wnd[0]/usr/ctxtBSEG-SGTXT").setFocus
session.findById("wnd[0]/usr/ctxtBSEG-SGTXT").caretPosition = Len(colE)
WScript.Sleep 300 * waitMultiplier
SafeFindById "wnd[0]/tbar[0]/btn[11]", 1000
session.findById("wnd[0]/tbar[0]/btn[11]").press
WScript.Sleep 1000 * waitMultiplier
' 检查错误
If Err.Number <> 0 Then
ws.Cells(i, 6).Value = "错误: " & Err.Description
Err.Clear
' 尝试返回初始界面
NavigateToInitialScreen
Else
ws.Cells(i, 6).Value = "处理成功"
End If
' 保存Excel状态
wb.Save
' 添加额外等待确保操作完成
WScript.Sleep 800 * waitMultiplier
Next
' 关闭Excel
wb.Close(True)
objExcel.Quit
MsgBox "所有凭证处理完成! 共处理 " & (lastRow - 1) & " 条凭证", vbInformation
WScript.Quit 0
' === 自定义函数 ===
Sub NavigateToInitialScreen()
On Error Resume Next
session.findById("wnd[0]/tbar[0]/okcd").text = "/n"
session.findById("wnd[0]/tbar[0]/btn[0]").press
WScript.Sleep 1000 * waitMultiplier
End Sub
Function SafeFindById(elementId, waitTime)
On Error Resume Next
Dim retryCount
retryCount = 0
Do While retryCount < 5
session.findById(elementId)
If Err.Number = 0 Then Exit Do
Err.Clear
retryCount = retryCount + 1
WScript.Sleep waitTime * waitMultiplier
Loop
If Err.Number <> 0 Then
MsgBox "错误:无法找到元素 " & elementId & vbCrLf & "请手动处理并重新运行脚本", vbCritical
WScript.Quit 1
End If
End Function