前言:建模之后,用代码赋予灵魂
第二篇我们用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统一处理。
配置自定义按钮:
- 在BOS IDE中打开单据设计器
- 在工具栏(ToolBar)上右键 → "添加按钮"
- 设置按钮的
Key(如btnSendEmail)和Caption(显示名称) - 在插件的
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中进行:
- 打开BOS IDE,进入单据设计器
- 在"操作列表"→"插件"中,选择"Python插件"
- 点击"编写"进入代码编辑器
调试方法:
- 使用
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账表、简单账表、万能报表,以及如何处理大数据量的报表性能问题。你将学会将"销售报价单"的数据以多维度报表的形式呈现给管理层。
练习题:
- 为"销售报价单"增加一个"打印预览"自定义按钮,点击后弹窗显示格式化内容
- 在列表插件中增加一个"批量作废"自定义按钮,批量将选中单据状态改为"已失效"
- 开发一个保存服务插件,当报价总金额超过100万时,自动将审批人设置为总经理(权限组)