金蝶云星空开发~第三篇:插件篇——C#插件开发与事件体系

前言:建模之后,用代码赋予灵魂

第二篇我们用BOS IDE完成了"销售报价单"的建模------有了字段、布局、校验规则、值更新。但真实的企业业务往往比配置更复杂:需要调用外部系统接口、需要动态计算复杂算法、需要根据上下文决定按钮是否可见......这些是配置无法覆盖的,必须通过插件代码来实现。

金蝶云星空的插件体系非常成熟,提供了从表单加载、数据保存、按钮点击到列表展示的全方位扩展点。本篇的目标是:让你掌握所有主流插件类型的开发方法,并能在实际项目中快速定位该用哪种插件、在哪个事件中写代码


第1章:插件体系全景

1.1 插件类型概览

金蝶云星空的插件按职责分为以下五大类:

插件类型 基类 适用场景 注册位置
表单插件 AbstractBillPlugIn 单据/动态表单的操作交互、数据校验、按钮逻辑 单据操作列表→插件
列表插件 AbstractListPlugIn 列表界面的过滤、格式化、批量操作 列表视图→插件
服务插件 多种基类(详见5.1) 操作服务层的拦截(保存、提交、审核等) 操作服务→插件
构建插件 AbstractDynamicWebFormBuilderPlugIn 动态构建或修改表单控件 表单属性→构建插件
账表插件 继承自SysReportBase 报表数据预处理、格式化 账表属性→插件

初学者最容易混淆的是表单插件服务插件的区别:

  • 表单插件运行在UI层,与用户界面交互直接相关(弹窗、按钮状态、字段变色等)
  • 服务插件运行在业务逻辑层(Service层),与界面无关,专注于数据操作的核心逻辑

简单记忆:界面相关的用表单插件,数据操作相关的用服务插件。

1.2 插件生命周期与事件执行顺序

理解插件的执行顺序是调试的基础。以一张单据从打开到保存的完整过程为例:

复制代码
用户操作         触发事件                      可使用的插件事件
─────────────────────────────────────────────────────────────────
打开单据  →     表单加载             →      OnLoad()、AfterBindData()
填写字段  →     字段值变化           →      DataChanged()
点击保存  →     保存前校验           →      BeforeSave()、OnPrepareOperationServiceOption()
              →  执行保存操作         →      服务插件(保存拦截)
              →  保存成功后           →      AfterSave()
点击提交  →     提交前校验           →      BeforeSubmit()、服务插件(提交拦截)
              →  执行提交操作         →      服务插件(提交拦截)
              →  提交成功后           →      AfterSubmit()
点击审核  →     (类似上述流程)
关闭单据  →     表单关闭             →      BeforeClose()

关键原则:Before事件中可以拦截(抛出异常阻止后续操作),After事件中只能做后续处理(不能阻断)。

1.3 HotUpdate热更新特性解析

金蝶云星空支持热更新(HotUpdate) :修改插件代码并重新编译部署后,无需重启IIS即可生效。

原理:系统在检测到Bin目录下的DLL文件变更时,会触发应用程序域(AppDomain)的重新加载。

使用注意事项

  • 热更新存在几秒到几十秒的延迟(视服务器负载而定)
  • 如果遇到更新不生效,可以尝试:刷新页面 → 等待30秒 → 再试;仍然不行则需重启IIS
  • 生产环境建议在低峰期部署,并在部署后观察日志确认生效

💡 开发小技巧 :在VS中编译时,将输出路径直接指向金蝶的Bin目录,保存后即可直接生效,省去手动拷贝步骤。


第2章:表单插件(AbstractBillPlugIn)核心事件

表单插件是开发中最常用的插件类型,本篇将详细讲解每一个核心事件及典型应用场景。

2.1 OnLoad:表单加载时的初始化逻辑

OnLoad是最早触发的表单事件,此时界面控件已创建但数据尚未加载。适合做:

  • 初始化字段默认值(动态默认值,而非静态默认值)
  • 根据用户/组织/配置动态控制界面元素可见性
  • 读取外部配置进行初始化

代码示例:动态设置字段默认值

csharp 复制代码
using System;
using Kingdee.BOS;
using Kingdee.BOS.Core.Bill.PlugIn;
using Kingdee.BOS.Core.DynamicForm.PlugIn.Args;

