using Admin.NET.Plugin.AiDOP.Supply; using Yitter.IdGenerator; namespace Admin.NET.Plugin.AiDOP.WorkOrder; /// 工单下达领料单生成(NbrMaster / NbrDetail,Type=SM)。 public class WorkOrderPickBillService : ITransient { private readonly ISqlSugarClient _db; private readonly NumberRuleService _numberRule; private readonly WorkOrderMaterialDetailSyncService _detailSync; public WorkOrderPickBillService( ISqlSugarClient db, NumberRuleService numberRule, WorkOrderMaterialDetailSyncService detailSync) { _db = db; _numberRule = numberRule; _detailSync = detailSync; } public async Task CreateForWorkOrderAsync(long tenantId, string workOrd, string account) { workOrd = workOrd.Trim(); var existing = await FindExistingPickBillAsync(tenantId, workOrd); if (!string.IsNullOrWhiteSpace(existing)) return new PickBillResult { Nbr = existing, Created = false, Message = "领料单已存在" }; var wo = await LoadWorkOrderAsync(tenantId, workOrd) ?? throw Oops.Oh($"工单 {workOrd} 不存在"); await _detailSync.EnsureFromResourceCheckAsync(tenantId, workOrd, account); var details = await LoadPickDetailsAsync(tenantId, workOrd); if (details.Count == 0) throw Oops.Oh($"工单 {workOrd} 无物料明细,请先完成订单评审资源检查"); var domain = string.IsNullOrWhiteSpace(wo.Domain) ? tenantId.ToString() : wo.Domain.Trim(); var lineTo = await ResolveLineSideLocationAsync(tenantId, wo.ItemNum); var now = DateTime.Now; var effDate = wo.OrdDate?.Date ?? now.Date; var pickDate = effDate < now.Date ? now.Date : effDate; string nbrNo; try { var numbers = await _numberRule.NextBatchInCurrentTransactionAsync("SM", domain, 1, account); nbrNo = numbers.FirstOrDefault() ?? throw Oops.Oh("领料单号生成失败"); } catch { nbrNo = "SM" + now.ToString("yyyyMMddHHmmss") + YitIdHelper.NextId().ToString()[^5..]; } var masterRecId = await NextRecIdAsync("NbrMaster"); await _db.Ado.ExecuteCommandAsync( """ INSERT INTO NbrMaster ( RecID, `Domain`, Type, Nbr, Status, Remark, Date, EffDate, WorkOrd, QtyOrd, QtyRec, ProdLine, Department, Name, TransType, IsActive, IsChanged, IsConfirm, BusinessID, CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id ) VALUES ( @RecId, @Domain, 'SM', @Nbr, '', @Remark, @PickDate, @EffDate, @WorkOrd, @QtyOrd, 0, @ProdLine, '101', @Name, '', 1, 1, 0, 0, @User, @Now, @User, @Now, @TenantId ) """, new SugarParameter("@RecId", masterRecId), new SugarParameter("@Domain", domain.Length > 8 ? domain[..8] : domain), new SugarParameter("@Nbr", nbrNo), new SugarParameter("@Remark", "下达自动领料"), new SugarParameter("@PickDate", pickDate), new SugarParameter("@EffDate", effDate), new SugarParameter("@WorkOrd", workOrd), new SugarParameter("@QtyOrd", wo.QtyOrded ?? 0), new SugarParameter("@ProdLine", wo.ProdLine ?? (object)DBNull.Value), new SugarParameter("@Name", account.Length > 12 ? account[..12] : account), new SugarParameter("@User", account.Length > 24 ? account[..24] : account), new SugarParameter("@Now", now), new SugarParameter("@TenantId", tenantId)); short line = 1; foreach (var d in details) { var detailRecId = await NextRecIdAsync("NbrDetail"); await _db.Ado.ExecuteCommandAsync( """ INSERT INTO NbrDetail ( RecID, `Domain`, Type, Nbr, Line, ItemNum, Dimension1, Dimension2, LocationFrom, LocationTo, QtyFrom, QtyTo, QtyOrd, QtyRec, CurrQtyOpened, UM, WorkOrd, ItemName, Status, IsActive, IsConfirm, IsChanged, BusinessID, NbrRecID, CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id ) VALUES ( @RecId, @Domain, 'SM', @Nbr, @Line, @ItemNum, '', '', @LocationFrom, @LocationTo, 0, 0, @QtyOrd, 0, @CurrQtyOpened, @UM, @WorkOrd, @ItemName, '', 1, 0, 1, 0, @NbrRecId, @User, @Now, @User, @Now, @TenantId ) """, new SugarParameter("@RecId", detailRecId), new SugarParameter("@Domain", domain.Length > 8 ? domain[..8] : domain), new SugarParameter("@Nbr", nbrNo), new SugarParameter("@Line", line), new SugarParameter("@ItemNum", d.ItemNum), new SugarParameter("@LocationFrom", d.LocationFrom ?? (object)DBNull.Value), new SugarParameter("@LocationTo", lineTo ?? (object)DBNull.Value), new SugarParameter("@QtyOrd", d.QtyRequired), new SugarParameter("@CurrQtyOpened", d.QtyRequired), new SugarParameter("@UM", d.Unit ?? (object)DBNull.Value), new SugarParameter("@WorkOrd", workOrd), new SugarParameter("@ItemName", d.ItemName ?? (object)DBNull.Value), new SugarParameter("@NbrRecId", masterRecId), new SugarParameter("@User", account.Length > 24 ? account[..24] : account), new SugarParameter("@Now", now), new SugarParameter("@TenantId", tenantId)); line++; } await _db.Ado.ExecuteCommandAsync( """ UPDATE mes_morder SET morder_state = '下达', MaterialSituation = CASE WHEN MaterialSituation = '缺料' THEN '未备料' ELSE IFNULL(MaterialSituation, '齐套') END, update_by_name = @User, update_time = @Now WHERE tenant_id = @TenantId AND morder_no = @WorkOrd AND IsDeleted = 0 """, new SugarParameter("@User", account), new SugarParameter("@Now", now), new SugarParameter("@TenantId", tenantId), new SugarParameter("@WorkOrd", workOrd)); return new PickBillResult { Nbr = nbrNo, Created = true, LineCount = details.Count, Message = "领料单已生成" }; } private async Task FindExistingPickBillAsync(long tenantId, string workOrd) { return await _db.Ado.GetStringAsync( """ SELECT Nbr FROM NbrMaster WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd AND Type = 'SM' AND IFNULL(TransType, '') = '' ORDER BY RecID DESC LIMIT 1 """, new List { new("@TenantId", tenantId), new("@WorkOrd", workOrd) }); } private async Task LoadWorkOrderAsync(long tenantId, string workOrd) { var rows = await _db.Ado.SqlQueryAsync( """ SELECT w.RecID AS RecId, w.`Domain`, w.ItemNum, w.QtyOrded, w.OrdDate, ( SELECT LEFT(IFNULL(r.ProdLine, ''), 8) FROM WorkOrdRouting r WHERE r.WorkOrd = w.WorkOrd AND r.tenant_id = w.tenant_id AND IFNULL(r.IsActive, 0) = 1 ORDER BY (r.OP + 0) LIMIT 1 ) AS ProdLine FROM WorkOrdMaster w WHERE w.tenant_id = @TenantId AND w.WorkOrd = @WorkOrd LIMIT 1 """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@WorkOrd", workOrd)); return rows.FirstOrDefault(); } private async Task> LoadPickDetailsAsync(long tenantId, string workOrd) { return await _db.Ado.SqlQueryAsync( """ SELECT d.ItemNum, SUM(d.QtyRequired) AS QtyRequired, MAX(IFNULL(d.Location, im.Location)) AS LocationFrom, MAX(IFNULL(d.UM, im.Um)) AS Unit, MAX(im.Descr) AS ItemName FROM WorkOrdDetail d LEFT JOIN ItemMaster im ON d.ItemNum = im.ItemNum WHERE d.tenant_id = @TenantId AND d.WorkOrd = @WorkOrd AND IFNULL(d.IsActive, 0) = 1 GROUP BY d.ItemNum HAVING SUM(d.QtyRequired) > 0 ORDER BY d.ItemNum """, new SugarParameter("@TenantId", tenantId), new SugarParameter("@WorkOrd", workOrd)); } private async Task ResolveLineSideLocationAsync(long tenantId, string? itemNum) { if (string.IsNullOrWhiteSpace(itemNum)) return null; var loc = await _db.Ado.GetStringAsync( """ SELECT LEFT(IFNULL(lm.Location, lm.PickingLocation), 8) FROM ProdLineDetail pld INNER JOIN LineMaster lm ON pld.Line = lm.Line AND lm.tenant_id = pld.tenant_id WHERE pld.Part = @ItemNum AND pld.tenant_id = @TenantId AND IFNULL(pld.IsActive, 1) = 1 ORDER BY IFNULL(pld.Sequence, 0) LIMIT 1 """, new List { new("@ItemNum", itemNum.Trim()), new("@TenantId", tenantId) }); return string.IsNullOrWhiteSpace(loc) ? null : loc; } private async Task NextRecIdAsync(string table) { return await _db.Ado.GetIntAsync($"SELECT IFNULL(MAX(RecID), 0) + 1 FROM {table}"); } public sealed class PickBillResult { public string Nbr { get; set; } = string.Empty; public bool Created { get; set; } public int LineCount { get; set; } public string Message { get; set; } = string.Empty; } private sealed class WorkOrderPickRow { public long RecId { get; set; } public string? Domain { get; set; } public string? ItemNum { get; set; } public decimal? QtyOrded { get; set; } public DateTime? OrdDate { get; set; } public string? ProdLine { get; set; } } private sealed class PickDetailRow { public string ItemNum { get; set; } = string.Empty; public decimal QtyRequired { get; set; } public string? LocationFrom { get; set; } public string? Unit { get; set; } public string? ItemName { get; set; } } }