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
|
{
|
/// <summary>
|
/// 收料单数据管理类(处理ERP收料单数据同步到MES收料单表)
|
/// 关联主表:MesInvItemArn(MES收料单主表)
|
/// 关联明细表:MesInvItemArnDetail(MES收料单明细表)
|
/// 关联ERP DTO:ErpSltz(聚合类,含主表+明细)、ErpSltza(ERP收料单主表)、ErpSltzBList(ERP收料单明细)
|
/// </summary>
|
public class MesInvItemArnManager : Repository<MesInvItemArn>
|
{
|
/// <summary>
|
/// 批量保存收料单(主表+明细)- 事务保证批量操作一致性
|
/// 单条失败则整体回滚
|
/// </summary>
|
public bool SaveList(List<ErpSltz> 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;
|
}
|
|
/// <summary>
|
/// 对外单条保存(内部自动事务)
|
/// </summary>
|
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;
|
}
|
|
/// <summary>
|
/// 事务内单条保存
|
/// </summary>
|
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<ErpSltzBList>();
|
|
// 通过 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-删除)")
|
};
|
}
|
|
/// <summary>
|
/// 获取或生成主表ID(按 ERP DeliveryNo 唯一)
|
/// </summary>
|
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<MesInvItemArn>()
|
.Where(m => m.BillNo == billNo)
|
.First();
|
|
return existingMain?.Id ?? Guid.NewGuid();
|
}
|
|
/// <summary>
|
/// 新增/更新(主表+明细:先删旧明细再插新)
|
/// </summary>
|
private bool HandleSaveOrUpdate(SqlSugarScope db, MesInvItemArn mesMain, List<MesInvItemArnDetail> mesDetails,
|
Guid mainId)
|
{
|
mesMain.Id = mainId;
|
|
bool isMainExist = db.Queryable<MesInvItemArn>()
|
.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<MesInvItemArnDetail>()
|
.Where(d => d.Mid == mainId)
|
.ExecuteCommand();
|
|
int insertNewDetailsResult = mesDetails.Count > 0
|
? db.Insertable(mesDetails).ExecuteCommand()
|
: 1;
|
|
return mainOperateResult > 0 && deleteOldDetailsResult >= 0 && insertNewDetailsResult > 0;
|
}
|
|
/// <summary>
|
/// 删除(先删明细再删主表)
|
/// </summary>
|
private bool HandleDelete(SqlSugarScope db, Guid mainId)
|
{
|
db.Deleteable<MesInvItemArnDetail>()
|
.Where(d => d.Mid == mainId)
|
.ExecuteCommand();
|
|
int deleteMainResult = db.Deleteable<MesInvItemArn>()
|
.Where(m => m.Id == mainId)
|
.ExecuteCommand();
|
|
return deleteMainResult >= 0;
|
}
|
|
/// <summary>
|
/// ERP 主表 -> MES 主表
|
/// </summary>
|
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<MesSupplier>()
|
.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)
|
};
|
}
|
|
/// <summary>
|
/// ERP 明细 -> MES 明细
|
/// 核心:通过 ERP ProductCode 匹配 mes_items.item_no 获取 item_id
|
/// </summary>
|
private List<MesInvItemArnDetail> MapErpDetailsToMesDetails(SqlSugarScope db, List<ErpSltzBList> 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<MesItems>()
|
.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<MesInvItemArnDetail>(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;
|
}
|
|
/// <summary>
|
/// 按单据号查询(主+明细)
|
/// </summary>
|
public (MesInvItemArn main, List<MesInvItemArnDetail> details) GetByBillNo(string billNo)
|
{
|
if (string.IsNullOrWhiteSpace(billNo))
|
throw new ArgumentNullException(nameof(billNo), "查询条件单据号不能为空");
|
|
var no = billNo.Trim();
|
|
// SqlSugar: First()
|
var main = Context.Queryable<MesInvItemArn>()
|
.Where(m => m.BillNo == no)
|
.First();
|
|
var details = main != null
|
? Context.Queryable<MesInvItemArnDetail>().Where(d => d.Mid == main.Id).ToList()
|
: new List<MesInvItemArnDetail>();
|
|
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
|
}
|
}
|