namespace DEM.SCM.PlugIn
{
    public class SaleQuoteBillPlugin : AbstractBillPlugIn
    {
        /// <summary>
        /// 表单加载初始化
        /// </summary>
        public override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            
            // 场景1:动态设置报价日期为当前日期(如果字段为空)
            DateTime currentDate = DateTime.Now.Date;
            if (this.View.Model.GetValue("FDate") == null)
            {
                this.View.Model.SetValue("FDate", currentDate);
            }

            // 场景2:根据当前用户组织设置默认有效期天数
            // 从用户扩展信息中读取配置(示例:从参数表中读取)
            int defaultValidDays = GetDefaultValidDaysFromConfig();
            if (this.View.Model.GetValue("FValidDays") == null)
            {
                this.View.Model.SetValue("FValidDays", defaultValidDays);
            }
        }

        private int GetDefaultValidDaysFromConfig()
        {
            // 模拟从某配置表读取
            return 15;
        }
    }
}

2.2 AfterBindData:数据绑定后的操作

AfterBindData在数据加载完成后触发(新建单据时也有调用)。与OnLoad的区别:

  • OnLoad:数据尚未绑定到模型
  • AfterBindData:数据已绑定到模型,可以读取字段值

典型应用

  • 根据加载的数据动态调整界面(如控制按钮状态)
  • 执行依赖数据内容的初始化逻辑
  • 修改已有数据的展示形式

代码示例:根据单据状态控制按钮可见性

csharp 复制代码
public override void AfterBindData(EventArgs e)
{
    base.AfterBindData(e);
    
    // 读取当前单据状态字段
    string status = this.View.Model.GetValue("FQuoteStatus")?.ToString() ?? "";
    
    // 如果状态为"已确认",禁用修改/删除按钮
    if (status == "2") // 假设2代表"已确认"
    {
        this.View.GetControl("FToolBar").SetEnable("btnEdit", false);
        this.View.GetControl("FToolBar").SetEnable("btnDelete", false);
    }
}

2.3 BeforeSave / AfterSave:保存前后的拦截与扩展

BeforeSave是数据校验的最后一道防线------在数据即将存入数据库前执行。如果校验失败,抛出异常即可阻止保存。

AfterSave在数据保存成功后执行,适合做:

  • 记录操作日志
  • 触发后续业务(如调用外部接口发送通知)
  • 更新缓存

代码示例:保存前校验 + 保存后日志

csharp 复制代码
public override void BeforeSave(BeforeSaveEventArgs e)
{
    base.BeforeSave(e);
    
    // 复杂校验:检查客户是否存在未完成的报价单(不允许重复)
    object customerId = this.View.Model.GetValue("FCustomerId");
    if (customerId != null && customerId is long)
    {
        long custId = (long)customerId;
        if (HasUnfinishedQuote(custId))
        {
            throw new Exception("该客户已有未完成的报价单,请先处理历史报价!");
        }
    }
}

private bool HasUnfinishedQuote(long customerId)
{
    // 通过SQL查询是否存在未完成的报价单
    string sql = @"
        SELECT COUNT(1) 
        FROM T_DEM_SaleQuote 
        WHERE FCustomerId = {0} 
        AND FQuoteStatus IN ('0','1')"; // 草稿或已提交状态
    object count = DBUtils.ExecuteScalar(this.Context, sql, customerId);
    return Convert.ToInt32(count) > 0;
}

public override void AfterSave(AfterSaveEventArgs e)
{
    base.AfterSave(e);
    
    // 保存成功后记录日志
    string billNo = this.View.Model.GetValue("FBillNo")?.ToString() ?? "";
    Logger.Info($"销售报价单[{billNo}] 已被保存,操作用户:{this.Context.User.Name}");
}

2.4 BarItemClick:按钮点击事件处理

表单上的自定义按钮(通常在工具栏中)的点击事件由BarItemClick统一处理。

配置自定义按钮

  1. 在BOS IDE中打开单据设计器
  2. 在工具栏(ToolBar)上右键 → "添加按钮"
  3. 设置按钮的Key(如btnSendEmail)和Caption(显示名称)
  4. 在插件的BarItemClick方法中通过BarItemKey区分按钮

代码示例:自定义按钮"发送邮件"

