金蝶云获取库存服务如何设置获取指定仓库的库存

目的

图片[1]-金蝶云获取库存服务如何设置获取指定仓库的库存-Dgcity
图片[2]-金蝶云获取库存服务如何设置获取指定仓库的库存-Dgcity

开发文档

金蝶云星空 · 获取库存按仓库白名单(允许获取库存)二次开发 — 详细教程

版本:金蝶云星空 9.1.728.3(其它版本 API 命名空间可能不同,本文按本机实测修正)
目标:在仓库基础资料加一个“允许获取库存”复选框;修改获取库存服务,让点击“获取库存”时,只返回被勾选仓库的库存数量(白名单)。


一、需求背景与目标

业务希望在“获取库存”时,只把允许获取库存的仓库纳入结果,屏蔽掉不允许的仓库。实现分两层:

  1. 数据层(本文核心,代码实现):拦截“获取库存”取数,过滤掉未勾选的仓库。
  2. 展示层(极易被忽略,BOS 配置):获取库存把库存数量写回单据字段(如 FInventoryQty)时,受“获取库存操作”的匹配维度控制。配错会导致字段始终为空——本文第 9 步专门讲。

最终效果:

  • 仓库 A 勾选、仓库 B 不勾选 → 获取库存结果里只有 A 的库存;
  • 支持任意个仓库勾选;可显示单仓数量,也可显示多勾选仓的合计(见第 9 步)。

二、适用环境与前置确认

值(本机实测)
金蝶版本9.1.728.3
运行时.NET Framework 4.8
数据库SQL Server(AIS20260604094951
站点 BinC:\Program Files (x86)\Kingdee\K3Cloud\WebSite\Bin
开发工具Visual Studio / dotnet CLI(net48 类库)

前置确认:

  • 已安装 .NET Framework 4.8 开发者包(能编译 net48)。
  • 能访问金蝶 WebSite\Bin 下的 Kingdee.*.dll(编译时需要引用)。
  • 有 BOS 设计器权限(加字段、注册插件、改匹配维度)。

三、关键认知:为什么“字段为空”往往不是代码问题

这是本次开发最关键的教训,先讲清楚,避免你重蹈覆辙。

金蝶“获取库存(QueryStock)”写回单据字段的流程是:

  1. 服务按物料取出即时库存;
  2. 操作上配置的“匹配维度”(仓库 / 库存状态 / 辅助属性 / 批号 …)把库存与分录匹配;
  3. 把匹配上的库存数量经字段映射写回单据字段(如 FInventoryQty 当前库存)。

所以:

  • 插件只决定“哪些仓库能进结果”
  • “结果怎么汇总到字段”由匹配维度 + 字段映射决定

我们实测踩到的两个坑:

  • 坑 A:基础生成的取数 SQL 没有 WHERE 子句,如果在代码里直接追加 AND EXISTS(...),SQL 会以 AND 开头 → 整条语法错误 → 取数为空。必须按“有无 WHERE”自适应,见第 6 步。
  • 坑 B(最隐蔽):若“按库存状态匹配”开着,而不同仓库的库存状态不同(如不良品仓状态=10257、良品仓=10000,分录只填一个状态),状态对不上 → 字段为空。解决:关掉“按库存状态匹配”(第 9 步)。
  • 坑 C:注册位置错了(注册到菜单按钮的点击事件插件,而不是获取库存服务操作的插件),钩子根本不会被调用(第 8 步)。

四、总体方案(双保险)

继承 AbstractGetInvStockPlugIn,覆盖三个钩子:

  1. 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 ...)。
  1. 内存层兜底(可靠) AfterGetAllData(DataTable, List<STK_Inventory>)
    直接剔除 DataTable / 列表中未勾选仓库的行。该钩子拿到的 dtbInvs 是已物化的内存表,稳定可靠。
  2. ApplyExtFilter 兼容性兜底保留(在本单据的填充路径上它实际收到的枚举为空,不参与字段填充;保留不影响业务)。

语义:FALLOWGETINVSTOCK='1'(勾选)→ 允许;'0' → 排除;NULL(历史仓库)→ 默认允许(安全默认,避免一刀切把所有仓库屏蔽)。


五、步骤一:仓库基础资料增加字段(BOS)

  1. 打开 BOS 设计器,登录对应数据中心。
  2. 打开 仓库(BD_STOCK) 业务对象(在【基础资料】→【库存管理】下)。
  3. 在左侧实体字段区新增字段
  • 字段类型:复选框(布尔)
  • 字段标识(属性名):FAllowGetInvStock ⚠️ 必须与插件查询的列名完全一致(大小写无所谓,SQL Server 排序规则通常不区分,但建议照抄)
  • 名称:允许获取库存
  • 默认值:True(即默认允许,安全)
  1. 把该字段拖到表单的 基本信息 → 控制 分组(按你的表单布局摆放即可)。
  2. 保存 → 发布。发布后系统会自动在 T_BD_STOCK 表生成列 FALLOWGETINVSTOCK(char(1),存 '1'/'0')。
  3. (可选)历史仓库若需明确为允许,执行本文附录 SQL 的回填语句,把存量数据置 '1'(默认已允许,可不执行)。

⚠️ 不要用手动建列 SQL 替代 BOS:手动建列后若再 BOS 发布会冲突。规范做法就是走 BOS。


六、步骤二:编写插件(完整源码 + 讲解)

