金蝶云星空 Python 插件开发总结
本文档是金蝶云星空插件开发过程中的实战血泪经验总结,涵盖环境限制、避坑指南和高级调试技巧。
一、平台介绍
金蝶云星空平台(企业版)进行插件开发,利用平台IronPython能力,开发python插件。python在平台中,是一种借壳执行的操作方式,故无法使用任何外部的import动作,只能引用内部的组件和实体对象。目前平台使用的是 IronPython 2.7.x 版本,开发时必需严格使用 Python 2.7 语法及标准库,绝不可运用 Python 3.x 的新特性,以免出现兼容性问题。
二、环境限制与底层"连环坑" (致命报错篇)
金蝶使用的是高度裁剪版的 IronPython 2.7 运行环境,它既不完全是 Python,也不完全是 C#,这导致了大量原生的代码写法在这里会直接崩溃。
1. 终极杀手:"编码 0" 报错
- 报错信息:
没有可用于编码 0 的数据 - 踩坑复盘:当我们尝试使用
repr()打印包含中文的对象,或者混用str()与unicode拼接包含中文字符的日志时,底层 .NET 环境由于缺少默认 ANSI 编码器(Code Page 0)会直接崩溃。 ✅ 完美解决方案(三大铁律):
- 废弃
str(),全面拥抱unicode():将所有类型转换写为unicode(value),这会安全调用 .NET 底层的.ToString()。 - 强制声明 Unicode 字符串:代码中所有的中文字符串字面量,必须加
u前缀(例如:u"现场满足")。 - 废弃
+拼接,使用StringBuilder:对于长日志和复杂字符串组装,强制引入 .NET 的StringBuilder。
- 废弃
# 错误写法 (极易触发编码0报错)
msg = "当前行号: " + str(row_num)
# 规范写法 (100% 安全)
from System.Text import StringBuilder
sb = StringBuilder()
sb.Append(u"当前行号: ").AppendLine(unicode(row_num))2. 标准库阉割 (Missing Modules)
- 报错信息:
No module named traceback/No module named Serialization - 踩坑复盘:金蝶安全沙箱移除了大量 Python 标准库(包括
traceback,json,os等)。 ✅ 解决方案:
- 异常捕获:直接用
except Exception as ex:,输出unicode(ex)即可,不要指望traceback.format_exc()。 - JSON处理:不要用标准库的
json,必须使用金蝶自带的Kingdee.BOS.JSON.JsonUtil.Serialize。
- 异常捕获:直接用
3. 引用语法极度脆弱 (Unexpected Token)
- 报错信息:
unexpected token 'from' - 踩坑复盘:在 Python 2.7 环境下,把两个
from...import语句写在同一行(或用分号隔开但格式化工具压缩导致换行丢失)会直接宕机。 - ✅ 解决方案:每一条
import或from...import必须独占一行,绝不允许挤在一起。
三、触发机制与上下文 (触发生效篇)
1. "点击没反应" 的千古难题
- 踩坑复盘:代码写得再好,点按钮完全没反应,也不报错。
✅ 解决方案:
- 缓存坑(核心原因):金蝶客户端在打开单据时会缓存插件。修改 BOS 里的 Python 脚本并保存后,必须将系统客户端中已打开的单据页签关掉(点X),重新点击菜单打开一张新单据,代码才会生效!
- 事件监听频道错误:表单里的自定义按钮,不会触发
BeforeSave生命周期事件。必须监听BarItemClick(菜单栏按钮)或ButtonClick(界面内按钮),并通过e.BarItemKey或e.Key精准匹配按钮标识。
# 严谨的按钮监听写法 (统一转大写防呆)
def BarItemClick(e):
if unicode(e.BarItemKey).upper() == u"PVRH_TBBUTTON_7":
execute_logic()
def ButtonClick(e):
if unicode(e.Key).upper() == u"PVRH_TBBUTTON_7":
execute_logic()2. 上下文丢失 (Context Error)
- 报错信息:
'BarItemClickEventArgs' object has no attribute 'Context' - 踩坑复盘:操作插件(后台服务)的参数
e里面有Context,但是表单前端插件的按钮事件e里没有。 - ✅ 解决方案:在表单插件中,永远使用系统注入的全局变量
this.Context来获取上下文(用于执行 SQL 等操作)。
四、DynamicObject 数据穿透技巧 (ORM 查表篇)
金蝶的 this.Model.DataObject 取出来的数据是 DynamicObject,它披着 Python 字典的外衣,但本质是 C# 对象。
1. 字典判断的陷阱 (Contains 报错)
- 报错信息:
'DynamicObject' object has no attribute 'Contains' - 踩坑复盘:试图用
row.Contains("子实体Key")来判断是否有子明细行。 - ✅ 解决方案:必须深入访问其属性集合。
# 错误
if row.Contains(u"PVRH_Cust_Entry100037"):
# 正确
if row.DynamicObjectType.Properties.ContainsKey(u"PVRH_Cust_Entry100037"):2. 辅助资料取值 (Name 属性不存在)
- 报错信息:
实体类型ASSISTANTDATAENTRYSELECT中不存在名为Name的属性 - 踩坑复盘:企图用
dir_obj["Name"]获取操作方向的名字。辅助资料(AssistantData)的属性构成与基础资料不同。 - ✅ 解决方案:对于辅助资料对象,其显示文本通常存储在
FDataValue字段中,而不是Name。
# 稳健的辅助资料取值
dir_val = unicode(dir_obj[u"FDataValue"]) if dir_obj else u"None"3. 子明细 (Sub-Entry) 的遍历法
- 场景:主明细行下挂载了选项功能(子单据体),需要遍历获取子单据体中基础资料的编码。
- ✅ 标准范式:
sub_key = u"PVRH_Cust_Entry100037"
if row.DynamicObjectType.Properties.ContainsKey(sub_key):
sub_rows = row[sub_key] # 拿到集合
if sub_rows and sub_rows.Count > 0:
for sub in sub_rows:
# 拿子行里的基础资料对象
opt_base = sub[u"F_PVRH_Base1"]
# 取编码
o_code = unicode(opt_base[u"Number"]) if opt_base else u""五、开发技巧
- 开发组件引用(clr.AddReference) 和 实体对象导入(from XXX import *) 尽量多、全,不用考虑性能问题,避免从哪里复制过来的代码,因为引用和导入的遗漏导致方法无法使用报错
- 区分字段属性的
(标识)、字段名、绑定实体属性,字段预加载事件@OnPreparePropertys 中使用的是(标识),DataEntitys取值中使用的是绑定实体属性 - 测试过程中,尽管加上[操作前确认提示]、[操作成功后提示],测试中就会有弹窗反馈,就可以知道有没有走到服务插件代码中;另外需要注意!服务插件重新编辑保存后,单据要关闭重新打开后才生效!
- 有部分数据处理,如实在无法理解到基础库实体对象的方法如何使用,可以尝试在python中使用内置函数解决;如一些基础的字符串转换、处理,无需import导入的动作(服务插件中不支持导入)
实体类型Entity中不存在名为XXX的属性,注意看下属性是归属于当前单据,还是由其他属性引用带入。如果是由其他属性(元素类型为:基础资料)引用,可以将该属性打印或者保存,观察下数据结构再进行获取
# 以下示例仅供参考 F_PVRH_BaseProperty_qtr = ("{0}").format(FEItem['F_PVRH_Base2']['Specification']) # 产品编号 F_PVRH_Creator = ("{0}").format(billObj['F_PVRH_CreatorId']['Name']) # 制单人名称 F_PVRH_Base = ("{0}").format(billObj['F_PVRH_Base']['Name']) # 客户名称 _name = first_bill_obj['Name'][2052] # 多语言文本示例 取得商品名称 2052=>中文写日志到上机操作日志控制台
log = LogObject() log.pkValue = bill_no + " start" # 唯一标志 log.Description = bill_no + " 推送开始" log.OperateName = "推送开始" log.ObjectTypeId = this.BusinessInfo.GetForm().Id # 操作的业务对象ID log.SubSystemId = this.BusinessInfo.GetForm().SubsysId # 子系统Id log.Environment = OperatingEnvironment.BizOperate # 操作员 LogServiceHelper.WriteLog(this.Context, log)- 调试常用
- 表单类插件:
this.View.ShowMessage("调试信息") - 服务类插件:
raise Exception("调试信息")
六、业务算法设计规范 (防混配与隔离)
1. 界面刷新强制化
- 算法在后台修改了
this.Model.SetValue()后,用户界面是不会自动变的。 - ✅ 规范:在代码最后,必须对所有修改过的字段调用一次视图更新。
this.View.UpdateView(u"F_PVRH_Combo_qtr2")
this.View.UpdateView(u"F_PVRH_Remarks_83g")七、"核弹级"调试技巧:数据透视法
在金蝶云里,Python 是写在沙箱里的黑盒,无法像本地 IDE 那样断点调试。如果取值不对,千万不要靠猜。
1. 属性全量扫描法
当你不知道一个 DynamicObject 里到底有什么字段(比如之前的操作方向),直接写循环暴打出来:
sb.AppendLine(u" [对象属性全扫描]:")
for p in obj.DynamicObjectType.Properties:
try:
p_val = unicode(obj[unicode(p.Name)])
sb.Append(u" - ").Append(unicode(p.Name)).Append(u" : ").AppendLine(p_val)
except:
pass2. 体检报告输出模式
构建清晰的 [STEP 1] -> 当前值 -> [PASS/FAIL] 日志流水。哪里红了查哪里,这不仅是开发利器,上线后更是业务排障的最佳工具。
八、数据库操作示例
在金蝶云星空中,ExecuteScalar、Execute、ExecuteDataSet和ExecuteDynamicObject是四个常用的数据库操作方法,它们的区别如下:
1. ExecuteScalar
- 功能:执行SQL语句并返回结果集的第一行第一列的值。
- 适用场景:用于查询单一值,例如统计结果(如
COUNT、SUM等)。 - 返回值:指定的数据类型(如
int、string等)。 Python插件代码示例:
ctx = e.Context sql = "SELECT COUNT(1) FROM T_BD_MATERIAL" CNT = DBServiceHelper.ExecuteScalar(ctx, sql, None) return int(CNT)
2. Execute
- 功能:执行SQL语句(如
INSERT、UPDATE、DELETE等),返回受影响的行数。 - 适用场景:用于执行修改数据库的操作,如插入、更新或删除数据。
- 返回值:整数,表示受影响的行数。
Python插件代码示例:
sql = """ /*dialect*/ UPDATE T_XXXX_YYENTRY -- 替换为实际的明细表名 SET FNEWVALUE = '新值' -- 替换为需要更新的字段名和值 WHERE FDETAILID = '{0}' -- 替换为明细ID字段名 AND FID = {1} -- 替换为主表ID字段名 """.format( this.View.Model.GetValue("FDETAILID", 0).ToString(), # 明细ID mainTableId # 主表ID ) affected_rows = DBServiceHelper.Execute(this.Context, sql)
3. ExecuteDataSet
- 功能:执行SQL语句并返回一个
DataSet对象,包含一个或多个DataTable。 - 适用场景:用于查询多行多列的数据,适合需要处理复杂结果集的场景。
- 返回值:
DataSet对象,可以通过Tables属性访问具体的数据表。 Python插件代码示例:
sql = """ /*dialect*/SELECT FSalerId,FCreateDate FROM T_SAL_ORDER WHERE FID = '{0}' """.format(str(FID)) ds = DBServiceHelper.ExecuteDataSet(ctx, sql) saler_id = ds.Tables[0].Rows[0]["FSalerId"] create_date = ds.Tables[0].Rows[0]["FCreateDate"]
4. ExecuteDynamicObject
- 功能:执行SQL语句并返回一个
dynamic对象,适合查询单行多列的数据。 - 适用场景:查询单行数据,且需要直接访问列值。
- 返回值:一个
dynamic对象,其中每个属性对应查询结果的一列。 Python插件代码示例:
sql = "/*dialect*/SELECT FMATERIALID, FNumber FROM T_BD_MATERIAL WHERE FMATERIALID = 1" result = DBServiceHelper.ExecuteDynamicObject(this.Context, sql) first = next(iter(result), None) if first: saler_id = first["FMATERIALID"].ToString() create_date = first["FNumber"].ToString()
5. 总结
ExecuteScalar:适合查询单一值。Execute:适合执行修改操作,返回受影响行数。ExecuteDataSet:适合查询多行多列数据,返回DataSet。ExecuteDynamicObject:适合查询单行多列数据,返回动态对象。
九、参考代码
# -*- coding: utf-8 -*-
# 表单插件示例:点击按钮执行打包计算
import clr
clr.AddReference('System')
clr.AddReference('mscorlib')
import System
from System.Text import StringBuilder
from Kingdee.BOS.ServiceHelper import DBServiceHelper
# ================= 业务配置 =================
TARGET_BUTTON_KEY = u"PVRH_tbButton_7"
VAL_PACK_OVERALL = u"A" # 整体包装
VAL_PACK_SPLIT = u"B" # 分体包装
# ================= 事件入口 =================
def BarItemClick(e):
if unicode(e.BarItemKey).upper() == TARGET_BUTTON_KEY.upper():
execute_pack_logic()
def ButtonClick(e):
if unicode(e.Key).upper() == TARGET_BUTTON_KEY.upper():
execute_pack_logic()
# ================= 核心业务逻辑 =================
def execute_pack_logic():
# 使用 StringBuilder 收集日志(避免编码问题)
sb = StringBuilder()
sb.AppendLine(u"### 自动打包计算 ###")
try:
# Step 1: 全局校验
sb.AppendLine(u"\n[Step 1] 全局校验:")
site_val = unicode(this.Model.GetValue(u"F_PVRH_Combo_re5"))
site_pass = (site_val == u"1")
sb.Append(u" 现场满足: ").AppendLine(u"[OK]" if site_pass else u"[FAIL]")
# Step 2: 逐行处理
bill_obj = this.Model.DataObject
entry_rows = bill_obj[u"SaleOrderEntry"]
for i in range(entry_rows.Count):
row = entry_rows[i]
# 默认分体
this.Model.SetValue(u"F_PVRH_Combo_qtr2", VAL_PACK_SPLIT, i)
# 业务逻辑处理...
# Step 3: 刷新界面
this.View.UpdateView(u"F_PVRH_Combo_qtr2")
this.View.ShowMessage(sb.ToString())
except Exception as ex:
this.View.ShowMessage(u"运行异常: " + unicode(ex))
# ================= 工具函数 =================
def post(url, postData):
bytes = Encoding.ASCII.GetBytes(postData)
webRequest = HttpWebRequest.Create(url)
webRequest.Method = 'POST'
webRequest.ContentType = 'application/json'
webRequest.Timeout = 1000 * 60 * 10
webRequest.ContentLength = bytes.Length
webRequest.GetRequestStream().Write(bytes, 0, bytes.Length)
webRequest.GetRequestStream().Flush()
webRequest.GetRequestStream().Close()
webResponse = webRequest.GetResponse()
streamReader = StreamReader(webResponse.GetResponseStream(), Encoding.GetEncoding('utf-8'))
result = streamReader.ReadToEnd()
return result
def get(url):
webRequest = HttpWebRequest.Create(url)
webRequest.Method = 'GET'
webResponse = webRequest.GetResponse()
streamReader = StreamReader(webResponse.GetResponseStream(), Encoding.GetEncoding('utf-8'))
result = streamReader.ReadToEnd()
return result
# 中文字符串转换成Unicode编码 避免在 Encoding.ASCII.GetBytes 方法中被转成乱码
def tidyString(s):
return s.encode('unicode_escape').decode('ascii') if isinstance(s, str) else s十、参考资料
- 插件实战开发-新手入门教程-服务插件
- 【新手入门】插件实操【分享汇总】
- Python开发
- 服务插件 代表性"e.DataEntitys"取值
- 服务插件如何遍历单据体明细,获取单据体明细行内码?
- #使用技巧#Python插件调试技巧及常见报错分析(新手必看,老手看不上的 干货)
本文由 ben 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Mar 5, 2026 at 01:39 pm
cool