csharp 复制代码
public override void BarItemClick(BarItemClickEventArgs e)
{
    base.BarItemClick(e);
    
    // 判断点击的是哪个按钮
    if (e.BarItemKey == "btnSendEmail")
    {
        // 获取当前单据数据
        string billNo = this.View.Model.GetValue("FBillNo")?.ToString() ?? "";
        string customerName = this.View.Model.GetValue("FCustomerId_Name")?.ToString() ?? "";
        
        // 构建邮件内容(示例:调用邮件发送服务)
        string subject = $"报价单[{billNo}]";
        string body = $"尊敬的{customerName},请查收附件中的报价单...";
        
        // 发送邮件(伪代码)
        // EmailHelper.SendEmail(customerEmail, subject, body);
        
        this.View.ShowMessage($"报价单[{billNo}] 邮件已发送!");
    }
}

2.5 DataChanged:字段值变更监听

DataChanged是表单插件中最核心的事件之一,每当用户修改字段值时触发。用于实现复杂的联动逻辑。

关键参数DataChangedEventArgs包含了:

  • FieldKey:变更的字段标识
  • OldValue:变更前的值
  • NewValue:变更后的值
  • RowIndex:如果是分录字段,表示变更的行索引

代码示例:复杂的字段联动

csharp 复制代码
public override void DataChanged(DataChangedEventArgs e)
{
    base.DataChanged(e);
    
    // 场景1:当客户变更时,自动加载该客户的默认联系人
    if (e.FieldKey == "FCustomerId")
    {
        if (e.NewValue != null && e.NewValue is long)
        {
            long custId = (long)e.NewValue;
            string defaultContact = GetDefaultContactByCustomer(custId);
            this.View.Model.SetValue("FContactPerson", defaultContact);
        }
    }
    
    // 场景2:当单据体物料变更时,自动带出物料默认价格
    if (e.FieldKey == "FMaterialId" && e.RowIndex >= 0)
    {
        if (e.NewValue != null && e.NewValue is long)
        {
            long materialId = (long)e.NewValue;
            decimal defaultPrice = GetMaterialPrice(materialId);
            // 设置当前行的单价(注意RowIndex)
            this.View.Model.SetValue("FPrice", defaultPrice, e.RowIndex);
            
            // 触发重新计算金额(通过值更新规则自动完成)
        }
    }
}

private string GetDefaultContactByCustomer(long custId)
{
    // 模拟从数据库查询
    return "张三";
}

private decimal GetMaterialPrice(long materialId)
{
    // 模拟从物料档案读取价格
    return 100.00m;
}

⚠️ 重要提示DataChanged中修改字段值会再次触发DataChanged,要避免死循环。金蝶平台对此有一定保护机制,但建议加上条件判断,如if (!object.Equals(e.OldValue, e.NewValue))

2.6 EntityRowDoubleClick:分录双击事件

当用户双击单据体(分录)的某一行时触发,常用于打开明细查看或关联单据跳转。

代码示例:双击分录查看物料详情

csharp 复制代码
public override void EntityRowDoubleClick(EntityRowDoubleClickEventArgs e)
{
    base.EntityRowDoubleClick(e);
    
    // 判断是哪个单据体(如果有多个分录)
    if (e.EntityKey == "FEntity")
    {
        int rowIndex = e.RowIndex;
        object materialId = this.View.Model.GetValue("FMaterialId", rowIndex);
        if (materialId != null)
        {
            // 打开物料详情页(动态表单)
            this.View.ShowForm("BD_Material", materialId);
        }
    }
}

第3章:列表插件(AbstractListPlugIn)

列表插件用于控制列表界面(即单据的列表视图)的行为,区别于表单插件控制的是单据的详情编辑界面。

3.1 列表插件特有方法与事件

方法/事件 说明 典型用途
OnInitialize 列表初始化时触发 设置列表默认过滤条件
BeforeBindRowData 每行数据绑定前触发 动态设置行样式、格式化显示
AfterBindRowData 每行数据绑定后触发 行数据处理后扩展
OnFormatRowCondition 行格式条件设置 根据数据设置行颜色、字体等
BarItemClick 列表工具栏按钮点击 处理列表界面的自定义操作

3.2 多字段模糊查询与过滤干预

场景:列表默认只支持按编码和名称模糊搜索,但用户希望同时能按客户名称、备注等字段搜索。

