目的
![图片[1]-金蝶云获取库存服务如何设置获取指定仓库的库存-Dgcity](https://dgcity.com/wp-content/uploads/2026/08/20260827081856286-image.png)
![图片[2]-金蝶云获取库存服务如何设置获取指定仓库的库存-Dgcity](https://dgcity.com/wp-content/uploads/2026/08/20260827082338532-image-1024x535.png)
开发文档
金蝶云星空 · 获取库存按仓库白名单(允许获取库存)二次开发 — 详细教程
版本:金蝶云星空 9.1.728.3(其它版本 API 命名空间可能不同,本文按本机实测修正)
目标:在仓库基础资料加一个“允许获取库存”复选框;修改获取库存服务,让点击“获取库存”时,只返回被勾选仓库的库存数量(白名单)。
一、需求背景与目标
业务希望在“获取库存”时,只把允许获取库存的仓库纳入结果,屏蔽掉不允许的仓库。实现分两层:
- 数据层(本文核心,代码实现):拦截“获取库存”取数,过滤掉未勾选的仓库。
- 展示层(极易被忽略,BOS 配置):获取库存把库存数量写回单据字段(如 FInventoryQty)时,受“获取库存操作”的匹配维度控制。配错会导致字段始终为空——本文第 9 步专门讲。
最终效果:
- 仓库 A 勾选、仓库 B 不勾选 → 获取库存结果里只有 A 的库存;
- 支持任意个仓库勾选;可显示单仓数量,也可显示多勾选仓的合计(见第 9 步)。
二、适用环境与前置确认
| 项 | 值(本机实测) |
|---|---|
| 金蝶版本 | 9.1.728.3 |
| 运行时 | .NET Framework 4.8 |
| 数据库 | SQL Server(AIS20260604094951) |
| 站点 Bin | C:\Program Files (x86)\Kingdee\K3Cloud\WebSite\Bin |
| 开发工具 | Visual Studio / dotnet CLI(net48 类库) |
前置确认:
- 已安装 .NET Framework 4.8 开发者包(能编译 net48)。
- 能访问金蝶
WebSite\Bin下的Kingdee.*.dll(编译时需要引用)。 - 有 BOS 设计器权限(加字段、注册插件、改匹配维度)。
三、关键认知:为什么“字段为空”往往不是代码问题
这是本次开发最关键的教训,先讲清楚,避免你重蹈覆辙。
金蝶“获取库存(QueryStock)”写回单据字段的流程是:
- 服务按物料取出即时库存;
- 按操作上配置的“匹配维度”(仓库 / 库存状态 / 辅助属性 / 批号 …)把库存与分录匹配;
- 把匹配上的库存数量经字段映射写回单据字段(如
FInventoryQty当前库存)。
所以:
- 插件只决定“哪些仓库能进结果”;
- “结果怎么汇总到字段”由匹配维度 + 字段映射决定。
我们实测踩到的两个坑:
- 坑 A:基础生成的取数 SQL 没有 WHERE 子句,如果在代码里直接追加
AND EXISTS(...),SQL 会以AND开头 → 整条语法错误 → 取数为空。必须按“有无 WHERE”自适应,见第 6 步。 - 坑 B(最隐蔽):若“按库存状态匹配”开着,而不同仓库的库存状态不同(如不良品仓状态=10257、良品仓=10000,分录只填一个状态),状态对不上 → 字段为空。解决:关掉“按库存状态匹配”(第 9 步)。
- 坑 C:注册位置错了(注册到菜单按钮的点击事件插件,而不是获取库存服务操作的插件),钩子根本不会被调用(第 8 步)。
四、总体方案(双保险)
继承 AbstractGetInvStockPlugIn,覆盖三个钩子:
- SQL 层过滤(主用)
RegexGetInvDataSql/RegexGetInvSumDataSql
在生成 SQL 末尾追加:... WHERE EXISTS (SELECT 1 FROM T_BD_STOCK ts WHERE ts.FSTOCKID = t.FSTOCKID AND ISNULL(ts.FALLOWGETINVSTOCK,'1')='1')
t是获取库存生成 SQL 里库存表的别名(实测确认:from t_stk_inventory t ...)。
- 内存层兜底(可靠)
AfterGetAllData(DataTable, List<STK_Inventory>)
直接剔除 DataTable / 列表中未勾选仓库的行。该钩子拿到的dtbInvs是已物化的内存表,稳定可靠。 ApplyExtFilter兼容性兜底保留(在本单据的填充路径上它实际收到的枚举为空,不参与字段填充;保留不影响业务)。
语义:FALLOWGETINVSTOCK='1'(勾选)→ 允许;'0' → 排除;NULL(历史仓库)→ 默认允许(安全默认,避免一刀切把所有仓库屏蔽)。
五、步骤一:仓库基础资料增加字段(BOS)
- 打开 BOS 设计器,登录对应数据中心。
- 打开 仓库(BD_STOCK) 业务对象(在【基础资料】→【库存管理】下)。
- 在左侧实体字段区新增字段:
- 字段类型:复选框(布尔)
- 字段标识(属性名):
FAllowGetInvStock⚠️ 必须与插件查询的列名完全一致(大小写无所谓,SQL Server 排序规则通常不区分,但建议照抄) - 名称:允许获取库存
- 默认值:True(即默认允许,安全)
- 把该字段拖到表单的 基本信息 → 控制 分组(按你的表单布局摆放即可)。
- 保存 → 发布。发布后系统会自动在
T_BD_STOCK表生成列FALLOWGETINVSTOCK(char(1),存'1'/'0')。 - (可选)历史仓库若需明确为允许,执行本文附录 SQL 的回填语句,把存量数据置
'1'(默认已允许,可不执行)。
⚠️ 不要用手动建列 SQL 替代 BOS:手动建列后若再 BOS 发布会冲突。规范做法就是走 BOS。
六、步骤二:编写插件(完整源码 + 讲解)
6.1 新建 net48 类库工程
- 工程名:
CustomPlugIn,目标框架.NET Framework 4.8,输出类型类库。 - 引用金蝶 Bin 下的:
Kingdee.BOS.dll、Kingdee.BOS.App.dll、Kingdee.K3.SCM.App.Core.dll、Kingdee.K3.SCM.Common.BusinessEntity.dll、Kingdee.K3.Core.dll等(把 Bin 目录加为程序集引用即可)。
6.2 完整源码(AllowGetInvStockPlugIn.cs)
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Kingdee.BOS; // Context, SqlParam
using Kingdee.BOS.App.Data; // DBUtils
using Kingdee.K3.SCM.App.Core; // AbstractGetInvStockPlugIn
using Kingdee.K3.SCM.Common.BusinessEntity.STK; // STK_Inventory
using Kingdee.K3.Core.SCM.Args; // GetInvStockDetailArg
namespace CustomPlugIn.Stock
{
/// <summary>
/// 获取库存白名单插件(最终版,无诊断日志)
/// 需求:仓库“允许获取库存(FALLOWGETINVSTOCK)”勾选才返回其库存。
/// 机制:SQL 层 WHERE EXISTS 过滤(主) + AfterGetAllData 内存剔除(备)。
/// </summary>
public class AllowGetInvStockPlugIn : AbstractGetInvStockPlugIn
{
private const string STOCK_TABLE = "T_BD_STOCK";
private const string FIELD_ALLOW = "FALLOWGETINVSTOCK";
private HashSet<long> _allowed;
private HashSet<long> AllowedStocks
{
get
{
if (_allowed == null) _allowed = LoadAllowedStocks();
return _allowed;
}
}
// 读取“允许获取库存”的仓库内码集合
private HashSet<long> LoadAllowedStocks()
{
var allowed = new HashSet<long>();
string sql = string.Format(
"SELECT FSTOCKID FROM {0} WHERE ISNULL({1},'1')='1'", STOCK_TABLE, FIELD_ALLOW);
try
{
DataSet ds = DBUtils.ExecuteDataSet(this.Ctx, sql);
if (ds != null && ds.Tables.Count > 0)
{
foreach (DataRow r in ds.Tables[0].Rows)
{
object v = r["FSTOCKID"];
if (v != null && v != DBNull.Value) allowed.Add(Convert.ToInt64(v));
}
}
}
catch
{
// 异常时返回空集合:等价于不排除任何仓库,保证不阻断正常业务
}
return allowed;
}
// 在生成 SQL 上追加白名单过滤(WHERE / AND 自适应,避免无 WHERE 时以 AND 开头导致语法错误)
private string AppendAllowFilter(string sql)
{
if (string.IsNullOrEmpty(sql)) return sql;
const string cond =
" EXISTS (SELECT 1 FROM " + STOCK_TABLE +
" ts WHERE ts.FSTOCKID = t.FSTOCKID AND ISNULL(ts." + FIELD_ALLOW + ",'1')='1') ";
int whereIdx = sql.LastIndexOf(" WHERE ", StringComparison.OrdinalIgnoreCase);
sql = whereIdx >= 0 ? sql + " AND " + cond : sql + " WHERE " + cond;
return sql;
}
public override string RegexGetInvDataSql(bool usePLNReserve, string sql, List<SqlParam> paras)
{
return AppendAllowFilter(sql);
}
public override string RegexGetInvSumDataSql(bool usePLNReserve, string sql, List<SqlParam> paras)
{
return AppendAllowFilter(sql);
}
// 内存双保险:剔除 DataTable 与 STK_Inventory 列表中未勾选仓库
public override void AfterGetAllData(DataTable dtbInvs, List<STK_Inventory> invDatas)
{
var allowed = AllowedStocks;
if (dtbInvs != null && dtbInvs.Columns.Contains("FStockId"))
{
for (int i = dtbInvs.Rows.Count - 1; i >= 0; i--)
{
object v = dtbInvs.Rows[i]["FStockId"];
long sid = (v == null || v == DBNull.Value) ? 0 : Convert.ToInt64(v);
if (!allowed.Contains(sid)) dtbInvs.Rows.RemoveAt(i);
}
}
if (invDatas != null)
{
invDatas.RemoveAll(x => x == null || !allowed.Contains(x.StockId));
}
}
public override IEnumerable<STK_Inventory> ApplyExtFilter(IEnumerable<STK_Inventory> data, GetInvStockDetailArg item)
{
if (data == null) return data;
var allowed = AllowedStocks;
return data.Where(inv => inv != null && allowed.Contains(inv.StockId)).ToList();
}
}
}
6.3 逐段讲解 / 版本坑提醒
- 命名空间差异:本机 9.1.728.3 与网上参考文档不同(见第 11 步对照表),照抄会编译失败。上面已按本机修正。
t.FSTOCKID:获取库存生成 SQL 中库存表别名就是t(实测:from t_stk_inventory t ...),可直接用。- WHERE/AND 自适应:
AppendAllowFilter先找 SQL 里有没有WHERE;没有就加WHERE EXISTS(...),有就加AND EXISTS(...)。这一步是本次排错的关键——最初失败正是因为基础 SQL 无 WHERE,旧代码直接拼AND ...导致整条 SQL 报错、取数为空。 ISNULL(...,'1')='1':历史仓库该字段为 NULL 时默认“允许”,避免误伤全部仓库;如需“严格白名单”(历史也必须勾选才允许),把两处ISNULL(...,'1')改成直接='1'即可。
七、步骤三:编译与部署
- 编译 Release:
cd InvStockFilter
dotnet build -c Release
产物:bin\Release\net48\CustomPlugIn.dll。
- 部署:把
CustomPlugIn.dll覆盖到C:\Program Files (x86)\Kingdee\K3Cloud\WebSite\Bin\CustomPlugIn.dll。
- 覆盖 Bin 下的 DLL 会触发站点应用池回收(正常);如未生效,手动在 IIS 里回收应用池 /
iisreset。
- 确认:检查
WebSite\Bin\CustomPlugIn.dll的修改时间已更新。
八、步骤四:在“获取库存”操作注册插件(位置关键!)
坑 C:注册到“菜单按钮的点击事件插件”是错的,钩子不会被调用。必须注册在获取库存服务操作的插件列表里。
- BOS 打开目标单据(例如发货通知单
SAL_DELIVERYNOTICE)。 - 找到 获取库存 / QueryStock 操作:在表单的【操作】列表里(不是“菜单集合/明细菜单”),标识通常含
QueryStock,元数据节点为QueryStockOperationMeta。 - 在该操作的 插件(PlugIns / 服务插件) 列表新增一项,填写完整类型名:
CustomPlugIn.Stock.AllowGetInvStockPlugIn, CustomPlugIn
![图片[3]-金蝶云获取库存服务如何设置获取指定仓库的库存-Dgcity](https://dgcity.com/wp-content/uploads/2026/08/20260827084148286-image-1024x571.png)
- 保存并发布该单据。
- 多张单据用到“获取库存”,需逐张重复注册(标准 BOS 没有全局一键开关)。
九、步骤五:配置“匹配维度”(字段能否出数的关键!)
坑 B:即便插件和字段映射都正确,FInventoryQty 仍可能为空——因为获取库存会把库存与分录按“匹配维度”匹配,其中“按库存状态匹配”会和白名单互相打架。
在 BOS 的同一“获取库存 / QueryStock 操作”下,找到 匹配维度 / 字段映射 配置:
- 关闭“按库存状态匹配”:否则获取库存拿分录的
FSTOCKSTATUSID过滤库存。若不同仓库库存状态不同(不良品仓=10257、良品仓=10000,而分录只填一个状态),状态对不上 → 字段为空。取消勾选“库存状态”(只保留你的业务需要的维度,如组织/物料)。 - “仓库”匹配决定是否汇总:
- 开(默认):FInventoryQty 与分录“发货仓库”绑定 → 显示该具体仓库的数量(多仓有货时不是合计)。
- 关:不再绑定具体发货仓库 → FInventoryQty 显示所有已勾仓库的合计。
- 按需求选择;想要“多勾选仓合计”就关掉“仓库”匹配。
保存发布。这一步与插件无关,但是字段真正出数的前提。
字段映射本身(库存字段 → FInventoryQty)需在获取库存操作的“字段映射”里配好(把库存的“现存量/可用量”映射到 FInventoryQty)。若你之前已经配好可不动。
十、步骤六:测试验证
准备:一条物料在多个仓库有即时库存(如 深圳良品成品仓、深圳不良品成品仓)。
- 在仓库基础资料,把其中一个仓库的“允许获取库存”取消勾选并保存。
- 打开已注册插件的单据,录入该物料,点分录“获取库存”(tbGetInvStock)。
- 预期:未勾选仓库的库存不再出现;已勾选仓正常返回。
- 场景扩展:
- 单仓显示:保留“仓库”匹配 → FInventoryQty = 该分录发货仓的数量(未勾选则 0)。
- 多仓合计:关掉“仓库”匹配 → 勾选 2 个仓,FInventoryQty = 这两个仓合计。
- 状态问题:关掉“按库存状态匹配”后,不同状态仓库都能出数。
十一、常见问题与排查(FAQ)
Q1:点“获取库存”后字段仍为空?
先排查第 9 步“匹配维度”(库存状态/仓库)配置,这是最常见原因;其次确认字段映射已配;最后才怀疑插件(见 Q3)。
Q2:报 t.FSTOCKID 无效 / 列不存在?
说明底层 SQL 库存表别名与 t 不符(本机实测就是 t)。可把 AppendAllowFilter 里 t.FSTOCKID 改成你的实际别名;或临时关闭 SQL 层过滤(见 Q3),仅用 AfterGetAllData 内存过滤。
Q3:如何快速判断是 SQL 层还是内存层的问题?
临时把 RegexGetInvDataSql / RegexGetInvSumDataSql 两个方法体改为 return sql;(关闭 SQL 过滤),仅保留 AfterGetAllData 内存过滤(已实测可靠)。若此时能正确过滤,则问题在 SQL 别名/语法;若仍不对,问题在 BOS 匹配维度/字段映射。
Q4:所有仓库都取不到库存?
确认 T_BD_STOCK.FALLOWGETINVSTOCK 列存在,且历史数据默认已为 '1'(NULL 本就默认允许,不必回填;若你改成了严格白名单 ='1',则需回填历史 '1')。
Q5:能支持任意个仓库勾选吗?
能。过滤是基于仓库表集合的 EXISTS,与勾选数量无关。
Q6:本机 API 与网上的参考文档不一样,编译不过?
见下表(本机 9.1.728.3 实测):
| 项目 | 本机实测(9.1.728.3) |
|---|---|
| 抽象类 | Kingdee.K3.SCM.App.Core.AbstractGetInvStockPlugIn |
SqlParam | Kingdee.BOS.SqlParam(在 Kingdee.BOS.dll) |
STK_Inventory | Kingdee.K3.SCM.Common.BusinessEntity.STK.STK_Inventory(强类型 StockId,非 DynamicObject) |
GetInvStockDetailArg | Kingdee.K3.Core.SCM.Args.GetInvStockDetailArg |
| 额外钩子 | 还有 RegexGetInvSumDataSql(汇总取数也要覆盖) |
| 库存表别名 | t(from t_stk_inventory t ...) |
| 字段列名 | FALLOWGETINVSTOCK(全大写,char(1) 存 '1'/'0') |
系统内置示例 Kingdee.K3.SCM.App.Core.PurAssortReqGetInvStock 继承同一抽象类,可佐证官方写法。
十二、回退方案
- 关闭插件过滤:
RegexGetInvDataSql/RegexGetInvSumDataSql改return sql;,保留AfterGetAllData。 - 彻底移除插件:从对应单据操作的插件列表删除
CustomPlugIn.Stock.AllowGetInvStockPlugIn, CustomPlugIn并发布。 - DLL 回退:用旧版
CustomPlugIn.dll覆盖 Bin 并回收站点。
十三、附录
附录 A:字段创建与历史回填参考 SQL(Add_FAllowGetInvStock.sql)
-- 0) 确认字段是否存在(若走 BOS 发布,列已自动生成,本段仅参考)
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME='T_BD_STOCK' AND COLUMN_NAME='FALLOWGETINVSTOCK';
-- 1) (仅当未走 BOS 时)手动建列 —— 一般不建议,会与 BOS 发布冲突
-- ALTER TABLE T_BD_STOCK ADD FALLOWGETINVSTOCK CHAR(1) NOT NULL DEFAULT('1');
-- 2) 历史仓库默认允许(NULL 本就按允许处理,此句可选)
UPDATE T_BD_STOCK SET FALLOWGETINVSTOCK='1' WHERE FALLOWGETINVSTOCK IS NULL;
附录 B:插件完整源码
见第六章节(即 AllowGetInvStockPlugIn.cs)。
附录 C:文件清单
| 文件 | 说明 |
|---|---|
AllowGetInvStockPlugIn.cs | 插件源码(最终干净版) |
CustomPlugIn.csproj | 工程文件(net48) |
CustomPlugIn.dll | 部署用 DLL |
Add_FAllowGetInvStock.sql | 字段/历史回填参考 SQL |
部署说明.md | 精简部署说明 |
教程文档.md | 本文 |
作者注:本文基于金蝶云星空 9.1.728.3 本机实测,所有“坑”均为真实踩过并验证通过的结论。其它小版本请先核对第十一节 API 差异表再编译。














暂无评论内容