EBS 个性化实战:用 Custom.pll 修改标准 Form 的 LOV 并接入 TaoToken

发布时间:2026/10/7 14:35:41
EBS 个性化实战:用 Custom.pll 修改标准 Form 的 LOV 并接入 TaoToken 1. EBS 标准 Form 的 LOV 为什么总差那么一点做 Oracle EBS 二次开发的朋友大概率都遇到过这种场景采购订单界面上的供应商 LOV业务方希望默认只显示某个地区、某个 OU 下、或者某个启用状态的供应商而不是把PO_VENDORS里所有 enabled 的供应商一股脑全列出来。标准 Form 的 LOV 是 Oracle 预置的你不可能去改POXPOEPO.fmb然后重新编译——那等于把标准产品给动了打补丁、升级、克隆环境全都会出问题。EBS 官方给的扩展点就是CUSTOM.pll。它挂在FND的CUSTOM包里标准 Form 在运行时会调用custom.event、custom.zoom这些入口你只要在里面写逻辑就能在不动标准 Form 源码的前提下把 LOV 的 Record Group 换成自己的查询。这就是所谓的「Form 个性化」中最经典的一类需求改 LOV 的查询 SQL、动态过滤、控制返回列。我这次要处理的场景很具体采购订单POXPOEPO的PO_HEADERS.VENDOR_NAME字段业务要求 LOV 只显示名称以「東莞」开头的供应商。原始 LOV 的 SQL 大致是这样来自PO_VENDORS和PO_SUPPLIER_SITES_VAL_V的关联select distinct pov.vendor_name, pov.segment1, decode(pov.hold_flag,Y,*,null), pov.hold_flag, pov.vendor_id, pov.num_1099, pov.vat_registration_num from po_vendors pov, po_supplier_sites_val_v pssv WHERE pov.enabled_flag Y and sysdate between nvl(pov.start_date_active, sysdate-1) and nvl(pov.end_date_active, sysdate1) and pov.vendor_id pssv.vendor_id and nvl(pssv.rfq_only_site_flag, N) N and (:po_headers.approved_date is null or (:po_headers.approved_date is not null and (pssv.invoice_currency_code :po_headers.currency_code or pssv.invoice_currency_code is null))) order by upper(pov.vendor_name)注意里面有两个绑定变量:po_headers.approved_date和:po_headers.currency_code。这两个东西在CUSTOM.pll里用create_group_from_query是传不进去的——Record Group 的查询字符串是纯文本绑定变量在创建时无法解析会直接报错。所以实战里通常的做法是把这两个条件删掉或者用name_in把当前值拼进字符串。我这次选择删掉因为业务只关心名称前缀过滤币种和审批日期不是硬约束。除了改 SQL还有一个更隐蔽的坑Record Group 不能重复创建。如果你在WHEN-NEW-FORM-INSTANCE里每次都create_group_from_query第二次进这个 Form 就会报ORA-06502或者 group already exists 之类的错。所以必须先find_group判断存在就delete_group再重建。再往上一层是「接入统一通道」的问题。很多团队现在会把 EBS 里需要调用外部 AI 能力的地方比如智能补全供应商名称、自动分类、单据摘要统一走一个 API 网关而不是每个 Form 各写各的 HTTP 请求。TaoToken 就是这样一个统一 Key/API 通道Base URL 是https://taotoken.net/api你可以在CUSTOM.pll里用UTL_HTTP或FND_REQUEST把请求发过去Key 和模型 ID 集中管理。这样 LOV 的过滤逻辑可以本地做但涉及语义匹配、模糊推荐的部分就能交给模型。这篇就按「先改 LOV 查询 → 再挂触发器 → 再编译部署 → 最后验证」的顺序走一遍中间把CUSTOM.pll的完整代码、Form 触发器挂载位置、常见报错都写清楚。适合已经在做 EBS 二次开发、手上有CUSTOM.pll编译权限、能进 Form Builder 的读者。2. TaoToken 前置把统一 Key 和 API 通道准备好在动CUSTOM.pll之前先把外部通道这块理清楚。EBS 里做 LOV 定制纯 SQL 过滤其实不需要联网但如果你想让 LOV 支持「按语义推荐供应商」「根据历史单据智能排序」这类能力就需要在CUSTOM.pll里发 HTTP 请求。这时候统一走 TaoToken 的好处是Key 只存一份、模型 ID 只配一次、以后换模型不用改每个 Form。TaoToken 的 API 入口是https://taotoken.net/api官网是https://taotoken.net/。你需要先在控制台创建一个 API Key然后拿到你要用的 Model ID。整个流程分三步拿 Key、选模型、记下 Base URL。第一步进控制台创建 Key。打开https://taotoken.net/console登录后进 API Keys 页面新建一个 Key。建议按环境命名比如ebs-dev、ebs-prod方便以后轮换。Key 只在创建时显示一次复制下来存到安全的地方别直接硬编码进CUSTOM.pll的源码里——生产环境建议放FND_PROFILE或者一张自定义配置表运行时用fnd_profile.value读出来。第二步确认模型 ID。进模型对话页面https://taotoken.net/chat可以先试一下你要用的模型确认返回正常。模型 ID 是大小写敏感的字符串比如claude-sonnet-4-5这类具体以控制台里列出的为准。这个 ID 后面要写进请求体。第三步记下 Base URL。所有请求都发到https://taotoken.net/api具体路径按接口文档来。接入文档在https://taotoken.net/doc里面有完整的 endpoint 列表、请求头格式、返回结构。EBS 里用UTL_HTTP发请求时注意Content-Type: application/json和Authorization: Bearer 你的Key这两个头必须带。如果你后面要做的是长期编码、Agent 类的任务比如让模型帮你批量生成 LOV 查询、自动改写 Record Group SQL可以看下 Coding Planhttps://taotoken.net/coding-plan。它更适合持续性的开发场景而不是单次调用。这里要强调一点TaoToken 是统一 API 通道不是让你在 EBS 里绕过什么。它的定位是「一个 Key 管多个模型、一个 Base URL 管多个请求」方便你在CUSTOM.pll里集中配置。EBS 本身还是跑在你的数据库和应用服务器上LOV 的 Record Group 还是由 Form 运行时创建两者不冲突。配置这块建议在CUSTOM.pll里定义一个常量包或者用 Profile-- 建议放在自定义配置表或 Profile 里不要硬编码 -- FND_PROFILE.OPTION: CUX_TAOTOKEN_BASE_URL https://taotoken.net/api -- FND_PROFILE.OPTION: CUX_TAOTOKEN_API_KEY 你的Key -- FND_PROFILE.OPTION: CUX_TAOTOKEN_MODEL_ID 你的Model ID这样CUSTOM.pll里只写读取逻辑Key 轮换时改 Profile 就行不用重新编译 PLL。实测下来这一步能省掉后面很多「改一次 Key 编译一次」的麻烦。3. 可复制配置Custom.pll 完整代码与 Form 触发器挂载这一节是核心。先给完整的CUSTOM.pll代码再讲 Form 触发器挂在哪、Record Group 怎么建、绑定变量怎么处理。3.1 Custom.pll 完整代码PACKAGE BODY custom IS -- 供应商 LOV 定制只显示名称以「東莞」开头的供应商 PROCEDURE set_po_vendor_lov IS l_query_string VARCHAR2(4000); l_supplier_group_id RECORDGROUP; BEGIN -- 只在光标位于 PO_HEADERS.VENDOR_NAME 时生效 IF name_in(system.cursor_item) PO_HEADERS.VENDOR_NAME THEN -- 原始 SQL 中的单引号需要转义为 这里用拼接方式构造 -- 注意:po_headers.approved_date 和 :po_headers.currency_code -- 是绑定变量无法在 create_group_from_query 中解析已删除 l_query_string : select distinct pov.vendor_name, pov.segment1, || decode(pov.hold_flag, || || Y || || , || || * || || ,null), pov.hold_flag, || pov.vendor_id, pov.num_1099, pov.vat_registration_num || from po_vendors pov, po_supplier_sites_val_v pssv || WHERE pov.enabled_flag || || Y || || and sysdate between nvl(pov.start_date_active, sysdate-1) || and nvl(pov.end_date_active, sysdate1) || and pov.vendor_id pssv.vendor_id || and nvl(pssv.rfq_only_site_flag, || || N || || ) || || N || || and pov.vendor_name like || || 東莞% || || order by upper(pov.vendor_name); fnd_message.debug(l_query_string: || l_query_string); -- Record Group 不能重复创建先判断是否存在存在则删除 IF NOT id_null(find_group(CUX_SUPPLIER_NAME)) THEN delete_group(CUX_SUPPLIER_NAME); END IF; l_supplier_group_id : create_group_from_query( CUX_SUPPLIER_NAME, l_query_string); -- 把标准 LOV 的 Record Group 替换成自定义的 set_lov_property(SUPPLIER_NAME, GROUP_NAME, CUX_SUPPLIER_NAME); END IF; END set_po_vendor_lov; -- 事件入口标准 Form 会调用 custom.event PROCEDURE event(event_name VARCHAR2) IS form_name VARCHAR2(30) : name_in(system.current_form); block_name VARCHAR2(30) : name_in(system.cursor_block); BEGIN IF event_name WHEN-NEW-FORM-INSTANCE THEN -- 注意system.cursor_item 在 Form 刚打开时可能为空 -- 所以 item 判断放在 set_po_vendor_lov 内部 IF form_name POXPOEPO AND block_name PO_HEADERS THEN set_po_vendor_lov; END IF; END IF; END event; END custom;几个关键点解释一下。第一name_in(system.cursor_item)的判断放在set_po_vendor_lov内部而不是event里。原因是某些 Form比如应用开发员的功能 Form在WHEN-NEW-FORM-INSTANCE触发时system.cursor_item还是空的如果在外层判断会直接no data found。放里面更安全。第二create_group_from_query的第二个参数是完整 SQL 字符串里面的单引号必须用转义。代码里用拼接是因为 PL/SQL 字符串本身也要转义实际生成的 SQL 里就是正常的单引号。第三set_lov_property(SUPPLIER_NAME, GROUP_NAME, CUX_SUPPLIER_NAME)里的SUPPLIER_NAME是标准 Form 里 LOV 的名字GROUP_NAME是属性名CUX_SUPPLIER_NAME是你新建的 Record Group 名。这三个名字必须和 Form 里实际的一致否则 LOV 不会变。第四绑定变量:po_headers.approved_date和:po_headers.currency_code被删掉了。如果你确实需要这两个条件得用name_in(PO_HEADERS.APPROVED_DATE)读出值再拼进字符串但要注意日期格式和 NULL 处理容易出错。我这次业务不需要直接删。3.2 Form 触发器挂载位置CUSTOM.pll本身不需要在 Form 里挂触发器——它是通过FND的CUSTOM包被标准 Form 自动调用的。但你要确认两件事一是CUSTOM.pll已经编译进AU_TOP/resource或者$AU_TOP/forms/US对应的路径并且FORMS60_PATH能找到它。二是标准 FormPOXPOEPO.fmb里确实有调用custom.event的触发器。Oracle 标准 Form 一般在WHEN-NEW-FORM-INSTANCE里会调custom.event(WHEN-NEW-FORM-INSTANCE)你可以在 Form Builder 里打开POXPOEPO.fmb确认一下但不要改它。如果你要挂的是WHEN-NEW-ITEM-INSTANCE或者ZOOM事件就在CUSTOM.pll的event过程里加对应的IF event_name ...分支。比如 LOV 的ZOOM逻辑IF event_name ZOOM THEN IF name_in(system.cursor_item) PO_HEADERS.VENDOR_NAME THEN -- 自定义 zoom 行为 NULL; END IF; END IF;3.3 接入 TaoToken 的配置片段如果你要在 LOV 里加语义推荐可以在CUSTOM.pll里加一个调用 TaoToken 的过程。配置用 JSON 形式管理{ base_url: https://taotoken.net/api, api_key_profile: CUX_TAOTOKEN_API_KEY, model_id: claude-sonnet-4-5, timeout_seconds: 30, endpoints: { chat: /v1/chat/completions, models: /v1/models } }对应的 PL/SQL 读取逻辑FUNCTION get_taotoken_config RETURN VARCHAR2 IS l_base_url VARCHAR2(200); l_api_key VARCHAR2(200); l_model_id VARCHAR2(100); BEGIN l_base_url : fnd_profile.value(CUX_TAOTOKEN_BASE_URL); l_api_key : fnd_profile.value(CUX_TAOTOKEN_API_KEY); l_model_id : fnd_profile.value(CUX_TAOTOKEN_MODEL_ID); RETURN l_base_url || | || l_api_key || | || l_model_id; END get_taotoken_config;这样 Key 和 Model ID 都在 Profile 里CUSTOM.pll只负责读。Base URL 固定是https://taotoken.net/api不要加 UTM 参数到 API 请求里UTM 只用于官网跳转。4. 验证请求编译部署后打开标准 Form 看 LOV 结果代码写完接下来是编译和部署。这一步做错前面全白搭。4.1 编译 CUSTOM.pll在 EBS 应用服务器上用f60gen或者frmcmp_batch编译 PLL。典型命令cd $AU_TOP/forms/US frmcmp_batch moduleCUSTOM.pll useridapps/password \ output_file$AU_TOP/resource/CUSTOM.plx \ module_typeLIBRARY compile_allspecial编译成功后CUSTOM.plx会生成在$AU_TOP/resource下。确认一下文件时间戳是新的。如果报PLS-00201: identifier FND_MESSAGE must be declared说明FND相关的包没找到检查FORMS60_PATH是否包含$AU_TOP/resource和$FND_TOP/resource。4.2 部署到应用服务器把CUSTOM.plx复制到所有应用服务器节点的$AU_TOP/resource下。如果你有多个 middle tier每个都要放。然后重启 Apache 或者清 Form 缓存cd $ADMIN_SCRIPTS_HOME adapcctl.sh stop adapcctl.sh start有些环境还需要清_pages缓存具体看你的 EBS 版本。R12.2 的话用adop或者直接重启 managed server。4.3 打开标准 Form 验证登录 EBS进采购订单界面POXPOEPO光标点到VENDOR_NAME字段点 LOV 按钮。正常情况下弹出的 LOV 里应该只显示名称以「東莞」开头的供应商。验证要点第一LOV 列表的列是否和原始一致。原始 LOV 有vendor_name、segment1、hold_flag标记、vendor_id、num_1099、vat_registration_num这些列。你的自定义 SQL 里select的列顺序和数量必须和 LOV 定义的列匹配否则会报ORA-06502或者列错位。第二过滤是否生效。如果还是显示全部供应商说明set_lov_property没生效检查 LOV 名字SUPPLIER_NAME是否写对Record Group 名CUX_SUPPLIER_NAME是否和create_group_from_query里一致。第三fnd_message.debug的输出。这个只在FND_DEBUG开启时显示可以在Help Diagnostics Examine里看或者查FND_LOG_MESSAGES表。确认l_query_string拼出来的 SQL 是合法的可以直接复制到 SQL*Plus 里跑一下。4.4 验证 TaoToken 请求如果接了如果你在 LOV 里加了语义推荐用UTL_HTTP发请求测试DECLARE l_req UTL_HTTP.req; l_resp UTL_HTTP.resp; l_url VARCHAR2(500) : https://taotoken.net/api/v1/chat/completions; l_body VARCHAR2(4000); BEGIN UTL_HTTP.set_transfer_timeout(30); l_req : UTL_HTTP.begin_request(l_url, POST, HTTP/1.1); UTL_HTTP.set_header(l_req, Content-Type, application/json); UTL_HTTP.set_header(l_req, Authorization, Bearer || fnd_profile.value(CUX_TAOTOKEN_API_KEY)); l_body : {model: || fnd_profile.value(CUX_TAOTOKEN_MODEL_ID) || ,messages:[{role:user,content:test}]}; UTL_HTTP.set_header(l_req, Content-Length, LENGTH(l_body)); UTL_HTTP.write_text(l_req, l_body); l_resp : UTL_HTTP.get_response(l_req); DBMS_OUTPUT.put_line(Status: || l_resp.status_code); UTL_HTTP.end_response(l_resp); EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.put_line(Error: || SQLERRM); END;如果返回 200说明 Key 和 Base URL 都对。如果返回 401检查 Key 是否复制完整、有没有多余空格。如果返回 404检查 endpoint 路径是不是/v1/chat/completions具体以接入文档https://taotoken.net/doc为准。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节把实际会撞到的报错列出来对照着查。报错一ORA-20001: local proxy failed或UTL_HTTP请求超时这个通常不是 TaoToken 的问题而是 EBS 应用服务器到外网的网络策略。EBS 的UTL_HTTP默认走数据库服务器网络如果数据库服务器没有出网权限请求会直接失败。解决办法是在数据库层面配置UTL_HTTP.SET_PROXY或者用FND_REQUEST走并发管理器。注意这里说的是企业内网代理配置不是让你做任何绕过网络策略的操作具体要问你们的网络管理员。报错二HTTP 401 UnauthorizedKey 不对。检查三件事Key 是否完整复制有没有漏字符、Authorization头格式是不是Bearer KeyBearer 后面有一个空格、Key 是否已过期或被禁用。去控制台https://taotoken.net/api-keys确认 Key 状态。如果 Key 放在 Profile 里用fnd_profile.value读出来打印一下确认没有前后空格。报错三Error reading choices或 LOV 打开报FRM-40202这是 Form 层面的错通常是 Record Group 的 SQL 有问题。可能原因SQL 语法错误、列数不匹配、Record Group 名重复。先在 SQL*Plus 里把l_query_string拼出来的 SQL 跑一遍确认能返回数据。然后检查create_group_from_query的列数和 LOV 定义的列数是否一致。如果 Record Group 已存在没删掉也会报这个错确认find_group和delete_group逻辑生效。报错四OAuth相关错误或invalid_grant如果你用的是需要 OAuth 的接口检查 token 是否过期、scope 是否正确。TaoToken 的 API Key 方式是 Bearer Token不需要走 OAuth 授权码流程。如果你在CUSTOM.pll里误用了 OAuth 的 endpoint会报这个错。确认你调的是/v1/chat/completions这类 API Key 认证的接口不是 OAuth 授权接口。报错五FRM-40735: WHEN-NEW-FORM-INSTANCE trigger raised unhandled exception ORA-06502这是CUSTOM.pll里的异常没被捕获。最常见的是name_in(system.cursor_item)返回 NULL然后拿去做字符串比较。解决办法是在set_po_vendor_lov开头加IF name_in(system.cursor_item) IS NULL THEN RETURN; END IF;。另外create_group_from_query如果 SQL 太长超过VARCHAR2(4000)也会报这个把l_query_string声明成VARCHAR2(4000)并确认 SQL 没超长。报错六LOV 打开了但过滤没生效检查set_lov_property的 LOV 名字。SUPPLIER_NAME是标准 Form 里 LOV 的Name属性不是Title。在 Form Builder 里打开POXPOEPO.fmb找到PO_HEADERS.VENDOR_NAME的 LOV看它的Name到底是什么。有些版本叫SUPPLIER_NAME有些叫VENDOR_NAME_LOV写错了就不生效。报错七编译 PLL 时报PLS-00302: component SET_LOV_PROPERTY must be declaredSET_LOV_PROPERTY是FND_LOV或者APP_LOV包里的过程确认你的CUSTOM.pll里ATTACH了正确的库。标准做法是在CUSTOM.pll顶部ATTACH FND和ATTACH APP_standard。如果还报错检查FORMS60_PATH是否包含$FND_TOP/forms/US。排查顺序建议先看fnd_message.debug输出的 SQL 能不能在 SQL*Plus 跑通再看 Record Group 创建是否成功最后看 LOV 属性是否替换。三步都过了LOV 一定生效。6. 继续往下走把 LOV 定制和统一通道用顺走到这里CUSTOM.pll改标准 Form LOV 的完整链路已经跑通了从原始 SQL 分析、绑定变量处理、Record Group 重建、LOV 属性替换到编译部署、打开 Form 验证。核心就三件事——SQL 要能独立跑通、Record Group 要先删后建、LOV 名字要对得上。如果你后面还要接 TaoToken 做语义推荐或者智能补全记住 Key 和 Model ID 放 ProfileBase URL 固定https://taotoken.net/api请求头带Authorization: Bearer Key。接入文档在https://taotoken.net/doc模型对话在https://taotoken.net/chatAPI Keys 管理在https://taotoken.net/api-keys。长期做编码和 Agent 任务的话Coding Plan 在https://taotoken.net/coding-plan。最后留一个实用技巧CUSTOM.pll里所有fnd_message.debug在生产环境记得关掉或者用fnd_profile.value(FND_DEBUG)控制不然日志表会被刷爆。另外每次改完 PLL一定要在测试环境先编译验证再推到生产——EBS 的 Form 缓存有时候很顽固重启 Apache 不一定够必要时清_pages或者重启 managed server。