代码示例:扩展模糊查询字段

csharp 复制代码
using Kingdee.BOS.Core.List.PlugIn;
using Kingdee.BOS.Core.List.PlugIn.Args;

namespace DEM.SCM.PlugIn
{
    public class SaleQuoteListPlugin : AbstractListPlugIn
    {
        public override void OnInitialize(InitializeArgs e)
        {
            base.OnInitialize(e);
            
            // 扩展模糊查询条件:增加客户名称、备注字段
            e.QueryParm.FilterList.Add("FCustomerId_Name");
            e.QueryParm.FilterList.Add("FRemark");
            
            // 设置默认排序字段
            e.QueryParm.Sort = "FBillNo DESC";
        }
    }
}

3.3 动态设置行高、单元格颜色

在列表界面,可以根据业务数据给特定的行或单元格着色,提升用户体验。

代码示例:超期报价单标红

csharp 复制代码
public override void OnFormatRowCondition(FormatRowConditionArgs e)
{
    base.OnFormatRowCondition(e);
    
    // 获取当前行的数据
    var dataRow = e.DataRow;
    
    // 判断是否超期:有效期已过且状态不是"已失效"
    DateTime? quoteDate = dataRow["FDate"] as DateTime?;
    int? validDays = dataRow["FValidDays"] as int?;
    string status = dataRow["FQuoteStatus"]?.ToString() ?? "";
    
    if (quoteDate.HasValue && validDays.HasValue && status != "3")
    {
        DateTime expireDate = quoteDate.Value.AddDays(validDays.Value);
        if (DateTime.Now.Date > expireDate.Date)
        {
            // 设置行背景色为红色(轻微)
            e.RowStyle.BackColor = System.Drawing.Color.LightPink;
            e.RowStyle.ForeColor = System.Drawing.Color.Red;
        }
    }
}

3.4 列表格式化与批量操作

场景:列表中的状态字段显示编码而不是名称,需要在界面上转换为可读文本。

csharp 复制代码
public override void AfterBindRowData(AfterBindRowDataArgs e)
{
    base.AfterBindRowData(e);
    
    // 获取状态字段值
    string statusCode = e.DataRow["FQuoteStatus"]?.ToString() ?? "";
    
    // 转换为中文显示
    string statusName = statusCode switch
    {
        "0" => "草稿",
        "1" => "已提交",
        "2" => "已确认",
        "3" => "已失效",
        _ => "未知"
    };
    
    // 在界面上更新显示(需在列表字段中配置"格式化"属性)
    // 更推荐的方式:在BOS IDE中配置枚举值后,系统自动转换
    // 代码方式用于更复杂的动态格式化
}

第4章:动态表单插件

动态表单是金蝶云星空中一种轻量级的界面形式------不像单据有独立的数据库表,它只是一个弹出窗口,用于数据收集或信息展示。

典型应用场景

  • 弹窗让用户输入参数(如报表查询条件)
  • 自定义的确认对话框
  • 嵌入第三方内容

4.1 AbstractDynamicFormPlugIn基类使用

动态表单插件的基类是AbstractDynamicFormPlugIn,其事件模型与AbstractBillPlugIn高度相似(OnLoad、DataChanged、BarItemClick等),此处不再重复。

4.2 动态表单的参数传递(OpenParameter)

调用动态表单时,可以通过OpenParameter传递参数。

调用方代码(在表单插件中打开动态表单):

csharp 复制代码
// 构建打开参数
DynamicFormShowParameter showParam = new DynamicFormShowParameter();
showParam.FormId = "DEM_MyDynamicForm"; // 动态表单的业务对象标识
showParam.ParentPageId = this.View.PageId;

// 传递参数
showParam.CustomParams.Add("CustomerId", 12345);
showParam.CustomParams.Add("IsEditMode", true);

// 打开动态表单
this.View.ShowForm(showParam);

动态表单插件中接收参数

csharp 复制代码
public override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    
    // 接收调用方传递的参数
    long customerId = 0;
    if (this.View.OpenParameter.CustomParams.ContainsKey("CustomerId"))
    {
        customerId = Convert.ToInt64(this.View.OpenParameter.CustomParams["CustomerId"]);
    }
    
    bool isEditMode = false;
    if (this.View.OpenParameter.CustomParams.ContainsKey("IsEditMode"))
    {
        isEditMode = Convert.ToBoolean(this.View.OpenParameter.CustomParams["IsEditMode"]);
    }
    
    // 根据参数加载数据、控制界面
    if (customerId > 0)
    {
        LoadCustomerData(customerId);
    }
    this.View.GetControl("FToolBar").SetEnable("btnEdit", isEditMode);
}

