kyy
5 天以前 f476ec010c22cd4e3c6a119eea035cbf4594bfbb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
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
    }
}