SAP SFP生成PDF表单:建模到Driver Program调用全流程
做 SAP 开发的谁没被“出一张好看的单据”这个需求缠过几回客户一句话背后就是整套打印开发流程。在 SD、MM、FI 的标准单据输出里绕不开 SAPscript、Smart Forms、SFP 这三条技术路线。SFP 也就是基于 Adobe PDF 的表单方案近年来越来越多项目在 SAP GUI 里直接用 SFP 建模再通过 Driver Program驱动报表程序调用生成 PDF 文件做打印、邮件发送、合并归档。这篇文章不打算做理论对比直接讲落地。我最近正好在 SAP GUI 里用 SFP 完成了一套 PDF 表单从建模到 Driver Program 调用全程走通中间踩了不少基础但致命的坑。今天把整个流程拆出来SFP 建模时该定义什么、Layout 怎么摆、Context 怎么绑定、调用程序的函数名怎么取、输出参数怎么设以及最常见的缓存、字体、空白页问题怎么排查。无论你是刚开始接触 Adobe Form 的新人还是被 SFP 搞得头疼的 ABAPer这篇应该都能帮上忙。1. 为什么要在 SAP GUI 里选择 SFP 做 Adobe PDF 表单1.1 三种输出方案对比SAPscript、Smart Forms 与 SFPSAP 传统的单据输出有三条路很多人容易混淆。SAPscript 是老一代方案基于 Script Form 的排版语言适合简单文本和地址打印但要说做漂亮的表格、图片、条形码开发起来非常痛苦。Smart Forms 是下一代SAP GUI 里能做可视化布局输出时也能通过函数模块直接获取 PDF但本质还是 SAP 自身的排版引擎很多细节需要动代码。SFP 全称 SAP Form Processing是基于 Adobe LiveCycle Designer 的 PDF 表单方案Layout 上用拖拽方式设计输出结果是原生 PDF数据接口、子表单、校验规则、交互域都支持。对比项SAPscriptSmart FormsSFPAdobe Form设计工具Script 编程窗口SAP GUI 图形设计器Adobe LiveCycle Designer 嵌入环境输出介质打印列表Spool / PDF原生 PDF / 打印 / 邮件附件数据接口定义不直观通过函数模块传参Form Interface Context 绑定交互式表单域不支持有限支持支持签名域、输入域复杂度简单场景还行中等复杂度设计器上手成本略高但上限高如果你的需求只是打印一张发票、一份采购订单Smart Forms 完全够用。但一旦客户要求“PDF 文件直接作为邮件附件发出”“模板上放二维码”“签字域要可填写”Smart Forms 处理起来就会很别扭。SFP 的优势正好落在这几个点上原生 PDF、视觉化布局、组件丰富。另一个很关键的区别是SFP 的方案适配 Adobe 生态交付给客户的 PDF 文件既专业又稳定这在很多客户验收时是加分项。1.2 两条建模路线Form Interface 与 Form without Interface进入事务码 SFP 后系统会问你要创建什么类型表单、样式、上下文或者 PDF 基础表单。创建 Adobe Form 时通常会遇到一个选择——带 Form Interface 还是不带 Form Interface。Form Interface 的含义就是你在表单里定义一个数据接口相当于把 ABAP 的结构声明映射到表单的 Context 上。后续 Driver Program 通过这个接口把业务数据塞进表单布局里的字段再绑定到 Context 对应的路径上。这种做法非常直观适合绝大多数业务单据。Form without Interface 则是表单自己维护数据不需要外部传参一般用于数据完全来自表单内部定义的场景。在我的实际项目中基本不会选用这条路线因为它的维护成本高外部调用时也不够灵活。尤其是当同一张表单要适配多个业务程序时不带接口的表单很难复用。如果业务上还需要 Web 端在线填单也别硬套 SFP。SFP 的强项是后端渲染 PDF真要做一个 HTML 表单或者录入页面直接选对应的前端表单引擎方案更合适。SAP GUI 里的 SFP 解决的是“输出打印 PDF”这条链路不是“在线编辑表单”那条链路。2. SFP 建模全流程从接口定义到 Layout 设计2.1 第一步规划数据结构与创建表单真正动手前先把数据结构想清楚。我通常先定义一个 ABAP 结构对应业务单据的抬头与行项目。以一个发货单示例结构大致长这样TYPES: BEGIN OF ts_item, posnr TYPE posnr, 行项目号 matnr TYPE matnr, 物料编号 werks TYPE werks_d, 工厂 menge TYPE menge_d, 数量 netwr TYPE netwr, 净价值 END OF ts_item. TYPES: tt_item TYPE STANDARD TABLE OF ts_item. TYPES: BEGIN OF ts_data, vbeln TYPE vbeln_vf, 发货单号 vkorg TYPE vkorg, 销售组织 kunnr TYPE kunnr, 客户编号 name1 TYPE name1, 客户名称 total TYPE netwr, 合计金额 items TYPE tt_item, 行项目内表 END OF ts_data.在 SE11 里把这个结构建成透明结构或者生成一个全局结构类型ZSSFP_DATA然后打开事务码 SFP创建表单。表单名字建议以 Z 开头比如ZSFP_DELIVERY。创建的时候系统会提示选择一个“接口/表格”这时候勾选“接口”并且把刚才的数据结构关联进去。这一步很容易忽略的地方在于很多人建完表单就直接去画 Layout结果发现左侧数据视图里根本没有字段可拖。原因就是 Form Interface 没有正确关联。这个接口一旦建立后续 Context 的结构会跟着变所以最好在画布局之前就确认清楚。2.2 理解 Context表单的数据源地图在 SFP 设计器里左侧常用的是“数据视图”和“设计视图”两种窗口。数据视图展示的就是 Context也就是表单可以引用的全部数据。Context 的根节点通常叫DATA或FORM下面挂抬头字段、行项目内表。设计视图则是画布局的区域相当于一张空白画布你可以从数据视图里把字段拖到画布上拖过去的同时系统会自动建立数据绑定。我见过不少同事在这块卡壳。他们以为把界面画好、字段拖上去就够了却忽略了一个核心逻辑SFP 的每个文本域、表格单元格必须通过绑定路径连接到 Context 上的具体字段生成的 PDF 才能拿到值。如果只是画了一个输入框没有绑定任何路径那最终输出就是空白的占位符。绑定路径长什么样一般会显示成$data.items.posnr这样的形式。设计器里拖拽时它会自动生成。手动写绑定路径也可以但容易写错大小写或层级建议优先用拖拽方式。2.3 第二步Layout 设计、页面设置与表格循环Layout 设计是工作量最大的环节。大概能分成四项页眉页脚、抬头区域、表格循环区域、合计区域。页眉页脚通常放在“主表单”的顶层。想固定打印公司 Logo 和页脚信息可以用“定位”方式放图片和文本这样每页都会出现。抬头区域一般用“流动”方式把客户名称、发货单号、日期这些字段按顺序排成一列或几列。这里要注意列宽对齐。我自己的习惯是把列宽设置成一样的数值避免打印出来参差不齐。行项目表则是核心。SFP 里做表格循环需要插入一个“表格空间”行属性里选择“重复子表单”然后把重复的数据源指向items[]。编译时系统会按内表的行数自动循环填充。做这一步时记得把行项目里的列宽固定下来别让某列的文字过长把整个表格撑破。页面大小和方向也要在这个阶段设置。找到主表单节点的“页面”属性把页面格式固定为 A4、纵向并把页边距设置成统一值。别小看这步后面合并 PDF 时页面大小不一致多半就是在这里埋的雷。还有一种情况某些子表单会继承父级页面方向导致个别页被切成横向最好把子表单的页面方向也显式指定。如果你接触过前端表单会发现 SFP 布局里的对齐问题和 Web 端框宽度对齐的逻辑很像。前一阵子有同事做 Ruoyi 前端下拉框、日期框和输入框宽度没统一被测试提了 bugSFP 里也是一样网格线、辅助线打开控件的宽度、间距尽量统一出来的 PDF 才专业。2.4 保存、预览与激活设计完成后点保存。SFP 的保存和普通 ABAP 对象不同它内部还有版本管理。保存之后建议先在 SFP 的预览功能里填充测试数据确认渲染效果。预览能发现约一半的问题字段没绑定、数据层级错位、表格列宽溢出基本都能在预览阶段看出来。确认无误后执行“激活”操作。注意激活之后系统中的表单函数模块才会更新。如果没有激活后面 Driver Program 调用时可能拿到旧版本。这一步也是我干活时反复强调的“先激活再调用”。3. Driver Program 调用三板斧完成 PDF 输出3.1 Driver Program 到底是什么角色Driver Program直译过来就是驱动报表程序。它更像是一个“取数 调用 输出”的外壳。表单只负责展示数据和排版不负责查询数据库也不负责输出到打印机或邮件。所有业务逻辑比如从哪个表取发货单、金额怎么算、传给表单的数据怎么组装全部写在 Driver Program 里。这种拆分非常实用。一套表单可以供多个程序调用每个程序用不同的取数逻辑同样一个程序也可以同时输出多套表单。逻辑和布局解耦后续需求变更时不必因为一个字段的取值规则而对 Layout 大动干戈。3.2 调用三重奏取函数名、设输出参数、调用生成函数很多 SFP 新手在这里会卡住。SAP 没有直接让你调用的“SFP 表单”你需要通过FP_FUNCTION_MODULE_NAME获取一个实际的函数模块名。第一步获取函数名DATA: lv_fm_name TYPE rs38l_fnam. CALL FUNCTION FP_FUNCTION_MODULE_NAME EXPORTING i_name ZSFP_DELIVERY IMPORTING e_funcname lv_fm_name.注意i_name传入的就是表单名。如果表单激活正常lv_fm_name会返回一个类似ZSFP_DELIVERY或者带了系统内部后缀的函数名。这个函数名你可以到 SE37 里查看它的接口里会包含表单 Form Interface 的定义。第二步设置输出参数。输出参数结构是sfpoutputparams重点字段是GETPDF置为ABAP_TRUE表示把 PDF 二进制数据返回给程序适合下载、邮件附件场景。NODIALOG置为ABAP_TRUE表示不弹出对话框适合后台运行。DEST打印机名称用于直接打印。REQNEW置为X表示生成新的输出请求。CONNECTION配置连接名称通常默认即可。第三步调用生成函数DATA: ls_data TYPE zssfp_data, ls_docparams TYPE sfpoutputparams, ls_result TYPE sfpoutputparams. ls_docparams-getpdf abap_true. ls_docparams-nodialog abap_true. CALL FUNCTION lv_fm_name EXPORTING is_data ls_data job_output_info ls_docparams IMPORTING job_output_info ls_result.这段代码里有两个关键点。第一is_data这个名字不是固定的它对应 Form Interface 的根节点名。如果你的接口根节点叫DATA生成函数里的导入参数通常是IS_DATA如果根节点叫其他名字参数名也会相应变化。最稳妥的方法是先到 SE37 看生成函数文档确认导入参数名再写调用代码。第二调用前最好把ls_data和ls_result清空。这个习惯很像前端表单提交前的“清空表单内容”逻辑后端也容易漏。SAP 内存里变量一旦被上次运行污染这次生成的 PDF 就会出现莫名数据残留。3.3 输出 PDF下载、邮件、直接打印拿到ls_result之后PDF 二进制数据实际上就在某个字段里。不同系统版本存放位置略有差异常见的是ls_result-pdf或者通过ls_result-docparams再取。我按照常见的ls_result-pdf来写示例。下载到本地用 ABAP 标准类转换并保存DATA: lt_solix TYPE solix_tab. CALL METHOD cl_bcs_convertxstring_to_solix EXPORTING iv_xstring ls_result-pdf IMPORTING et_solix lt_solix. CALL METHOD cl_gui_frontend_servicesgui_download EXPORTING bin_filesize xstrlen( ls_result-pdf ) filename C:\temp\delivery.pdf filetype BIN CHANGING data_tab lt_solix.如果是发邮件用 BCS 组件DATA: lo_send_request TYPE REF TO cl_bcs, lo_document TYPE REF TO cl_document_bcs. lo_send_request cl_bcscreate_persistent( ). lo_document cl_document_bcscreate_document( i_type PDF i_subject 发货单 i_hex ls_result-pdf ). lo_send_request-add_document( lo_document ).如果是直接打印不需要设置GETPDF改用ls_docparams-dest LP01. ls_docparams-reqnew X.调用生成函数后系统会直接把打印请求发到对应输出设备程序里不用再做额外处理。3.4 高级补充PDF_FORM_OPEN / WRITE / CLOSE普通业务用生成函数就够了。还有一种相对底层的调用方式是使用PDF_FORM_OPEN、PDF_FORM_WRITE、PDF_FORM_CLOSE三个函数。这种方式适合你要把 XML 数据直接喂给表单引擎或者需要一页一页写入、动态拼装复杂文档的场景。大致流程是CALL FUNCTION PDF_FORM_OPEN EXPORTING i_insert_docoutput ls_docparams IMPORTING e_job_output_info ls_job. CALL FUNCTION PDF_FORM_WRITE EXPORTING i_job_output_info ls_job i_xml lv_xml. CALL FUNCTION PDF_FORM_CLOSE EXPORTING i_job_output_info ls_job IMPORTING e_job_output_info ls_job.这种方式更接近“表单引擎”的底层 API灵活度更高但排查问题也更难。如果是第一次做 SFP我建议先把生成函数的常规调用跑通再考虑这种高级玩法。4. 常见问题与避坑指南4.1 改完表单调用程序还是输出旧版式这是我被问过最多的问题。症状是在 SFP 里改了布局保存也激活了但程序调用后生成的 PDF 没有变化。原因通常是旧函数模块缓存没刷新或者表单的版本时间没对上。我的排查路径是先在 SFP 打开表单确认“激活”或“生成”是否已经执行再到 SE37 里看对应的函数模块最近修改时间。如果函数模块时间没更新说明激活动作没有触发函数模块重建。此时可以重新执行一次“生成”必要时在对象列表里删除旧的函数模块再重新生成。生产机上做这个操作要谨慎最好走传输请求别直接在开发机测试后手动改。4.2 中文字体变成方框或者乱码SAP 后端渲染 Adobe PDF 依赖 ADSAdobe Document Services组件而 ADS 渲染时又依赖服务器上安装的字体。开发机经常表现正常因为开发机的 ADS 环境装了可用字体生产服务器如果缺少对应中文字体PDF 里中文就会变成方框。解决办法分两步。第一步确认服务器字体目录下有可用的中文字体文件。第二步在 SFP 布局里检查是否指定了特殊字体。如果项目里用的都是标准中文字体但生产机还是乱码很可能是字体文件没放对位置需要 BASIS 同事协助把字体部署到 ADS 对应的字体目录。经验之谈别在表单里使用生僻的自定义字体尤其不要用 Adobe 的某个特定版本字体。项目上线后客户一旦换一台没有该字体的电脑预览整个 PDF 版式就废了。4.3 生成的 PDF 是空白页但字段已经画上去了这种问题十有八九出在 Context 绑定上。你在布局里放了文本但它没有绑定到 Context 的字段上或者绑定的路径写错了大小写或者 Form Interface 的数据结构没有正确传进调用程序。排查步骤很简单。先在 SFP 预览里输入测试数据看效果。如果预览正常说明问题出在 Driver Program 传参上如果预览就是空白的说明 Layout 绑定出了问题。这里有个小技巧在 Driver Program 里临时写一段代码把ls_data的内容展开到日志里确认字段值真的传到了调用函数。很多时候你以为填了数据其实内表是空的字段自然没有输出。4.4 合并 PDF 后页面大小不一样这是最近客户反馈很典型的一个坑。他们用 Adobe 或其他工具把多个单据合并成一个 PDF结果发现不同单据页面大小参差不齐有的 A4 纵向有的 Letter 横向还伴有多余白边。这里有双重原因。一方面是 SFP 源表单没有统一定义页面大小。前面在 2.3 提到每个表单都要把页面格式固定为 A4 或客户要求的统一格式不要留着默认的 Letter。另一方面是合并工具本身有些 PDF 合并工具提供了“统一页面大小”的选项一旦勾选它会强制把每一页改成相同尺寸导致内容被拉伸或留白。正确做法是SFP 源头统一定义页面属性和方向合并时选择“保留原始页面大小”。如果你已经被 Adobe 合并成了“大小不一样”的效果可以在“组织页面”里手动裁剪页面框但这是补救措施不是长期方案。真正治本的办法还是锁定每个表单的页面规格。4.5 大批量输出时内存溢出或者速度很慢SFP 渲染 PDF 本身就是后端 CPU 密集操作。如果一个 Driver Program 里循环几百份单据每份都生成 PDF 并保存在内表里应用服务器内存很快就会被顶满。我的做法是大批量输出走后台 Job通过输出请求方式交给后台处理如果必须在前台交互也建议每处理完一份就释放对应变量不要把所有 PDF 的 XSTRING 累积到一个内表里。如果客户需求是“所有单据合并成一个 PDF 发邮件”内存占用会更高。建议先用cl_bcs_convertxstring_to_solix把每个 PDF 转成二进制大对象流式写入或者用分段合并的方式不要一次性把 100 个 PDF 的原始内容都堆在同一个变量里。5. 实战心得这套流程还能怎么玩5.1 通用输出工具化把“取函数名 设输出 下载/打印”这套逻辑封装成一个公共方法是我在项目里做的最有价值的一件事。业务程序只需要传入表单名和数据结构剩下的调用细节全部由公共方法处理。后续增加下载路径规范、打印设备调整只需要改一处不用每个报表程序各自维护。我在一个项目里用这套思路把销售订单确认函、发货单、标签打印三套表单全部收敛到同一个输出模块里新增业务单据输出时工作量从半天缩短到半小时。5.2 往交互式表单和自动发送方向扩展SFP 表单不只是静态 PDF。如果业务需要可以在表单里设计可填写域、数字签名域生成后的 PDF 打开时就允许收件人填写并签名。这适合审批场景比如客户确认函、质检报告生成后发给对方对方填写盖章再返回。配合邮件网关和后台 Job甚至可以做到系统自动触发、自动发送整个流程完全无人值守。当然这套扩展也意味着项目复杂度会上升尤其是签名域、校验规则的配置。我的建议是先把标准打印链路跑稳再逐步往交互和自动化方向加功能否则问题叠加在一起排查起来会很痛苦。根据我的个人经验SFP 最大的坑从不在 Layout 设计本身而是整个链路依赖的环境ADS 服务是否正常、字体是否部署、函数模块是否重新生成、Form Interface 参数是否匹配。你只要把这几个链路节点逐一确认SFP 开发其实比 Smart Forms 还要顺手。希望这篇文章能帮你少走几步弯路。