目的:采购员导出的采购订单给供应商图纸 但是几个月后图纸有更新, 采购员忘记给供应商或者不知有新的图纸不能及时知会供应商,供应商按旧的图纸生产造成损耗!有了这个功能后导出的采购订单直接带有图纸版本号,发给供应商直接携带过去!
效果如下:
![图片[1]-金蝶云星空ERP采购申请单下推采购订单中显示PLM图纸版本号-Dgcity](https://dgcity.com/wp-content/uploads/2026/08/20260824084205805-image.png)
上图是采购申请单下推过来的采购订单显示,下图是套打显示
![图片[2]-金蝶云星空ERP采购申请单下推采购订单中显示PLM图纸版本号-Dgcity](https://dgcity.com/wp-content/uploads/2026/08/20260824084114654-image.png)
条件
1、本插件是在金蝶云星空9.1版本下实现,其他版本没验证,但估计可行!
2、图纸名和物料编码需一致,如上面的物料编码H227-1912-AH在PLM文件库中上传的图纸物理文件名必须为H227-1912-AH.dwg.
3、因为我司的采购订单须由采购申请单下推,所以插件是在采购申请单下推采购订单的单据转换中增加
4、只显示PLM文件档中的图纸文件中是已发行的版本号!
源码说明
采购申请单 → 采购订单 转换插件(PLM 版本写入 F_PLM_VERSION)
一、功能
采购申请单下推生成采购订单时,按「物料编码 = PLM 图纸名(去 .dwg)」取 PLM 中已发行的最新图纸版本,
写入采购订单明细字段 F_PLM_VERSION(此字段需在采购订单自行增加,名称叫PLM图纸版本号,字段标识改为 F_PLM_VERSION),保存后永久保存。
![图片[3]-金蝶云星空ERP采购申请单下推采购订单中显示PLM图纸版本号-Dgcity](https://dgcity.com/wp-content/uploads/2026/08/20260824090435633-image.png)
例:物料 H228-0958-B0 → 图纸 H228-0958-B0.dwg,PLM 已发行最新版本为 A/2 → 订单明细 F_PLM_VERSION = A/2。
二、目录
PRToPOConvertPlugIn.cs:插件源码(核心)K3PlmVersionPlugin.csproj:VS 工程文件
三、编译
- 用 Visual Studio 打开/编译本工程(或用
dotnet build/msbuild)。 - 修改
K3PlmVersionPlugin.csproj里 4 个HintPath,指向你本机金蝶站点 bin,例如:
C:\Kingdee\K3Cloud\WebSite\bin\- 或
C:\Program Files (x86)\Kingdee\K3Cloud\WebSite\bin\
- 编译产出
K3PlmVersionPlugin.dll。
四、配置插件(按实际 PLM 改源码)
打开 PRToPOConvertPlugIn.cs 顶部「配置区」:
TargetEntryEntityKey:采购订单明细实体 Key,默认FPOOrderEntry(本账套实测值,带 F 前缀)。插件会先按此 Key 取,取不到再自动遍历所有实体、找含物料字段的那个作为明细,无需硬编码。TargetVersionFieldKey:采购订单明细上自定义文本字段 Key,默认F_PLM_VERSION(已实测正确)。PlmCodeField:PLM 图纸编码字段,默认FCODE(= 物料编码,不带 .dwg)。PlmReleasedFilter:留空 = 取该图纸的【最大版本号】(等同 PLM 文档库显示的已发行最新版)。
若需严格限定某生命周期阶段,可填例如AND FLIFECIRCLESTAGE='AJ'(值以你 PLM 实际为准)。PlmConnectionString:留空 = 用当前账套库(本机即 AIS20260604094951)。PLM 独立库时填连接串。
已实库核对(AIS20260604094951):
- 图纸编码:
T_PLM_PDM_BASEVERSION.FCODE(不带 .dwg);物理文件:T_PLM_PDM_PHYSICALFILE.FFILENAME(如 H227-1806-BH.dwg)。 - 版本号:
T_PLM_PDM_BASEVERSION.FVERNO(如 A/3),每个版本一行;FMAXVERNO与之相同。 - 取数 SQL(插件内已用):
SELECT FVERNO FROM T_PLM_PDM_BASEVERSION
WHERE FCODE IN ('物料编码','物料编码.dwg') AND FVERNO IS NOT NULL AND RTRIM(FVERNO)<>''
多个版本由插件 PickLatestVersion 按 A/3 > A/2 > A/0 取最大。
五、注册到单据转换
- 登录金蝶云星空 BOS 设计器 / Web BOS。
- 动态建模 → 业务流 → 单据转换。
- 找到「采购申请单 → 采购订单」的转换规则(若有多个,选实际下推用的那条)。
- 打开规则 → 切到 插件 页签 → 新增:
- 类型:.NET 插件
- 类名(全名):
MyCompany.K3Cloud.PLM.PRToPOConvertPlugIn - 程序集:
K3PlmVersionPlugin(不带 .dll)
- 保存、签入、构建(发布)。
- 把
K3PlmVersionPlugin.dll拷到金蝶站点bin(或Web.config中K3CloudPlugInDir指定的插件目录)。 - 重启 IIS / 应用池。
六、验证
- 下推一张含物料
H228-0958-B0的采购申请单生成采购订单。 - 查看订单明细
F_PLM_VERSION应显示 PLM 已发行最新版本(如A/2)。 - 保存订单后,该值在单据里永久保存。
- 排错看日志:
C:\Temp\PLMVersionPlugin.log(如目录不存在会自动创建)。
七、关键假设(请确认)
- PLM 数据位置:本插件默认 PLM 数据可通过 SQL 在同账套库或独立库查到。
若 PLM 是独立系统(Web 服务),请把UseSqlLookup设为false并实现GetPlmVersionByWebApi。 - F_PLM_VERSION 建在明细:因版本按物料,必须建在采购订单单据体,不要建在单据头。
源码
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using Kingdee.BOS;
using Kingdee.BOS.App.Data;
using Kingdee.BOS.Core;
using Kingdee.BOS.Core.Metadata;
using Kingdee.BOS.Core.Metadata.ConvertElement.PlugIn;
using Kingdee.BOS.Core.Metadata.ConvertElement.PlugIn.Args;
using Kingdee.BOS.Orm.DataEntity;
using Kingdee.BOS.Core.Metadata.EntityElement;
using Kingdee.BOS.Core.Metadata.FieldElement;
namespace MyCompany.K3Cloud.PLM
{
/// <summary>
/// 采购申请单 -> 采购订单 单据转换插件
/// 在下推生成采购订单时,按“物料编码 = PLM 图纸编码(去 .dwg)”取 PLM 中【已发行】的【最新】图纸版本,
/// 写入采购订单明细字段 F_PLM_VERSION,保存后永久保存。
///
/// 已对 AIS20260604094951 实库验证(H227-1806-BH=A/3、H227-1809-BH=A/2、H227-1907-AH=A/0、H227-1736-BH=A/1 全部命中):
/// 版本字段 -> T_PLM_PDM_BASEVERSION.FVERNO(如 A/3),FVerNO 即“(文档)版本号”
/// 匹配路径 -> 两条路都兼容:
/// (1) 物料编码 = 版本表/主表 FCODE(生产库直接相等);
/// (2) 物料编码 = 图纸名 T_PLM_PDM_BASE_L.FNAME(或 物料号.dwg),经 主表FID -> 主表FCODE -> 版本表FCODE。
/// “最新版本” = 该图纸所有版本中的最大版本号(A/3 > A/2 > A/0,A/B/C 字母升序,数字越大越新)。
/// </summary>
public class PRToPOConvertPlugIn : AbstractConvertPlugIn
{
#region ====== 配置区(一般无需改,除非你的PLM表/字段命名不同)======
/// <summary>采购订单明细(单据体)实体 Key。采购订单明细一般为 FPOOrderEntry(注意带 F 前缀)。</summary>
private const string TargetEntryEntityKey = "FPOOrderEntry";
/// <summary>目标字段 Key(采购订单明细上已新增的文本字段)。</summary>
private const string TargetVersionFieldKey = "F_PLM_VERSION";
/// <summary>PLM 图纸编码字段(= 物料编码)。</summary>
private const string PlmCodeField = "FCODE";
/// <summary>已发行过滤:留空 = 直接取该图纸的“最大版本号”(推荐,等同PLM文档库显示的已发行最新版)。
/// 若需严格限定某生命周期阶段,可填例如 "AND FLIFECIRCLESTAGE='AJ'"(值以你PLM实际为准)。</summary>
private const string PlmReleasedFilter = "";
/// <summary>PLM 数据库连接串。留空 = 使用当前金蝶账套数据库(this.Context 自动指向当前账套,本机即 AIS20260604094951)。
/// 若 PLM 在独立库,填其连接串。</summary>
private const string PlmConnectionString = "";
/// <summary>日志文件路径(调试用)。默认关闭,需要时把 EnableLog 改为 true。</summary>
private const string LogPath = @"C:\Temp\PLMVersionPlugin.log";
/// <summary>是否开启调试日志。生产环境保持 false;排查问题临时改 true 后重启 IIS 即可。</summary>
private const bool EnableLog = false;
#endregion
public override void OnAfterCreateLink(CreateLinkEventArgs e)
{
base.OnAfterCreateLink(e);
if (e == null || e.TargetExtendedDataEntities == null) return;
var targetBI = e.TargetBusinessInfo;
// 定位明细实体:优先用配置键,找不到则自动识别“含 MaterialId 字段”的实体
Entity entryEntity = FindEntryEntity(targetBI);
if (entryEntity == null)
{
var sb = new System.Text.StringBuilder();
sb.Append("未找到明细实体。实体清单: ");
foreach (var en in targetBI.Entrys)
if (en != null) sb.Append(en.Key + " ");
WriteLog(sb.ToString());
return;
}
WriteLog("使用明细实体:" + entryEntity.Key);
// 校验目标版本字段是否存在
var verField = FindField(entryEntity, TargetVersionFieldKey);
if (verField == null)
{
var sbv = new System.Text.StringBuilder();
sbv.Append("未找到版本字段 " + TargetVersionFieldKey + "。字段清单: ");
foreach (var f in entryEntity.Fields) sbv.Append(f.Key + " ");
WriteLog(sbv.ToString());
return;
}
// 定位物料字段(用于读取物料编码),用其动态属性名读取
var matField = FindField(entryEntity, "FMaterialId", "MaterialId");
if (matField == null)
{
var sbm = new System.Text.StringBuilder();
sbm.Append("未找到物料字段(FMaterialId/MaterialId)。字段清单: ");
foreach (var f in entryEntity.Fields) sbm.Append(f.Key + " ");
WriteLog(sbm.ToString());
return;
}
WriteLog("物料字段动态属性名:" + matField.PropertyName);
int hit = 0;
// 路径A:从单据头根数据包导航到明细
string rootKey = targetBI.GetEntity(0).Key;
ExtendedDataEntity[] roots = e.TargetExtendedDataEntities.FindByEntityKey(rootKey);
if (roots == null || roots.Length == 0)
roots = e.TargetExtendedDataEntities.FindByEntityKey(string.Empty);
if (roots != null)
{
foreach (var rootEntity in roots)
{
var root = rootEntity.DataEntity as DynamicObject;
if (root == null) continue;
var entries = entryEntity.DynamicProperty.GetValue(root) as DynamicObjectCollection;
if (entries == null) continue;
hit += ProcessEntries(entries, verField, matField);
}
}
// 路径B:若路径A无命中,直接按明细实体 Key 取明细数据包
if (hit == 0)
{
ExtendedDataEntity[] entryDEs = e.TargetExtendedDataEntities.FindByEntityKey(entryEntity.Key);
if (entryDEs != null)
{
var list = new List<DynamicObject>();
foreach (var ede in entryDEs)
{
var entry = ede.DataEntity as DynamicObject;
if (entry != null) list.Add(entry);
}
hit += ProcessEntries(list, verField, matField);
}
}
if (hit == 0)
WriteLog("未取到任何明细数据(路径A/B均无命中),请确认下推是否生成了明细行");
else
WriteLog(string.Format("本次共写入版本 {0} 行", hit));
}
private int ProcessEntries(System.Collections.Generic.IEnumerable<DynamicObject> entries, Field verField, Field matField)
{
int n = 0;
if (entries == null) return n;
string matProp = (matField != null && !string.IsNullOrEmpty(matField.PropertyName)) ? matField.PropertyName : "MaterialId";
foreach (var entry in entries)
{
DynamicObject material = null;
try { material = entry[matProp] as DynamicObject; } catch { material = null; }
if (material == null) { try { material = entry["MaterialId"] as DynamicObject; } catch { material = null; } }
string materialNumber = GetMaterialNumber(material);
if (string.IsNullOrWhiteSpace(materialNumber)) continue;
string version = GetLatestReleasedVersion(materialNumber);
if (!string.IsNullOrWhiteSpace(version))
{
try
{
entry[verField.PropertyName] = version;
n++;
WriteLog(string.Format("物料 {0} -> PLM版本 {1}", materialNumber, version));
}
catch (Exception ex)
{
WriteLog(string.Format("写入版本失败(物料 {0}): {1}", materialNumber, ex.Message));
}
}
}
return n;
}
private Entity FindEntryEntity(BusinessInfo bi)
{
// 配置键优先
var en = bi.GetEntity(TargetEntryEntityKey);
if (en != null) return en;
// 自动识别:遍历实体,找“含物料字段(FMaterialId/MaterialId)且非根”的明细实体
string rootKey = bi.GetEntity(0) != null ? bi.GetEntity(0).Key : "";
foreach (var e2 in bi.Entrys)
{
if (e2 == null) continue;
if (!string.IsNullOrEmpty(rootKey) && e2.Key == rootKey) continue;
if (FindField(e2, "FMaterialId", "MaterialId") != null) return e2;
}
return null;
}
private Field FindField(Entity entity, params string[] keys)
{
if (entity == null || entity.Fields == null) return null;
foreach (var f in entity.Fields)
foreach (var k in keys)
if (string.Equals(f.Key, k, StringComparison.OrdinalIgnoreCase)) return f;
return null;
}
private string GetMaterialNumber(DynamicObject material)
{
if (material == null) return null;
object num = material["Number"] ?? material["FNumber"];
return num == null ? null : num.ToString().Trim();
}
private string GetLatestReleasedVersion(string materialNumber)
{
return GetPlmVersionBySql(materialNumber);
}
/// <summary>取 PLM 最新版本:优先 T_PLM_PDM_BASEVERSION.FVERNO(该图所有版本取最大),
/// 兜底 T_PLM_PDM_BASE.FMAXVERNO / FVERNO。匹配走双路径(FCODE 直接相等 或 经图纸名关联)。</summary>
private string GetPlmVersionBySql(string materialNumber)
{
try
{
string drawingNo = materialNumber;
if (drawingNo.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase))
drawingNo = drawingNo.Substring(0, drawingNo.Length - 4);
var versions = new List<string>();
// 1) 主路径:版本表全部版本,C# 取最大
versions.AddRange(RunSqlReturnStrings(BuildRobustSql("T_PLM_PDM_BASEVERSION", "FVERNO", drawingNo)));
// 2) 兜底:主表大版本号
if (versions.Count == 0)
versions.AddRange(RunSqlReturnStrings(BuildRobustSql("T_PLM_PDM_BASE", "FMAXVERNO", drawingNo)));
// 3) 兜底:主表当前版本号
if (versions.Count == 0)
versions.AddRange(RunSqlReturnStrings(BuildRobustSql("T_PLM_PDM_BASE", "FVERNO", drawingNo)));
return PickLatestVersion(versions);
}
catch (Exception ex)
{
WriteLog("GetPlmVersionBySql 异常:" + ex.Message);
return null;
}
}
/// <summary>
/// 构造鲁棒取版 SQL。匹配路径二选一:
/// (a) t.FCODE 直接 = 物料号(生产库常见);
/// (b) 或 经 文档多语言名表 T_PLM_PDM_BASE_L.FNAME(= 物料号 或 物料号.dwg)
/// -> 主表 T_PLM_PDM_BASE.FID -> 主表 FCODE -> 版本表 FCODE。
/// 文档主表与版本表共用同一 FCODE,故用 FCODE 桥接。
/// </summary>
private string BuildRobustSql(string table, string verField, string drawingNo)
{
string d = drawingNo.Replace("'", "''");
string codeField = PlmCodeField; // 一般为 FCODE
string filter = string.IsNullOrWhiteSpace(PlmReleasedFilter) ? "" : " " + PlmReleasedFilter;
return string.Format(
"SELECT {0} FROM {1} t WITH (NOLOCK) " +
"WHERE ( (t.{2} = '{3}' OR t.{2} IN ( " +
" SELECT b.{2} FROM T_PLM_PDM_BASE b WITH (NOLOCK) " +
" WHERE b.FID IN (SELECT FID FROM T_PLM_PDM_BASE_L WITH (NOLOCK) " +
" WHERE FNAME IN ('{3}','{3}.dwg','{3}.DWG')) ) ) ) " +
"AND {0} IS NOT NULL AND RTRIM({0}) <> ''{4}",
verField, table, codeField, d, filter);
}
private List<string> RunSqlReturnStrings(string sql)
{
var list = new List<string>();
if (string.IsNullOrWhiteSpace(PlmConnectionString))
{
#pragma warning disable 0618
// 当前金蝶账套数据库(本机账套 AIS20260604094951)
DataSet ds = DBUtils.ExecuteDataSet(this.Context, sql);
#pragma warning restore 0618
if (ds != null && ds.Tables.Count > 0)
{
foreach (DataRow row in ds.Tables[0].Rows)
if (row[0] != DBNull.Value)
list.Add(row[0].ToString());
}
}
else
{
using (var conn = new System.Data.SqlClient.SqlConnection(PlmConnectionString))
using (var cmd = new System.Data.SqlClient.SqlCommand(sql, conn))
{
conn.Open();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
if (!reader.IsDBNull(0))
list.Add(reader[0].ToString());
}
}
}
return list;
}
/// <summary>从多个版本中挑“最新”。支持 A/3、B/10 这类“字母/数字”格式;A<B<...,数字越大越新。</summary>
private string PickLatestVersion(List<string> versions)
{
if (versions == null || versions.Count == 0) return null;
string latest = versions[0];
foreach (var v in versions)
if (CompareVersion(v, latest) > 0) latest = v;
return latest;
}
private int CompareVersion(string a, string b)
{
var pa = ParseVersion(a);
var pb = ParseVersion(b);
int c = string.Compare(pa.Major, pb.Major, StringComparison.OrdinalIgnoreCase);
if (c != 0) return c;
return pa.Minor.CompareTo(pb.Minor);
}
private VerParts ParseVersion(string v)
{
var result = new VerParts();
if (string.IsNullOrWhiteSpace(v)) return result;
// 兼容 A/3、A.3、A-3 写法
string s = v.Replace('.', '/').Replace('-', '/').Trim();
var parts = s.Split('/');
result.Major = parts[0].Trim();
int m;
if (parts.Length > 1 && int.TryParse(parts[1].Trim(), out m)) result.Minor = m;
return result;
}
private class VerParts
{
public string Major = "";
public int Minor = 0;
}
private void WriteLog(string msg)
{
if (!EnableLog) return;
try
{
Directory.CreateDirectory(Path.GetDirectoryName(LogPath));
File.AppendAllText(LogPath,
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " " + msg + "\r\n");
}
catch { }
}
}
}
附件为开发源码以及成品的DLL文件,不会开发的可以直接下载使用














暂无评论内容