4.3 动态表单与主界面交互

动态表单可以返回值给调用方。

动态表单中返回值

csharp 复制代码
// 在动态表单中,用户点击"确定"按钮时返回数据
public override void BarItemClick(BarItemClickEventArgs e)
{
    base.BarItemClick(e);
    
    if (e.BarItemKey == "btnConfirm")
    {
        // 收集用户输入的数据
        string selectedCode = this.View.Model.GetValue("FCode")?.ToString() ?? "";
        string selectedName = this.View.Model.GetValue("FName")?.ToString() ?? "";
        
        // 构造返回值
        JObject result = new JObject();
        result["Code"] = selectedCode;
        result["Name"] = selectedName;
        
        // 关闭当前动态表单并返回数据(需在调用方通过ReturnEventArgs接收)
        this.View.ReturnToParentWindow(result.ToString());
        this.View.Close();
    }
}

调用方接收返回值

csharp 复制代码
// 在调用方中处理返回事件
public override void OnShowFormResult(ShowFormResultEventArgs e)
{
    base.OnShowFormResult(e);
    
    if (e.FormResult != null)
    {
        JObject result = JObject.Parse(e.FormResult.ToString());
        string code = result["Code"]?.ToString() ?? "";
        string name = result["Name"]?.ToString() ?? "";
        
        this.View.ShowMessage($"用户选择了:{code} - {name}");
    }
}

第5章:服务插件(操作服务插件)

5.1 服务插件的注册位置与基类

服务插件的基类与注册位置因操作类型而异:

操作类型 基类 注册位置
保存操作 AbstractSaveServicePlugIn 操作列表→保存操作→服务插件
提交操作 AbstractSubmitServicePlugIn 操作列表→提交操作→服务插件
审核操作 AbstractAuditServicePlugIn 操作列表→审核操作→服务插件
反审核操作 AbstractUnAuditServicePlugIn 操作列表→反审核操作→服务插件
删除操作 AbstractDeleteServicePlugIn 操作列表→删除操作→服务插件

核心区别:服务插件在业务逻辑层执行,不依赖UI,因此即使是通过WebAPI调用也会触发。

5.2 操作服务插件内添加校验器

服务插件的主要用途是在操作执行前进行不可绕过的业务校验

代码示例:提交操作插件------校验报价单完整性

csharp 复制代码
using Kingdee.BOS.Service.PlugIn;
using Kingdee.BOS.Core.Validation;

namespace DEM.SCM.PlugIn
{
    public class SaleQuoteSubmitServicePlugin : AbstractSubmitServicePlugIn
    {
        public override void OnPrepareOperationServiceOption(PrepareOperationServiceOptionArgs e)
        {
            base.OnPrepareOperationServiceOption(e);
            
            // 添加自定义校验器
            e.ServiceOption.Validators.Add(new QuoteCompleteValidator());
        }
    }
    
    /// <summary>
    /// 自定义校验器:检查报价单是否完整
    /// </summary>
    public class QuoteCompleteValidator : AbstractValidator
    {
        public QuoteCompleteValidator() : base("QuoteCompleteValidator", "报价完整性校验")
        {
        }
        
        public override void Validate(ValidateContext context)
        {
            base.Validate(context);
            
            // 获取要校验的数据
            var dataEntity = context.DataEntitys.FirstOrDefault();
            if (dataEntity == null) return;
            
            // 校验:客户是否为空
            if (dataEntity["FCustomerId"] == null || string.IsNullOrEmpty(dataEntity["FCustomerId"].ToString()))
            {
                context.AddError("报价单缺少客户信息,请完善后提交!");
                return;
            }
            
            // 校验:分录是否存在
            var entries = dataEntity["FEntity"] as System.Collections.IList;
            if (entries == null || entries.Count == 0)
            {
                context.AddError("报价单明细为空,请录入物料后提交!");
                return;
            }
            
            // 校验:每个分录的物料、数量、价格是否完整
            foreach (var entry in entries)
            {
                var entryObj = entry as DynamicObject;
                if (entryObj == null) continue;
                
                if (entryObj["FMaterialId"] == null)
                {
                    context.AddError("存在物料为空的分录,请完善后提交!");
                    return;
                }
                
                decimal qty = Convert.ToDecimal(entryObj["FQty"] ?? 0);
                decimal price = Convert.ToDecimal(entryObj["FPrice"] ?? 0);
                if (qty <= 0 || price <= 0)
                {
                    context.AddError("存在数量或单价≤0的分录,请检查后提交!");
                    return;
                }
            }
        }
    }
}