6.1 新建 net48 类库工程

  • 工程名:CustomPlugIn,目标框架 .NET Framework 4.8,输出类型 类库
  • 引用金蝶 Bin 下的:Kingdee.BOS.dllKingdee.BOS.App.dllKingdee.K3.SCM.App.Core.dllKingdee.K3.SCM.Common.BusinessEntity.dllKingdee.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' 即可。

七、步骤三:编译与部署

  1. 编译 Release:
   cd InvStockFilter
   dotnet build -c Release

产物:bin\Release\net48\CustomPlugIn.dll

  1. 部署:把 CustomPlugIn.dll 覆盖到 C:\Program Files (x86)\Kingdee\K3Cloud\WebSite\Bin\CustomPlugIn.dll
  • 覆盖 Bin 下的 DLL 会触发站点应用池回收(正常);如未生效,手动在 IIS 里回收应用池 / iisreset
  1. 确认:检查 WebSite\Bin\CustomPlugIn.dll 的修改时间已更新。

八、步骤四:在“获取库存”操作注册插件(位置关键!)

坑 C:注册到“菜单按钮的点击事件插件”是错的,钩子不会被调用。必须注册在获取库存服务操作的插件列表里。

  1. BOS 打开目标单据(例如发货通知单 SAL_DELIVERYNOTICE)。
  2. 找到 获取库存 / QueryStock 操作:在表单的【操作】列表里(不是“菜单集合/明细菜单”),标识通常含 QueryStock,元数据节点为 QueryStockOperationMeta
  3. 在该操作的 插件(PlugIns / 服务插件) 列表新增一项,填写完整类型名:
   CustomPlugIn.Stock.AllowGetInvStockPlugIn, CustomPlugIn
图片[3]-金蝶云获取库存服务如何设置获取指定仓库的库存-Dgcity
  1. 保存并发布该单据。
  2. 多张单据用到“获取库存”,需逐张重复注册(标准 BOS 没有全局一键开关)。

九、步骤五:配置“匹配维度”(字段能否出数的关键!)

坑 B:即便插件和字段映射都正确,FInventoryQty 仍可能为空——因为获取库存会把库存与分录按“匹配维度”匹配,其中“按库存状态匹配”会和白名单互相打架。

在 BOS 的同一“获取库存 / QueryStock 操作”下,找到 匹配维度 / 字段映射 配置:

  • 关闭“按库存状态匹配”:否则获取库存拿分录的 FSTOCKSTATUSID 过滤库存。若不同仓库库存状态不同(不良品仓=10257、良品仓=10000,而分录只填一个状态),状态对不上 → 字段为空。取消勾选“库存状态”(只保留你的业务需要的维度,如组织/物料)。
  • “仓库”匹配决定是否汇总
  • 开(默认):FInventoryQty 与分录“发货仓库”绑定 → 显示该具体仓库的数量(多仓有货时不是合计)。
  • 关:不再绑定具体发货仓库 → FInventoryQty 显示所有已勾仓库的合计
  • 按需求选择;想要“多勾选仓合计”就关掉“仓库”匹配。

保存发布。这一步与插件无关,但是字段真正出数的前提

字段映射本身(库存字段 → FInventoryQty)需在获取库存操作的“字段映射”里配好(把库存的“现存量/可用量”映射到 FInventoryQty)。若你之前已经配好可不动。


十、步骤六:测试验证

准备:一条物料在多个仓库有即时库存(如 深圳良品成品仓、深圳不良品成品仓)。

  1. 在仓库基础资料,把其中一个仓库的“允许获取库存”取消勾选并保存。
  2. 打开已注册插件的单据,录入该物料,点分录“获取库存”(tbGetInvStock)。
  3. 预期:未勾选仓库的库存不再出现;已勾选仓正常返回。
  4. 场景扩展:
  • 单仓显示:保留“仓库”匹配 → FInventoryQty = 该分录发货仓的数量(未勾选则 0)。
  • 多仓合计:关掉“仓库”匹配 → 勾选 2 个仓,FInventoryQty = 这两个仓合计。
  • 状态问题:关掉“按库存状态匹配”后,不同状态仓库都能出数。

十一、常见问题与排查(FAQ)

Q1:点“获取库存”后字段仍为空?
先排查第 9 步“匹配维度”(库存状态/仓库)配置,这是最常见原因;其次确认字段映射已配;最后才怀疑插件(见 Q3)。

Q2:报 t.FSTOCKID 无效 / 列不存在?
说明底层 SQL 库存表别名与 t 不符(本机实测就是 t)。可把 AppendAllowFiltert.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
SqlParamKingdee.BOS.SqlParam(在 Kingdee.BOS.dll
STK_InventoryKingdee.K3.SCM.Common.BusinessEntity.STK.STK_Inventory(强类型 StockId,非 DynamicObject)
GetInvStockDetailArgKingdee.K3.Core.SCM.Args.GetInvStockDetailArg
额外钩子还有 RegexGetInvSumDataSql(汇总取数也要覆盖)
库存表别名tfrom t_stk_inventory t ...
字段列名FALLOWGETINVSTOCK(全大写,char(1) 存 '1'/'0'

系统内置示例 Kingdee.K3.SCM.App.Core.PurAssortReqGetInvStock 继承同一抽象类,可佐证官方写法。


十二、回退方案

  • 关闭插件过滤:RegexGetInvDataSql/RegexGetInvSumDataSqlreturn 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 差异表再编译。

成品DLL及项目文件

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发
头像-Dgcity
欢迎您留下宝贵的见解!
提交
头像-Dgcity

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容