金蝶云星空ERP采购申请单下推采购订单中显示PLM图纸版本号

目的:采购员导出的采购订单给供应商图纸 但是几个月后图纸有更新, 采购员忘记给供应商或者不知有新的图纸不能及时知会供应商,供应商按旧的图纸生产造成损耗!有了这个功能后导出的采购订单直接带有图纸版本号,发给供应商直接携带过去!
效果如下:

图片[1]-金蝶云星空ERP采购申请单下推采购订单中显示PLM图纸版本号-Dgcity

上图是采购申请单下推过来的采购订单显示,下图是套打显示

图片[2]-金蝶云星空ERP采购申请单下推采购订单中显示PLM图纸版本号-Dgcity

条件

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

例:物料 H228-0958-B0 → 图纸 H228-0958-B0.dwg,PLM 已发行最新版本为 A/2 → 订单明细 F_PLM_VERSION = A/2

二、目录

  • PRToPOConvertPlugIn.cs:插件源码(核心)
  • K3PlmVersionPlugin.csproj:VS 工程文件

三、编译

  1. 用 Visual Studio 打开/编译本工程(或用 dotnet build / msbuild)。
  2. 修改 K3PlmVersionPlugin.csproj 里 4 个 HintPath,指向你本机金蝶站点 bin,例如:
  • C:\Kingdee\K3Cloud\WebSite\bin\
  • C:\Program Files (x86)\Kingdee\K3Cloud\WebSite\bin\
  1. 编译产出 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 取最大。

五、注册到单据转换

  1. 登录金蝶云星空 BOS 设计器 / Web BOS
  2. 动态建模 → 业务流 → 单据转换
  3. 找到「采购申请单 → 采购订单」的转换规则(若有多个,选实际下推用的那条)。
  4. 打开规则 → 切到 插件 页签 → 新增:
  • 类型:.NET 插件
  • 类名(全名):MyCompany.K3Cloud.PLM.PRToPOConvertPlugIn
  • 程序集:K3PlmVersionPlugin(不带 .dll)
  1. 保存、签入、构建(发布)。
  2. K3PlmVersionPlugin.dll 拷到金蝶站点 bin(或 Web.configK3CloudPlugInDir 指定的插件目录)。
  3. 重启 IIS / 应用池。

六、验证

  1. 下推一张含物料 H228-0958-B0 的采购申请单生成采购订单。
  2. 查看订单明细 F_PLM_VERSION 应显示 PLM 已发行最新版本(如 A/2)。
  3. 保存订单后,该值在单据里永久保存。
  4. 排错看日志: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文件,不会开发的可以直接下载使用

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

昵称

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

    暂无评论内容