5.3 单据操作的拦截与扩展

通过服务插件,可以拦截操作并在执行前后插入自定义逻辑。

代码示例:审核操作插件------审批通过后自动同步数据

csharp 复制代码
public class SaleQuoteAuditServicePlugin : AbstractAuditServicePlugIn
{
    public override void BeginOperationTransaction(BeginOperationTransactionArgs e)
    {
        base.BeginOperationTransaction(e);
        // 审核操作执行前(事务开始前)的逻辑
        // 可用于预留资源、加锁等
    }
    
    public override void EndOperationTransaction(EndOperationTransactionArgs e)
    {
        base.EndOperationTransaction(e);
        
        // 审核操作执行成功后(事务提交后)的逻辑
        // 适合调用外部接口、发送通知等
        var dataEntity = e.DataEntitys.FirstOrDefault();
        if (dataEntity != null)
        {
            string billNo = dataEntity["FBillNo"]?.ToString() ?? "";
            
            // 调用外部接口同步数据(伪代码)
            // SyncService.SyncToMES(billNo, dataEntity);
            
            // 发送消息通知
            Logger.Info($"报价单[{billNo}] 已审核通过");
        }
    }
}

第6章:构建插件(BuilderPlugIn)

6.1 AbstractDynamicWebFormBuilderPlugIn使用

构建插件允许在表单运行时动态修改 界面控件,包括增删控件、调整布局等。其执行时机早于OnLoad

6.2 CreateControl事件:动态创建与修改控件

典型场景:根据配置动态决定是否显示某个控件。

csharp 复制代码
using Kingdee.BOS.Core.DynamicForm.PlugIn;
using Kingdee.BOS.Core.DynamicForm.PlugIn.Args;

namespace DEM.SCM.PlugIn
{
    public class SaleQuoteBuilderPlugin : AbstractDynamicWebFormBuilderPlugIn
    {
        public override void CreateControl(CreateControlArgs e)
        {
            base.CreateControl(e);
            
            // 判断是否为特定控件
            if (e.Control.Id == "FRemark")
            {
                // 动态修改控件属性:根据配置决定是否显示
                bool showRemark = GetConfigShowRemark();
                if (!showRemark)
                {
                    e.Control.Visible = false;
                }
            }
            
            // 动态添加一个新控件(高级用法,需精确控制布局)
            // 不推荐新手使用,容易破坏布局结构
        }
        
        private bool GetConfigShowRemark()
        {
            // 从系统参数表中读取配置
            return true;
        }
    }
}

6.3 嵌入WebBrowser等自定义控件

构建插件支持将标准控件替换为自定义控件,如嵌入WebBrowser用于显示HTML内容。

csharp 复制代码
public override void CreateControl(CreateControlArgs e)
{
    base.CreateControl(e);
    
    // 将某个字段控件替换为WebBrowser
    if (e.Control.Id == "FWebContent")
    {
        // 创建WebBrowser控件
        var webBrowser = new System.Windows.Forms.WebBrowser();
        webBrowser.Url = new Uri("https://www.example.com/report");
        
        // 替换原有控件(需注意类型兼容)
        e.Control.ReplaceControl(webBrowser);
    }
}

⚠️ 警告:嵌入WebBrowser等第三方控件需谨慎,可能引起性能问题或兼容性问题。非必要不推荐使用。


第7章:Python插件开发

7.1 Python插件的应用场景与优势

除了C#,金蝶云星空还支持Python插件。其优势在于:

  • 无需编译,修改后立即生效
  • 语法简洁,适合快速原型验证
  • 适合非.NET背景的开发人员

适用场景:简单的校验逻辑、轻量级数据处理、快速原型开发。

