
1. 先把需求讲清楚销售订单怎么变成出库单做SD模块的接口和报表开发迟早会遇到一个需求把销售订单批量生成外向交货单也就是常说的“根据订单创建出库单”。在SAP里这一串动作的标准事务代码是VL01N属于SD和LE的整合场景。很多刚接触的同事会问这不是手工敲个回车就行吗写程序是不是过度设计了实际上只要跑过几千行订单的批量交货你就能明白这个需求一旦出现往往是业务量逼着你上程序。先统一一下术语。不同行业叫法很乱有的叫发货单有的叫出库单SAP标准名称是“外向交货单”抬头表是LIKP行项目表是LIPS事务代码VL01N创建、VL02N修改或过账、VL03N查看。程序里做的核心动作就是把已审批的销售订单VBAK/VBAP以及订单计划行VBEP里的交货信息转换成一张有独立编号的外向交货单写进LIKP/LIPS并且在凭证流表VBFA里挂上与销售订单的前后继关系。这个系列在第1篇里已经把订单读取、凭证流这些前置内容铺开了这篇我就把重心放在“创建出库单”这个动作上重点讲清楚函数清单、参数结构、完整代码示例以及我在项目里踩过的坑。1.1 为什么放着VL01N不用非要写函数手工VL01N适用于零星补单一天几十单还行。但现实业务往往是这样电商中台把上千张订单推给SAP系统做完可用性检查后物流计划部要在半小时内生成交货单再推给仓库执行。这时候靠财务、计划员一个个敲屏幕既不现实也容易漏单。我见过最夸张的场景是一个单子有三十多个行项目光数量滚动就得核对半天人工操作完全扛不住。另外SAP标准其实给了批量交货方案比如事务代码VL10A、VL10B、VL10C它们在后台也可以跑批。那为什么还要自己写函数因为标准批量方案的筛选条件是写死的很多客户有自己的一套规则某些自定义字段值为空的不允许交货信用冻结的订单要跳过交货数量要按照客户指定比例拆分创建完成后还要回写外部系统状态。这些诉求靠VL10的增强也能做但维护成本高、排障不透明项目组权衡下来往往会选择写一个自有的批量创建程序。我的建议始终是先看标准功能能不能满足满足不了再自研不要一上来就写BAPI。1.2 创建出库单涉及的数据链路和状态位调用函数之前先得把这个业务动作涉及的数据链路搞懂不然排错的时候会很痛苦。销售订单这边抬头表VBAK里要重点关注VSTEL发运点、VBTYP销售凭证类型、LIFSK抬头交货冻结、VKORG等行项目表VBAP里要看WERKS工厂、LFSTA交货状态、ABSTA拒绝状态、LIFSK行项目交货冻结、PSTYV行项目类别计划行表VBEP里是WMENG计划行数量、BMENG已确认数量、EDATU计划行日期。交货单生成后写进LIKP/LIPS同时VBFA里会多出一条“交货单后单据”的凭证流记录。比较容易被忽略的是状态表VBUK和VBUP。很多异常单表面看数据都正常一调用BAPI就报“交货冻结”或者“订单未释放”其实状态就藏在这些表里。例如订单若被信用管理卡住或者有“全部交货冻结”VBUK里的交货状态字段会挂上冻结标识。批量创建程序里最好先查一遍这些状态能省下很多BAPI调用费。还有可用性检查标准流程里BAPI创建交货时会自动再做一次ATP检查数量、日期一变有可能出现警告甚至错误信息这些都要在返回结构里处理。2. 核心函数清单一个创建动作背后还站着哪些函数标题既然是“相关的函数列表介绍”这部分我把实际项目里围绕“订单创建出库单”用到的函数按用途分了几类整理成了一张清单。这也是我建议每个新人都要存一份的速查表。函数名用途使用场景BAPI_OUTB_DELIVERY_CREATE_SLS根据销售订单创建外向交货单主流程BAPI_OUTB_DELIVERY_CREATE_STO根据转储订单创建外向交货单工厂间STO补货SD_DELIVERY_CREATE传统创建函数维护老旧程序时识别BAPI_OUTB_DELIVERY_CHANGE修改交货单的行项目数量、日期创建后的调整BAPI_OUTB_DELIVERY_CONFIRM_DEC确认拣配并发货过账创建后直接过账BAPI_DELIVERYPROCESSING_EXEC交货单处理及过账新项目推荐WS_DELIVERY_UPDATE传统交货过账更新老项目仍在用ENQUEUE_EVVBLBE / DEQUEUE_EVVBLBE锁定/解锁交货单防并发重复创建BAPI_TRANSACTION_COMMIT提交BAPI事务创建后必须调用可用性检查类函数ATP库存检查创建前预检下面把重点的几个展开讲。2.1 创建主函数怎么选创建外向交货单的主函数标准BAPI是BAPI_OUTB_DELIVERY_CREATE_SLS。这个BAPI接收销售订单号和行项目号核心输入是一个内表结构BAPISDLS每个条目代表一个销售订单行可以一次性传多行、多单。系统会按照交货类型、发运点、收货方等条件自动拆分或合并结果在输出内表DELIVERYS里返回如果是正常创建里面会有交货单号。这个函数是我日常的主力。如果是库存转储订单STO也就是两个工厂之间的调拨不能直接用上面这个要用BAPI_OUTB_DELIVERY_CREATE_STO。这两兄弟参数结构很像但业务逻辑来源不同一个是销售订单一个是采购申请或转储单。项目里遇到跨工厂补货场景别搞混了。另外在存量系统里可能还会看到SD_DELIVERY_CREATE这个传统函数。它年纪很大参数相对简练也能从销售订单建交货单但返回消息是老式结构调试起来不如BAPI直观。新代码里不建议再用但接手旧程序时如果看到它要能认出来。2.2 围绕创建动作的周边函数BAPI本身不是一个独立事务。调用创建BAPI之后如果业务逻辑正确还要调BAPI_TRANSACTION_COMMIT提交否则卢指导说的“工作台更新但数据库没提交”就会发生。提交前如果发现返回里有错误一般调BAPI_TRANSACTION_ROLLBACK回滚。很多新手漏掉COMMIT创建完在程序里看到的单号跑完程序去VL02N里查却什么都没有就是这个原因。并发场景下还要考虑锁。交货单创建和过账都会锁表EVVBLBE对应的函数是ENQUEUE_EVVBLBE和DEQUEUE_EVVBLBE。批量程序虽然是顺序执行的但报表界面和定时任务可能同时触发不加锁可能出现同一个订单被创建出两张交货单。我一般在程序开头对要处理的订单号范围加锁创建完成再释放比单纯靠业务校验更保险。还有一类周边函数是消息处理。BAPI返回的RETURN是标准BAPIRET2内表字段包括TYPE消息类型、ID消息类、NUMBER、MESSAGE以及MESSAGE_V1到V4MESSAGE通常已经是格式化后的完整文本。如果程序还要兼容老接口可能碰到BAPIRETURN结构那就要用消息转换函数处理。实际项目里我更喜欢直接循环RETURN拼消息然后扔到自建日志表或者ALV展示既简单又直观。2.3 创建之后过账与确认相关函数创建交货单只是第一步。很多项目要做的其实是“创建并过账”也就是VL02N里那个“发货过账”动作在交付流程里叫PGIPost Goods Issue。过账后库存正式减少物料凭证产生财务后续才能开票。新项目中官方推荐的过账BAPI是BAPI_DELIVERYPROCESSING_EXEC和BAPI_OUTB_DELIVERY_CONFIRM_DEC。前者的签名比较绕输入是嵌套的BAPIIDPR结构需要填处理类型和交货单号后者名字直白确认拣配并过账很多外贸或电商项目直接用它完成PGI。老系统里更常见的是WS_DELIVERY_UPDATE这个函数直接更新交货单状态并过账不少大客户的旧接口就是这么跑的虽然SAP后来有了新BAPI但这个函数仍然活跃。这里必须提醒一句交货过账之前如果系统启用了WM或EWM仓库层面的确认步骤没做直接调过账BAPI很可能报错。所以不要急着在“创建”程序里顺手把PGI也做了。除非业务明确要求创建后立即过账并且不走仓库执行否则我建议只创建到交货单过账动作留给仓库或下一个接口环节。做接口设计时把“创建”和“过账”拆成两个明确步骤后续排障会轻松很多。3. 实操用BAPI_OUTB_DELIVERY_CREATE_SLS完整跑一个批量创建程序理论讲完上一段代码。下面这套逻辑我最近一个项目里还在用经历了多轮需求变更整体比较稳。虽然每家客户增强不一样但框架可以直接抄。3.1 参数结构和字段准备先看BAPI_OUTB_DELIVERY_CREATE_SLS最常见的几个参数SALES_ORDERS输入表结构BAPISDLS每个条目对应销售订单行。字段SALES_ORDER是订单号SALES_ORDER_ITEM是行项目号DELIVERY_QTY是要创建的交货数量。DELIVERY输入单条结构BAPIDELIVERY。创建时常见用法是填DELIVERY_TYPE外向交货类型标准一般是LF和PICKING_DATE拣配日期。SHIPPING输入结构BAPISHIPPINGSHIP_POINT字段传发运点。如果系统能自动确定也可以不传但项目落地时不传的后果通常是BAPI报“无法确定装运点”所以稳妥起见我都是先读出来主动传进去。DELIVERYS输出表结构与BAPIDELIVERY一致。创建成功后从第一行取DELIVERY字段就是交货单号。RETURN输出表BAPIRET2标准消息结构。发运点怎么取销售订单抬头表VBAK本身有VSTEL字段。绝大多数正常单子创建时就已经确定发运点直接拿出来传给SHIPPING-SHIP_POINT。少数老数据没维护可以从销售组织对应的表TVTA取默认发运点实在取不到就报错提示配置问题。这个顺序我在项目里反复验证过比让BAPI自己去猜要可控。3.2 创建前要做的数据准备批量创建不是无脑循环。我写的程序里每个订单行进入BAPI之前都要过三关。第一关是冻结检查。读取VBAK-LIFSK和VBAP-LIFSK如果有值说明订单层面或行项目层面挂了交货冻结直接跳过并记录下来。还要看VBAP-ABSTA如果行项目被拒绝也别传进去。第二关是剩余数量计算。交货数量不能拍脑袋传标准逻辑是“计划行数量减去已交货数量”。计划行数量从VBEP的WMENG或BMENG取已交货数量可以从LIPS按参考订单号汇总也可以从VBFA查已流转数量。把我前面提到的数据链路利用起来这步做扎实了创建出来的交货单数量才是业务可用的。如果你不传DELIVERY_QTYBAPI内部也有默认数量逻辑但那个行为不够透明生产环境里一旦数据有偏差很难解释所以我还是建议显式计算并传递。第三关是重复创建检查。有些定时任务跑着跑着发生异常重跑就很容易把同一订单再创建一遍。我习惯在创建前先按订单号去查VBFA里有没有已经存在状态正常的交货单记录有就跳过。这个检查配合锁函数基本能杜绝重复单。3.3 代码骨架给一个可以直接跑通的最小版本。为了看逻辑方便我把订单来源简化成一段内表赋值实际项目里改成从选择屏幕、接口表或数据库表读就可以了。DATA: ls_so TYPE bapisdls, ls_delivery TYPE bapidelivery, ls_shipping TYPE bapishipping, lt_sales TYPE TABLE OF bapisdls, lt_delivery TYPE TABLE OF bapidelivery, lt_return TYPE TABLE OF bapiret2, ls_dlv_out TYPE bapidelivery, lv_vbeln TYPE vbeln_vl. * 准备交货类型和日期 ls_delivery-delivery_type LF. ls_delivery-picking_date sy-datum. * 发运点订单表里读取取不到则报错跳过 SELECT SINGLE vstel FROM vbak INTO ls_shipping-ship_point WHERE vbeln p_vbeln. IF sy-subrc 0 OR ls_shipping-ship_point IS INITIAL. MESSAGE e001(zsd) WITH p_vbeln 发运点未维护. ENDIF. * 组装销售订单行 CLEAR ls_so. ls_so-sales_order p_vbeln. ls_so-sales_order_item 000010. ls_so-delivery_qty 10. 这里传计算后的剩余可交数量 APPEND ls_so TO lt_sales. * 调用创建BAPI CALL FUNCTION BAPI_OUTB_DELIVERY_CREATE_SLS TABLES sales_orders lt_sales deliverys lt_delivery return lt_return. * 检查错误 IF line_exists( lt_return[ type E ] ). LOOP AT lt_return INTO DATA(ls_ret) WHERE type CA EA. WRITE: / 创建失败, ls_ret-message. ENDLOOP. CALL FUNCTION BAPI_TRANSACTION_ROLLBACK. ELSE. READ TABLE lt_delivery INTO ls_dlv_out INDEX 1. lv_vbeln ls_dlv_out-delivery. CALL FUNCTION BAPI_TRANSACTION_COMMIT EXPORTING wait X. WRITE: / 交货单创建成功, lv_vbeln. ENDIF.这里只演示了一个订单一个行项目的情况。核心要点有两个一是RETURN里只要出现TYPE为E或A的消息就要按失败处理不能只认SY-SUBRC二是创建成功后必须调BAPI_TRANSACTION_COMMIT而且建议把WAIT参数设置成‘X’确保交货单号和物料凭证在提交完成后立即可查。3.4 多订单、多行项目怎么拆单生产环境里一个销售订单经常有十几个行项目一次批量程序可能处理成百上千个订单。直接把所有行塞进SALES_ORDERS内表BAPI会按系统交货拆分规则自动分组。大多数情况下同一个订单、同一个发运点的行项目会合并到同一张交货单但如果行项目工厂不同或发运点不同就会被拆成多张交货单。这本身不是错误但业务预期要提前对齐别等上线后业务拿着单号来问“为什么一个单子拆了三次”。我常用的策略是一个销售订单一个销售订单地处理每个订单内部所有行项目一次性传给BAPI拿到返回结果后再处理下一个订单。这样既保证了一个订单内部逻辑上的整体性又方便定位失败原因。如果你把几百个订单一次性传给BAPI返回的DELIVERYS和RETURN混在一起你想知道哪个失败对应哪个订单处理起来非常痛苦。提交策略同样要讲。每个订单创建成功后立刻COMMIT失败则ROLLBACK。有人喜欢全批量做完再统一COMMIT一旦中途有锁等待或者运行到一半程序崩了之前创建的几百张交货单全部回滚业务那边很难接受。分单提交看起来多消耗一点数据库操作时间但换来的是每单独立、失败户可控生产环境里最重要。4. 常见报错与业务数据问题的排查实录这块内容是我个人最想写的部分。BAPI的代码逻辑相对固定真正折腾人的都是各种业务数据问题和配置问题。4.1 创建报错总是一个E但具体原因藏在消息里很多初学者看到RETURN里有E就慌了直接把MESSAGE整段抛给业务业务也看不懂。这里分享一个排查顺序先看消息类ID和消息号再上SE91查消息详情最后结合后台配置确认。比如常见的VL 581 错误“交货冻结被设置”多半是订单抬头的LIFSK挂了值信用管理释放或财务释放没有过VL 348 错误提示可用性检查异常那就是ATP数量不够或检查范围配置问题。BAPIRET2的MESSAGE字段虽然是格式化好的文本但真正精确到原因是ID和NUMBER组合。日志里建议把TYPE、ID、NUMBER、MESSAGE四个字段都记录下来方便事后翻查。4.2 交货冻结、信用冻结、项目被拒这类问题出现频率非常高。订单在VA01创建后如果信贷检查没过或者销售未释放抬头状态就会挂上冻结。批量程序创建时直接报“订单被冻结”。我的做法是在创建前查询VBAK-LIFSK和VBAP-LIFSK有值就跳过并通过日志告诉业务“这张单因为什么冻结被跳过了”。这样业务可以去后台处理程序也不会傻傻地一直报错重试。还有一种情况是行项目被整体拒掉判断条件是VBAP-ABSTA非空。这类行项目即使传进去BAPI也会返回错误。创建前拦掉程序执行效率和可读性都更好。4.3 可用性检查不通过和批次拆分可用性检查不通过有两种表现。一种是直接报错比如“物料 XXX 没有库存”这种好理解就是ATP范围里找不到足够库存。另一种是BAPI返回警告提示“缺货数量多少”但交货单还是创建出来了交货数量被系统自动扣减。这种最危险因为你以为创建成功数量就对实际上数量已经变了。处理方式有两种一种是创建前自己跑一遍可用性检查数量不够就跳过另一种是创建后对比SALES_ORDERS传入数量与交货单实际行项目数量不一致就记录告警。我倾向于后一种因为前一种要额外维护一套ATP检查逻辑而后直接利用BAPI创建时的检查结果代码改动小、逻辑可靠。批次拆分问题也很常见。物料启用了自动批次确定但客户希望在创建交货时按指定批次创建。BAPI本身不直接接收批次信息需要走增强或者先强制固定批次。遇到这种情况我一般先确认是否必须按批次交货如果是就得在增强点MV50AFLL或BADI LE_SHP_DELIVERY_PROC里处理。4.4 发运点、工厂、库位没配齐“装运点未定义”“无法确定工厂”这类错误九成是后台配置缺失。BAPI的逻辑是死的它必须在配置表里找到对应的发运点、工厂、库位找不到就报错。排查顺序先看销售订单里有没有维护好销售组织、分销渠道、部门再看VBAK-VSTEL是否有值没有就去TVTA里按销售组织匹配发运点。工厂和库位层面要检查订单行项目里的WERKS是否在交货单相关配置中存在行项目默认的库位LGORT是否维护到物料主数据自动批次确定是否配好了。这些配置问题在测试环境就要反复验证别等到生产批量作业跑挂了再去查。4.5 受托项目等特殊业务场景如果你的客户做受托代销业务也就是“受托项目”这个场景必须单独说。受托业务下库存是客户的SAP里的处理逻辑和标准销售订单交货不同。订单的凭证类型、行项目类别都有特殊性直接从销售订单调用标准创建BAPI很可能创建出来的交货单不符合受托业务要求或者干脆不允许走标准发货流程。以前遇到过一个需求业务方坚持认为“订单能建出来出库单就能建出来”结果上线后一堆受托订单在创建交货时被拦报错信息指向“不能与SD发货一起返回”这类的业务约束。后来我们统一做了行项目类别判断VBTYP和PSTYV匹配受托业务时直接走另一套处理逻辑或者跳过由专门接口处理问题才解决。教训就是批量创建程序不要一视同仁先把业务模式和项目类别分清楚。4.6 并发重复创建批量程序跑批一般放在半夜但有些客户报表和生产导入任务会同时触发两个程序都读到了同一个未交货订单结果就是创建出两张交货单。这种问题在测试环境很难复现生产上出现一次就够头疼。解决办法是加锁。在程序处理单个订单前调用ENQUEUE_EVVBLBE锁住订单对应的交货单范围创建成功并COMMIT后再调用DEQUEUE_EVVBLBE释放锁。如果加锁时返回加锁失败说明有其他进程正在处理程序直接跳过这个订单。同时在数据层面用VBFA做重复检查兜底。双保险下来并发重复创建的问题基本能杜绝。5. 批量作业上线前一定要做的几项检查最后结合我自己的交付经验给大家列一个上线前检查清单。这些内容不是教科书上的是大大小小项目里踩坑换来的。第一日志务必结构化。批量创建程序必须有日志表或者ALV输出至少要记录处理时间、销售订单号、行项目号、传入数量、创建出的交货单号、返回消息类型、消息文本、处理状态。业务人员不需要懂ABAP看到一张表就知道哪些成功、哪些失败、为什么失败。很多项目上线初期业务天天来找开发问“昨天跑批结果咋样”就是因为日志没做好。第二测试数据要覆盖特殊场景。普通订单只是最基本的情况测试时要专门准备有交货冻结的订单、可用性不足的订单、跨工厂多行项目的订单、批次物料订单、受托业务订单、已经部分交货的订单。每个场景走一遍程序才不会上线后四处冒烟。第三考虑重跑机制。程序要设计成可重跑的。如果某个时段跑批失败修复问题后重新执行不能重复创建出库单。这就需要前面说的重复检查逻辑兜底。我的习惯是在程序里把“已成功创建交货单”的订单存入日志表重跑时先读日志表排除再结合VBFA查一遍双保险。第四过账动作谨慎开启。除非业务非常明确否则创建程序里默认只创建交货单不做过账。过账环节单独一个函数或接口由仓库或下一步流程触发。我曾经见过一个程序创建完自动过账结果因WM未确认导致大量错误单最后业务手工补救了两周。把动作拆开风险面积小很多。第五性能控制在可接受范围。BAPI调用本身有开销尤其几百上千个订单循环调用时不能一台服务器硬扛。我一般建议批量程序分批处理比如一次循环50个订单每批COMMIT一次外加大量数据时用并行进程但并行的前提是对锁和日志做好并发控制。性能优化有一个原则先保证正确性再谈速度。回到创建出库单这件事本身SAP的标准功能和增强点已经提供了足够多的支撑BAPI_OUTB_DELIVERY_CREATE_SLS只是其中最关键的一环。真正决定项目质量的是数据准备、异常处理和日志设计这些外围工夫。把这一整套流程想明白不要说接一个销售订单创建交货单的接口就是以后让你接EWM、接SRM、接电商中台的交付流程心里都有底。