项目概要

~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 便于断点续跑;特殊客户追加送达方信息的分支处理。

技术栈

VBScript SAP GUI Scripting API Excel COM Python pandas

项目文档

版本:v1.0更新:2026-05类型:SAP自动化 / Python / VBScript

一、背景与目标

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