劣势:性能不如C#,不支持复杂的数据结构操作,无法调用金蝶底层API的全部能力。

7.2 Python开发编辑器与调试方法

Python插件的开发在BOS IDE中进行:

  1. 打开BOS IDE,进入单据设计器
  2. 在"操作列表"→"插件"中,选择"Python插件"
  3. 点击"编写"进入代码编辑器

调试方法

  • 使用print()输出到日志(需开启调试日志)
  • 使用raise Exception("错误信息")抛出异常进行中断调试

7.3 Python与C#插件的能力对比

对比项 C#插件 Python插件
性能 较低
调试能力 支持VS断点调试 仅支持日志输出
API覆盖 完整 有限
部署方式 编译DLL部署 直接在元数据中存储
适用复杂度 任意复杂度 简单逻辑
版本管理 通过源码控制 存储在元数据中,不易版本对比

建议:正式项目推荐使用C#开发插件;Python插件可用于运维脚本、临时数据处理等场景。


本章实战总结:销售报价单插件清单

至此,我们为"销售报价单"开发了以下插件:

  • 表单插件(SaleQuoteBillPlugin)
    • OnLoad:初始化默认日期和有效期
    • AfterBindData:根据状态控制按钮
    • BeforeSave:校验客户是否重复报价
    • AfterSave:记录操作日志
    • BarItemClick:处理"发送邮件"自定义按钮
    • DataChanged:客户联动联系人、物料联动价格
  • 列表插件(SaleQuoteListPlugin)
    • 扩展模糊查询字段
    • 超期报价单标红提示
  • 服务插件(SaleQuoteSubmitServicePlugin)
    • 提交时校验报价完整性(客户、分录、数量价格)
  • 审核服务插件(SaleQuoteAuditServicePlugin)
    • 审核成功后同步到MES系统

常见问题速查

问题 解决方案
插件代码修改后不生效 等待30秒(热更新延迟)或重启IIS
插件中无法使用this.View.ShowMessage() 检查是否在UI线程执行,服务插件中不能使用
DataChanged死循环 在修改值前判断新旧值是否相同
找不到金蝶DLL引用 WebSite\Bin目录手动添加引用
插件报错"找不到程序集" 检查DLL是否部署到Bin目录,且版本与平台一致

下一篇预告

第四篇将进入报表开发------教你如何开发SQL账表、简单账表、万能报表,以及如何处理大数据量的报表性能问题。你将学会将"销售报价单"的数据以多维度报表的形式呈现给管理层。


练习题

  1. 为"销售报价单"增加一个"打印预览"自定义按钮,点击后弹窗显示格式化内容
  2. 在列表插件中增加一个"批量作废"自定义按钮,批量将选中单据状态改为"已失效"
  3. 开发一个保存服务插件,当报价总金额超过100万时,自动将审批人设置为总经理(权限组)
相关推荐
小懿互联集成平台2 个月前
企业微信与金蝶云星空业财审批一体化对接实施方案
数据分析·企业微信·金蝶云星空·小懿互联·数据集成对接·采购申请审批·费用报销审批
小懿互联集成平台3 个月前
金蝶云星空与钉钉OA审批对接-构建一体化财务付款管理体系
钉钉·金蝶云星空·数据对接·小懿互联·构建一体化财务付款
小懿互联集成平台3 个月前
金蝶云星空与赛狐跨境电商ERP系统数据互通对接
大数据·金蝶云星空·数据对接·小懿互联·赛狐erp
金蝶LOG3 个月前
【金蝶云星空】出纳做账-应收票据背书退回
金蝶·erp·进销存系统·金蝶云星空
金蝶LOG5 个月前
【金蝶云星空】如何新增组织机构(新公司)
金蝶·erp·进销存系统·金蝶云星空
小懿互联集成平台5 个月前
金蝶云星空与找钢网对接实施方案-实现业财融合一体
业财融合·金蝶云星空·数据对接·小懿互联·找钢网
小懿互联集成平台5 个月前
金蝶云星空账套重构对接实施方案
重构·金蝶云星空·小懿互联·财务核算重构
懒人咖7 个月前
缺料分析时携带用料清单的二开字段
c#·金蝶云星空
小懿互联集成平台7 个月前
金蝶云星空与Clover POS系统数据互通对接
金蝶云星空·数据对接·clover pos·数据对接集成·小懿互联