namespace Admin.NET.Plugin.AiDOP.Supply; /// /// 工序外协订单服务 /// [ApiDescriptionSettings(Order = 311, Description = "工序外协订单")] [Route("api/Supply")] [AllowAnonymous] [NonUnify] public class ProcessOutsourceOrderService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly UserManager _userManager; public ProcessOutsourceOrderService(ISqlSugarClient db, UserManager userManager) { _db = db; _userManager = userManager; } [DisplayName("工序外协订单列表")] [HttpGet("process-outsource-order/list")] public async Task GetList([FromQuery] ProcessOutsourceOrderListInput input) { var page = input.Page <= 0 ? 1 : input.Page; var pageSize = input.PageSize <= 0 ? 10 : input.PageSize; var offset = (page - 1) * pageSize; var where = new List { "p.Potype='PW'", "p.IsActive<=1" }; var pars = new List(); if (!string.IsNullOrWhiteSpace(input.PurOrd)) { where.Add("p.PurOrd LIKE @PurOrd"); pars.Add(new SugarParameter("@PurOrd", $"%{input.PurOrd.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.WorkOrd)) { where.Add("p.WorkOrd LIKE @WorkOrd"); pars.Add(new SugarParameter("@WorkOrd", $"%{input.WorkOrd.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Buyer)) { where.Add("p.Buyer=@Buyer"); pars.Add(new SugarParameter("@Buyer", input.Buyer.Trim())); } if (!string.IsNullOrWhiteSpace(input.Supp)) { where.Add("p.Supp=@Supp"); pars.Add(new SugarParameter("@Supp", input.Supp.Trim())); } var baseSql = $""" SELECT p.RecID AS Id, p.PurOrd AS PurOrd, p.WorkOrd AS WorkOrd, p.ERPWorkOrd AS ERPWorkOrd, CONCAT(TRIM(IFNULL(p.Supp,'')),' ',TRIM(IFNULL(s.SortName,''))) AS SuppName, p.Buyer AS BuyerCode, CONCAT(TRIM(IFNULL(p.Buyer,'')),' ',TRIM(IFNULL(e.Name,''))) AS Buyer, f.ItemNum AS ItemNum, i.Descr AS ItemName, CONCAT(TRIM(CAST(IFNULL(r.Op,0) AS CHAR(20))),' ',TRIM(IFNULL(r.Descr,''))) AS Op, p.OrdDate AS OrdDate, f.QtyOrded AS QtyOrded, f.CumQtyBO AS CumQtyBO, f.DueDate AS DueDate, n.`Date` AS FlDate, CASE WHEN IFNULL(LENGTH(p.Status),0)=0 OR p.Buyer IS NULL THEN 'R' ELSE p.Status END AS Status, p.Supp AS Supp, p.ReqBy AS ReqBy, p.Department AS Department, p.`Usage` AS `Usage`, p.Curr AS Curr, p.Remark AS Remark FROM PurOrdMaster p LEFT JOIN SuppMaster s ON p.Domain=s.Domain AND p.Supp=s.Supp LEFT JOIN DepartmentMaster d ON p.Domain=d.Domain AND p.Department=d.Department LEFT JOIN EmployeeMaster e ON p.Domain=e.Domain AND p.Buyer=e.Employee LEFT JOIN PurOrdDetail f ON p.PurOrd=f.PurOrd LEFT JOIN RoutingOpDetail r ON f.Op=r.Op AND f.ItemNum=r.RoutingCode LEFT JOIN ItemMaster i ON f.ItemNum=i.ItemNum LEFT JOIN PurOrdRctMaster o ON o.OrdNbr=p.PurOrd LEFT JOIN NbrMaster n ON p.WorkOrd=n.WorkOrd AND n.Type='CA' WHERE {string.Join(" AND ", where)} """; var outerWhere = string.Empty; if (!string.IsNullOrWhiteSpace(input.Status)) { outerWhere = "WHERE t.Status=@Status"; pars.Add(new SugarParameter("@Status", input.Status.Trim())); } var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM ({baseSql}) t {outerWhere}", pars); var list = await _db.Ado.SqlQueryAsync( $"SELECT * FROM ({baseSql}) t {outerWhere} ORDER BY {BuildOrderBy(input.SortField, input.SortOrder)} LIMIT {pageSize} OFFSET {offset}", pars); return new { total, page, pageSize, list }; } [DisplayName("工序外协订单详情")] [HttpGet("process-outsource-order/{id:int}")] public async Task GetDetail(int id) { var master = (await _db.Ado.SqlQueryAsync( """ SELECT p.RecID AS Id,p.PurOrd AS PurOrd,p.OrdDate AS OrdDate,p.Supp AS Supp,p.ReqBy AS ReqBy,p.WorkOrd AS WorkOrd, p.Buyer AS Buyer,p.Department AS Department,p.`Usage` AS `Usage`,p.Curr AS Curr,p.Remark AS Remark, CASE WHEN IFNULL(LENGTH(p.Status),0)=0 OR p.Buyer IS NULL THEN 'R' ELSE p.Status END AS Status FROM PurOrdMaster p WHERE p.RecID=@Id LIMIT 1 """, new SugarParameter("@Id", id))).FirstOrDefault(); if (master == null) throw Oops.Oh("记录不存在"); var details = await _db.Ado.SqlQueryAsync( """ SELECT d.RecID AS Id, d.Line AS Line, d.ItemNum AS ItemNum, IFNULL(i.Descr,'') AS ItemName, d.Op AS Op, d.UM AS UM, d.Location AS Location, d.QtyOrded AS QtyOrded, d.QtyReceived AS QtyReceived, d.ReceiptQty AS ReceiptQty, d.DueDate AS DueDate, d.LotSerial AS LotSerial, d.Potype AS Potype, d.PurOrd AS PurOrd, d.PurOrdRecID AS PurOrdRecID FROM PurOrdDetail d LEFT JOIN ItemMaster i ON d.ItemNum=i.ItemNum WHERE d.PurOrdRecID=@PurOrdRecID ORDER BY d.Line,d.RecID """, new SugarParameter("@PurOrdRecID", id)); return new { master, details }; } [DisplayName("按工单生成工序外协订单")] [HttpPost("process-outsource-order/create-by-workord")] public async Task CreateByWorkOrd([FromBody] ProcessOutsourceOrderCreateInput input) { if (string.IsNullOrWhiteSpace(input.WorkOrd)) throw Oops.Oh("请选择工单"); var workOrd = input.WorkOrd.Trim(); await _db.Ado.ExecuteCommandAsync( "CALL pr_MES_GeneratePW(@WorkOrd)", new SugarParameter("@WorkOrd", workOrd)); var created = (await _db.Ado.SqlQueryAsync( """ SELECT RecID AS Id, PurOrd AS PurOrd FROM PurOrdMaster WHERE WorkOrd=@WorkOrd AND Potype='PW' ORDER BY RecID DESC LIMIT 1 """, new SugarParameter("@WorkOrd", workOrd))).FirstOrDefault(); if (created == null) throw Oops.Oh("工序外协订单生成失败"); return new { id = created.Id, purOrd = created.PurOrd, message = "生成成功" }; } [DisplayName("保存工序外协订单")] [HttpPost("process-outsource-order/save")] public async Task Save([FromBody] ProcessOutsourceOrderSaveInput input) { if (input.Id <= 0) throw Oops.Oh("缺少主键"); if (string.IsNullOrWhiteSpace(input.PurOrd)) throw Oops.Oh("采购单号不能为空"); if (string.IsNullOrWhiteSpace(input.Supp)) throw Oops.Oh("供应商不能为空"); if (string.IsNullOrWhiteSpace(input.Department)) throw Oops.Oh("部门不能为空"); var exists = await _db.Ado.GetIntAsync("SELECT COUNT(1) FROM PurOrdMaster WHERE RecID=@Id", new SugarParameter("@Id", input.Id)); if (exists <= 0) throw Oops.Oh("记录不存在"); try { _db.Ado.BeginTran(); await _db.Ado.ExecuteCommandAsync( """ UPDATE PurOrdMaster SET OrdDate=@OrdDate,Supp=@Supp,ReqBy=@ReqBy,WorkOrd=@WorkOrd,Buyer=@Buyer,Department=@Department,`Usage`=@Usage,Curr=@Curr,Remark=@Remark,UpdateUser=@UpdateUser,UpdateTime=@UpdateTime WHERE RecID=@Id """, new SugarParameter("@Id", input.Id), new SugarParameter("@OrdDate", ParseDate(input.OrdDate)), new SugarParameter("@Supp", input.Supp!.Trim()), new SugarParameter("@ReqBy", string.IsNullOrWhiteSpace(input.ReqBy) ? "PO" : input.ReqBy.Trim()), new SugarParameter("@WorkOrd", input.WorkOrd?.Trim()), new SugarParameter("@Buyer", string.IsNullOrWhiteSpace(input.Buyer) ? "110" : input.Buyer.Trim()), new SugarParameter("@Department", input.Department!.Trim()), new SugarParameter("@Usage", string.IsNullOrWhiteSpace(input.Usage) ? "外协" : input.Usage.Trim()), new SugarParameter("@Curr", input.Curr?.Trim()), new SugarParameter("@Remark", input.Remark?.Trim()), new SugarParameter("@UpdateUser", _userManager.Account), new SugarParameter("@UpdateTime", DateTime.Now) ); await SaveDetailsAsync(input.Id, input.PurOrd!.Trim(), input.WorkOrd, input.Details); _db.Ado.CommitTran(); } catch { _db.Ado.RollbackTran(); throw; } return new { id = input.Id, message = "保存成功" }; } /// /// 明细三路合并: /// ① DB有且入参有(按 Id 匹配)→ 更新 /// ② DB无但入参有 → 新增 /// ③ DB有但入参无 → 删除 /// private async Task SaveDetailsAsync(int masterId, string purOrd, string? workOrd, List details) { var dbDetails = await _db.Ado.SqlQueryAsync( "SELECT RecID AS Id FROM PurOrdDetail WHERE PurOrdRecID=@PurOrdRecID", new SugarParameter("@PurOrdRecID", masterId)); var dbById = dbDetails.ToDictionary(x => x.Id); var inputIds = new HashSet(details.Where(x => x.Id.HasValue && x.Id.Value > 0).Select(x => x.Id!.Value)); var maxLine = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(Line),0) FROM PurOrdDetail WHERE PurOrdRecID=@PurOrdRecID", new SugarParameter("@PurOrdRecID", masterId)); var lineSeed = maxLine; for (var i = 0; i < details.Count; i++) { var item = details[i]; if (string.IsNullOrWhiteSpace(item.ItemNum)) continue; var line = item.Line ?? (i + 1); if (line <= 0) { lineSeed++; line = lineSeed; } var itemInfo = (await _db.Ado.SqlQueryAsync( """ SELECT ItemNum,Descr,UM,Location FROM ItemMaster WHERE ItemNum=@ItemNum LIMIT 1 """, new SugarParameter("@ItemNum", item.ItemNum.Trim()))).FirstOrDefault(); if (item.Id.HasValue && item.Id.Value > 0 && dbById.ContainsKey(item.Id.Value)) { await _db.Ado.ExecuteCommandAsync( """ UPDATE PurOrdDetail SET Line=@Line,ItemNum=@ItemNum,Descr=@Descr,Op=@Op,UM=@UM,Location=@Location,QtyOrded=@QtyOrded,QtyReceived=@QtyReceived,ReceiptQty=@ReceiptQty,DueDate=@DueDate,LotSerial=@LotSerial,Potype='PW',PurOrd=@PurOrd,PurOrdRecID=@PurOrdRecID,WorkOrd=@WorkOrd,UpdateUser=@UpdateUser,UpdateTime=@UpdateTime WHERE RecID=@Id """, new SugarParameter("@Id", item.Id.Value), new SugarParameter("@Line", line), new SugarParameter("@ItemNum", item.ItemNum.Trim()), new SugarParameter("@Descr", itemInfo?.Descr), new SugarParameter("@Op", item.Op ?? 0), new SugarParameter("@UM", string.IsNullOrWhiteSpace(item.UM) ? itemInfo?.UM : item.UM!.Trim()), new SugarParameter("@Location", string.IsNullOrWhiteSpace(item.Location) ? itemInfo?.Location : item.Location!.Trim()), new SugarParameter("@QtyOrded", item.QtyOrded ?? 0), new SugarParameter("@QtyReceived", item.QtyReceived ?? 0), new SugarParameter("@ReceiptQty", item.ReceiptQty ?? 0), new SugarParameter("@DueDate", ParseDate(item.DueDate)), new SugarParameter("@LotSerial", item.LotSerial?.Trim()), new SugarParameter("@PurOrd", purOrd), new SugarParameter("@PurOrdRecID", masterId), new SugarParameter("@WorkOrd", workOrd?.Trim()), new SugarParameter("@UpdateUser", _userManager.Account), new SugarParameter("@UpdateTime", DateTime.Now) ); } else { await _db.Ado.ExecuteCommandAsync( """ INSERT INTO PurOrdDetail (PurOrd,Line,ItemNum,Descr,Op,UM,Location,QtyOrded,QtyReceived,ReceiptQty,DueDate,LotSerial,Potype,PurOrdRecID,WorkOrd,Status,CreateUser,CreateTime,UpdateUser,UpdateTime,tenant_id) VALUES (@PurOrd,@Line,@ItemNum,@Descr,@Op,@UM,@Location,@QtyOrded,@QtyReceived,@ReceiptQty,@DueDate,@LotSerial,'PW',@PurOrdRecID,@WorkOrd,'R',@CreateUser,@CreateTime,@UpdateUser,@UpdateTime,@TenantId) """, new SugarParameter("@PurOrd", purOrd), new SugarParameter("@Line", line), new SugarParameter("@ItemNum", item.ItemNum.Trim()), new SugarParameter("@Descr", itemInfo?.Descr), new SugarParameter("@Op", item.Op ?? 0), new SugarParameter("@UM", string.IsNullOrWhiteSpace(item.UM) ? itemInfo?.UM : item.UM!.Trim()), new SugarParameter("@Location", string.IsNullOrWhiteSpace(item.Location) ? itemInfo?.Location : item.Location!.Trim()), new SugarParameter("@QtyOrded", item.QtyOrded ?? 0), new SugarParameter("@QtyReceived", item.QtyReceived ?? 0), new SugarParameter("@ReceiptQty", item.ReceiptQty ?? 0), new SugarParameter("@DueDate", ParseDate(item.DueDate)), new SugarParameter("@LotSerial", item.LotSerial?.Trim()), new SugarParameter("@PurOrdRecID", masterId), new SugarParameter("@WorkOrd", workOrd?.Trim()), new SugarParameter("@CreateUser", _userManager.Account), new SugarParameter("@CreateTime", DateTime.Now), new SugarParameter("@UpdateUser", _userManager.Account), new SugarParameter("@UpdateTime", DateTime.Now), new SugarParameter("@TenantId", _userManager.TenantId <= 0 ? null : _userManager.TenantId) ); } } foreach (var toDelete in dbDetails.Where(x => !inputIds.Contains(x.Id))) { await _db.Ado.ExecuteCommandAsync("DELETE FROM PurOrdDetail WHERE RecID=@Id", new SugarParameter("@Id", toDelete.Id)); } } [DisplayName("删除工序外协订单")] [HttpPost("process-outsource-order/delete/{id:int}")] public async Task Delete(int id, [FromQuery] string purOrd) { if (string.IsNullOrWhiteSpace(purOrd)) throw Oops.Oh("缺少采购单号"); var ordNo = purOrd.Trim(); var hasReceipt = await _db.Ado.GetIntAsync( """ SELECT COUNT(1) FROM PurOrdRctDetail WHERE OrdNbr=@PurOrd AND IFNULL(QtyReceived,0)>0 """, new SugarParameter("@PurOrd", ordNo)); if (hasReceipt > 0) throw Oops.Oh("存在收货数量,不允许删除"); var hasShip = await _db.Ado.GetIntAsync( "SELECT COUNT(1) FROM scm_shdzb WHERE po_bill=@PurOrd", new SugarParameter("@PurOrd", ordNo)); if (hasShip > 0) throw Oops.Oh("存在发货单,不允许删除"); try { _db.Ado.BeginTran(); await _db.Ado.ExecuteCommandAsync("DELETE FROM PurOrdDetail WHERE PurOrd=@PurOrd", new SugarParameter("@PurOrd", ordNo)); await _db.Ado.ExecuteCommandAsync("DELETE FROM PurOrdMaster WHERE RecID=@Id AND PurOrd=@PurOrd", new SugarParameter("@Id", id), new SugarParameter("@PurOrd", ordNo)); _db.Ado.CommitTran(); } catch { _db.Ado.RollbackTran(); throw; } return new { message = "删除成功" }; } [DisplayName("采购组下拉")] [HttpGet("process-outsource-order/options/buyers")] public async Task GetBuyerOptions() { var list = await _db.Ado.SqlQueryAsync( """ SELECT Employee AS Value, CONCAT(TRIM(Employee),' ',TRIM(IFNULL(Name,''))) AS Label FROM EmployeeMaster ORDER BY Employee """); return new { list }; } [DisplayName("供应商下拉")] [HttpGet("process-outsource-order/options/suppliers")] public async Task GetSupplierOptions() { var list = await _db.Ado.SqlQueryAsync( """ SELECT Supp AS Value, CONCAT(TRIM(Supp),' ',TRIM(IFNULL(SortName,''))) AS Label FROM SuppMaster ORDER BY Supp """); return new { list }; } [DisplayName("部门下拉")] [HttpGet("process-outsource-order/options/departments")] public async Task GetDepartmentOptions() { var list = await _db.Ado.SqlQueryAsync( """ SELECT Department AS Value, CONCAT(TRIM(Department),' ',TRIM(IFNULL(Descr,''))) AS Label FROM DepartmentMaster ORDER BY Department """); return new { list }; } [DisplayName("币别下拉")] [HttpGet("process-outsource-order/options/currencies")] public async Task GetCurrencyOptions() { var list = await _db.Ado.SqlQueryAsync( """ SELECT Val AS Value, CONCAT(TRIM(Val),' ',TRIM(IFNULL(Comments,''))) AS Label FROM GeneralizedCodeMaster WHERE FldName='Curr' ORDER BY Val """); return new { list }; } [DisplayName("库位下拉")] [HttpGet("process-outsource-order/options/locations")] public async Task GetLocationOptions() { var list = await _db.Ado.SqlQueryAsync( """ SELECT Location AS Value, CONCAT(TRIM(Location),' ',TRIM(IFNULL(Descr,''))) AS Label FROM LocationMaster WHERE IFNULL(Typed,'')<>'Supp' ORDER BY Location """); return new { list }; } [DisplayName("工序下拉")] [HttpGet("process-outsource-order/options/ops")] public async Task GetOpOptions([FromQuery] int purOrdRecId) { if (purOrdRecId <= 0) return new { list = new List() }; var list = await _db.Ado.SqlQueryAsync( """ SELECT DISTINCT CAST(Op AS CHAR(50)) AS Value, CONCAT(TRIM(CAST(Op AS CHAR(50))),' ',TRIM(IFNULL(Descr,''))) AS Label FROM RoutingOpDetail WHERE UDeci5!=0 AND RoutingCode IN (SELECT ItemNum FROM PurOrdDetail WHERE PurOrdRecID=@PurOrdRecID) ORDER BY Op """, new SugarParameter("@PurOrdRecID", purOrdRecId)); return new { list }; } private static DateTime? ParseDate(string? v) => DateTime.TryParse(v, out var d) ? d : null; private static string BuildOrderBy(string? sortField, string? sortOrder) { var dir = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC"; return sortField?.ToLowerInvariant() switch { "purord" => $"t.PurOrd {dir}", "workord" => $"t.WorkOrd {dir}", "suppname" => $"t.SuppName {dir}", "buyer" => $"t.Buyer {dir}", "itemnum" => $"t.ItemNum {dir}", "itemname" => $"t.ItemName {dir}", "op" => $"t.Op {dir}", "orddate" => $"t.OrdDate {dir}", "qtyorded" => $"t.QtyOrded {dir}", "cumqtybo" => $"t.CumQtyBO {dir}", "duedate" => $"t.DueDate {dir}", "fldate" => $"t.FlDate {dir}", "status" => $"t.Status {dir}", _ => "t.Id DESC" }; } private sealed class ProcessOutsourceOrderListRow { public int Id { get; set; } public string? PurOrd { get; set; } public string? WorkOrd { get; set; } public string? ERPWorkOrd { get; set; } public string? SuppName { get; set; } public string? Buyer { get; set; } public string? BuyerCode { get; set; } public string? ItemNum { get; set; } public string? ItemName { get; set; } public string? Op { get; set; } public DateTime? OrdDate { get; set; } public decimal? QtyOrded { get; set; } public decimal? CumQtyBO { get; set; } public DateTime? DueDate { get; set; } public DateTime? FlDate { get; set; } public string? Status { get; set; } public string? Supp { get; set; } public string? ReqBy { get; set; } public string? Department { get; set; } public string? Usage { get; set; } public string? Curr { get; set; } public string? Remark { get; set; } } private sealed class ProcessOutsourceOrderMasterRow { public int Id { get; set; } public string? PurOrd { get; set; } public DateTime? OrdDate { get; set; } public string? Supp { get; set; } public string? ReqBy { get; set; } public string? WorkOrd { get; set; } public string? Buyer { get; set; } public string? Department { get; set; } public string? Usage { get; set; } public string? Curr { get; set; } public string? Remark { get; set; } public string? Status { get; set; } } private sealed class ProcessOutsourceOrderDetailRow { public int Id { get; set; } public int? Line { get; set; } public string? ItemNum { get; set; } public string? ItemName { get; set; } public int? Op { get; set; } public string? UM { get; set; } public string? Location { get; set; } public decimal? QtyOrded { get; set; } public decimal? QtyReceived { get; set; } public decimal? ReceiptQty { get; set; } public DateTime? DueDate { get; set; } public string? LotSerial { get; set; } public string? Potype { get; set; } public string? PurOrd { get; set; } public int? PurOrdRecID { get; set; } } private sealed class OptionRow { public string? Value { get; set; } public string? Label { get; set; } } private sealed class IdPurOrdRow { public int Id { get; set; } public string? PurOrd { get; set; } } private sealed class DbDetailRow { public int Id { get; set; } } private sealed class ItemLookupRow { public string? ItemNum { get; set; } public string? Descr { get; set; } public string? UM { get; set; } public string? Location { get; set; } } }