using MES.Service.DB; using MES.Service.Dto.webApi; using MES.Service.Modes; using MES.Service.util; using SqlSugar; using System; using System.Collections.Generic; using System.Linq; namespace MES.Service.service { /// /// 收料单数据管理类(处理ERP收料单数据同步到MES收料单表) /// 关联主表:MesInvItemArn(MES收料单主表) /// 关联明细表:MesInvItemArnDetail(MES收料单明细表) /// 关联ERP DTO:ErpSltz(聚合类,含主表+明细)、ErpSltza(ERP收料单主表)、ErpSltzBList(ERP收料单明细) /// public class MesInvItemArnManager : Repository { /// /// 批量保存收料单(主表+明细)- 事务保证批量操作一致性 /// 单条失败则整体回滚 /// public bool SaveList(List erpSltzList) { if (erpSltzList == null || !erpSltzList.Any()) throw new ArgumentNullException(nameof(erpSltzList), "待保存的收料单列表不能为空"); return UseTransaction(db => { foreach (var erpSltz in erpSltzList) { if (!SaveInTransaction(db, erpSltz)) return 0; // 触发回滚 } return 1; // 提交 }) > 0; } /// /// 对外单条保存(内部自动事务) /// public bool Save(ErpSltz erpSltz) { if (erpSltz == null) throw new ArgumentNullException(nameof(erpSltz), "收料单数据不能为空"); if (erpSltz.ErpSltza == null) throw new ArgumentNullException(nameof(erpSltz.ErpSltza), "收料单主表数据不能为空"); return UseTransaction(db => SaveInTransaction(db, erpSltz) ? 1 : 0) > 0; } /// /// 事务内单条保存 /// private bool SaveInTransaction(SqlSugarScope db, ErpSltz erpSltz) { if (erpSltz == null) throw new ArgumentNullException(nameof(erpSltz), "收料单数据不能为空"); if (erpSltz.ErpSltza == null) throw new ArgumentNullException(nameof(erpSltz.ErpSltza), "收料单主表数据不能为空"); var erpMain = erpSltz.ErpSltza; var erpDetails = erpSltz.ErpSltzBList ?? new List(); // 通过 ERP 单号确定/生成主表ID Guid mainId = GetOrCreateMainId(db, erpMain); // 映射 var mesMain = MapErpMainToMesMain(erpMain, mainId,db); var mesDetails = MapErpDetailsToMesDetails(db, erpDetails, mainId); // 类型:1 新增,2 更新,3 删除 var type = TrimOrNull(erpMain.TYPE); return type switch { "1" or "2" => HandleSaveOrUpdate(db, mesMain, mesDetails, mainId), "3" => HandleDelete(db, mainId), _ => throw new NotImplementedException($"未实现的收料单操作类型:{erpMain.TYPE}(支持:1-新增,2-更新,3-删除)") }; } /// /// 获取或生成主表ID(按 ERP DeliveryNo 唯一) /// private Guid GetOrCreateMainId(SqlSugarScope db, ErpSltza erpMain) { var billNo = TrimOrNull(erpMain.DeliveryNo) ?? throw new ArgumentException("ERP收料单单据号(DeliveryNo)不能为空,无法确定主表唯一性", nameof(erpMain.DeliveryNo)); // SqlSugar: 用 First(),无数据返回 null var existingMain = db.Queryable() .Where(m => m.BillNo == billNo) .First(); return existingMain?.Id ?? Guid.NewGuid(); } /// /// 新增/更新(主表+明细:先删旧明细再插新) /// private bool HandleSaveOrUpdate(SqlSugarScope db, MesInvItemArn mesMain, List mesDetails, Guid mainId) { mesMain.Id = mainId; bool isMainExist = db.Queryable() .Where(m => m.Id == mainId) .Any(); int mainOperateResult; if (isMainExist) { mainOperateResult = db.Updateable(mesMain) .IgnoreColumns(m => new { m.CreateDate }) .Where(m => m.Id == mainId) .ExecuteCommand(); } else { mesMain.CreateDate = DateTime.Now; mainOperateResult = db.Insertable(mesMain).ExecuteCommand(); } // 明细同步:删旧 + 插新 int deleteOldDetailsResult = db.Deleteable() .Where(d => d.Mid == mainId) .ExecuteCommand(); int insertNewDetailsResult = mesDetails.Count > 0 ? db.Insertable(mesDetails).ExecuteCommand() : 1; return mainOperateResult > 0 && deleteOldDetailsResult >= 0 && insertNewDetailsResult > 0; } /// /// 删除(先删明细再删主表) /// private bool HandleDelete(SqlSugarScope db, Guid mainId) { db.Deleteable() .Where(d => d.Mid == mainId) .ExecuteCommand(); int deleteMainResult = db.Deleteable() .Where(m => m.Id == mainId) .ExecuteCommand(); return deleteMainResult >= 0; } /// /// ERP 主表 -> MES 主表 /// private MesInvItemArn MapErpMainToMesMain(ErpSltza erpMain, Guid mainId, SqlSugarScope db) { var billNo = TrimOrNull(erpMain.DeliveryNo) ?? throw new ArgumentException("收料单单据号不能为空"); // 获取供应商编号并查询对应的ID string suppNo = TrimOrNull(erpMain.SupplierId); decimal ? suppId = null; if (!string.IsNullOrEmpty(suppNo)) { // 从数据库查询供应商ID(SuppNo唯一) suppId = db.Queryable() .Where(s => s.SuppNo == suppNo) .Select(s => s.Id) .First(); // 可选:如果需要严格验证供应商存在性,取消下面注释 if (suppId == null) { throw new KeyNotFoundException($"未找到编号为【{suppNo}】的供应商信息"); } } // 是否 SRM var isSrm = ToInt(erpMain.F_ZJXF_sfgx, 0); // 是否委外:ERP的 fType 为 "1"/"true" 视为委外 var fTypeStr = TrimOrNull(erpMain.fType); bool isOutsourcing = fTypeStr is not null && (fTypeStr == "1" || fTypeStr.Equals("true", StringComparison.OrdinalIgnoreCase)); return new MesInvItemArn { Id = mainId, BillNo = billNo, SuppId = suppId.ToString(), CreateDate = ToDate(erpMain.FDate), // null 表示未知 Remark = TrimOrNull(erpMain.Remark), IsSrm = isSrm, EbelnK3id = TrimOrNull(erpMain.erpId), CreateBy = TrimOrNull(erpMain.createBy) ?? "SYSTEM", FType = isOutsourcing, ReceiveOrgId = TrimOrNull(erpMain.ReceiveOrgId) }; } /// /// ERP 明细 -> MES 明细 /// 核心:通过 ERP ProductCode 匹配 mes_items.item_no 获取 item_id /// private List MapErpDetailsToMesDetails(SqlSugarScope db, List erpDetails, Guid mainId) { var productCodes = erpDetails .Where(d => !string.IsNullOrWhiteSpace(d.ProductCode)) .Select(d => d.ProductCode.Trim()) .Distinct() .ToList(); // 先 ToList 再 ToDictionary,避免直接对 ISugarQueryable 调用拓展方法失败 var itemPairs = db.Queryable() .Where(item => productCodes.Contains(item.ItemNo)) .Select(item => new { item.ItemNo, item.Id }) .ToList(); var itemMap = itemPairs.ToDictionary(k => k.ItemNo, v => v.Id); var list = new List(erpDetails.Count); foreach (var erpDetail in erpDetails) { var lineNo = ToInt(erpDetail.LineNo); var productCode = TrimOrNull(erpDetail.ProductCode); if (productCode == null) throw new ArgumentNullException(nameof(erpDetail.ProductCode), $"ERP收料单明细【行:{lineNo}】的物料编码(ProductCode)不能为空,无法生成明细"); if (!itemMap.TryGetValue(productCode, out var itemId)) throw new KeyNotFoundException( $"ERP收料单明细【行:{lineNo}】的物料编码【{productCode}】在 MES.mes_items 中未找到匹配的 item_no"); var detail = new MesInvItemArnDetail { DeliveryLineID = lineNo, Eid = ToInt(erpDetail.ErpId), Mid = mainId, Ebeln = TrimOrNull(erpDetail.FBillNo), ItemId = ToInt(itemId), EbelnQty = ToDecimal(erpDetail.PurchaseQty), SubQty = ToDecimal(erpDetail.DeliveryQty), Quantity = ToDecimal(erpDetail.IncludeQty), PurchaseUnit = TrimOrNull(erpDetail.PurchaseUnit), InventoryUnit = TrimOrNull(erpDetail.InventoryUnit), Memo = TrimOrNull(erpDetail.Remark), EbelnK3id = ToInt(erpDetail.FSrcBillNo), LineK3id = ToInt(erpDetail.FSrcBillLine), SalesOrderId = TrimOrNull(erpDetail.SalesOrderId), MtoNo = TrimOrNull(erpDetail.FMtoNo), Lot = TrimOrNull(erpDetail.FLot), UrgentFlag = ToBool(erpDetail.urgentFlag), DepotId = erpDetail.DepotId }; list.Add(detail); } return list; } /// /// 按单据号查询(主+明细) /// public (MesInvItemArn main, List details) GetByBillNo(string billNo) { if (string.IsNullOrWhiteSpace(billNo)) throw new ArgumentNullException(nameof(billNo), "查询条件单据号不能为空"); var no = billNo.Trim(); // SqlSugar: First() var main = Context.Queryable() .Where(m => m.BillNo == no) .First(); var details = main != null ? Context.Queryable().Where(d => d.Mid == main.Id).ToList() : new List(); return (main, details); } #region 安全转换/工具方法 private static string TrimOrNull(string s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim(); private static int ToInt(object value, int @default = 0) { switch (value) { case null: return @default; case int i: return i; case long l: return unchecked((int)l); case short sh: return sh; case byte b: return b; case decimal dm: return (int)dm; case double db: return (int)db; case float f: return (int)f; case string s when int.TryParse(s, out var n): return n; default: return @default; } } private static decimal ToDecimal(object value, decimal @default = 0m) { switch (value) { case null: return @default; case decimal dm: return dm; case double db: return (decimal)db; case float f: return (decimal)f; case int i: return i; case long l: return l; case string s when decimal.TryParse(s, out var n): return n; default: return @default; } } private static bool ToBool(object value, bool @default = false) { switch (value) { case null: return @default; case bool b: return b; case int i: return i != 0; case long l: return l != 0; case string s: var t = s.Trim().ToLowerInvariant(); return t switch { "1" => true, "0" => false, "y" => true, "n" => false, "true" => true, "false" => false, _ => @default }; default: return @default; } } private static DateTime? ToDate(object value) { switch (value) { case null: return null; case DateTime dt: return dt; case string s when DateTime.TryParse(s, out var d): return d; default: return null; } } #endregion } }