namespace Admin.NET.Plugin.AiDOP.Order; /// /// 出货计划服务 📦 /// 路由前缀:/api/Order/shippingplan/... /// [ApiDescriptionSettings(Order = 290, Description = "出货计划")] [Route("api/Order")] [AllowAnonymous] [NonUnify] public class ShippingPlanService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly SqlSugarRepository _planRep; private readonly SqlSugarRepository _detailRep; private readonly UserManager _userManager; public ShippingPlanService( ISqlSugarClient db, SqlSugarRepository planRep, SqlSugarRepository detailRep, UserManager userManager) { _db = db; _planRep = planRep; _detailRep = detailRep; _userManager = userManager; } // ══════════════════════════════════════════════════════════════ // 列表 GET /api/Order/shippingplan/list // ══════════════════════════════════════════════════════════════ /// 获取出货计划分页列表 📦 [DisplayName("获取出货计划列表")] [HttpGet("shippingplan/list")] public async Task GetShippingPlanList([FromQuery] ShippingPlanListInput input) { var pars = new List(); var conditions = new List(); if (!string.IsNullOrWhiteSpace(input.OrdNbr)) { conditions.Add("b.OrdNbr LIKE @OrdNbr"); pars.Add(new SugarParameter("@OrdNbr", $"%{input.OrdNbr.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.BillNo)) { conditions.Add("b.bill_no LIKE @BillNo"); pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Country)) { conditions.Add("b.Country = @Country"); pars.Add(new SugarParameter("@Country", input.Country.Trim())); } if (!string.IsNullOrWhiteSpace(input.ShippingDateFrom)) { conditions.Add("a.ShippingDate >= @ShippingDateFrom"); pars.Add(new SugarParameter("@ShippingDateFrom", input.ShippingDateFrom.Trim())); } if (!string.IsNullOrWhiteSpace(input.CustomNo)) { conditions.Add("b.CustomNo LIKE @CustomNo"); pars.Add(new SugarParameter("@CustomNo", $"%{input.CustomNo.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.ItemNum)) { conditions.Add("b.ItemNum LIKE @ItemNum"); pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%")); } var whereClause = conditions.Count > 0 ? "WHERE " + string.Join(" AND ", conditions) : ""; var baseSql = $""" SELECT a.ShippingSite, a.LotSerial, a.ShippingDate, a.ShippingAddress, a.Consignee, a.Telephone, a.RecID AS Id, b.OrdNbr, b.bill_no AS BillNo, b.OrdDate, b.Country, b.CustomNo, b.CustomName, b.ItemNum, b.ItemName, b.Specification, b.Qty, b.Packaging, CAST(c.RecID AS CHAR) AS AsnRecID, CAST(b.RecID AS CHAR) AS Sid, b.Volume, b.Weight FROM ShippingPlan a LEFT JOIN ShippingPlanDetail b ON a.RecID = b.plan_id LEFT JOIN ASNBOLShipperDetail c ON b.ItemNum = c.ContainerItem AND b.bill_no = c.ordnbr AND c.shtype = 'SH' AND c.Typed <> 'S' AND c.IsActive = 1 {whereClause} """; var offset = (input.Page - 1) * input.PageSize; var total = await _db.Ado.GetIntAsync( $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars); var list = await _db.Ado.SqlQueryAsync( $"SELECT * FROM ({baseSql}) AS t ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}", pars); return new { total, page = input.Page, pageSize = input.PageSize, list }; } // ══════════════════════════════════════════════════════════════ // 详情 GET /api/Order/shippingplan/{id} // ══════════════════════════════════════════════════════════════ /// 获取出货计划详情(含明细)📦 [DisplayName("获取出货计划详情")] [HttpGet("shippingplan/{id}")] public async Task GetShippingPlanDetail(int id) { var plan = await _planRep.GetFirstAsync(u => u.RecID == id) ?? throw Oops.Oh("出货计划不存在"); var details = await _detailRep.GetListAsync(u => u.PlanId == id); return new { recID = plan.RecID, lotSerial = plan.LotSerial, shippingDate = plan.ShippingDate?.ToString("yyyy-MM-dd"), shippingSite = plan.ShippingSite, consignee = plan.Consignee, shippingAddress = plan.ShippingAddress, telephone = plan.Telephone, remark = plan.Remark, details = details.Select(d => new { recID = d.RecID, ordNbr = d.OrdNbr, billNo = d.BillNo, customNo = d.CustomNo, customName = d.CustomName, ordDate = d.OrdDate?.ToString("yyyy-MM-dd"), country = d.Country, itemNum = d.ItemNum, itemName = d.ItemName, specification = d.Specification, qty = d.Qty, weight = d.Weight, volume = d.Volume, packaging = d.Packaging, remark = d.Remark, }) }; } // ══════════════════════════════════════════════════════════════ // 保存(新增/编辑)POST /api/Order/shippingplan/save // ══════════════════════════════════════════════════════════════ /// 保存出货计划(新增或编辑)📦 [DisplayName("保存出货计划")] [ApiDescriptionSettings(Name = "SaveShippingPlan"), HttpPost("shippingplan/save")] public async Task SaveShippingPlan([FromBody] ShippingPlanSaveInput input) { var now = DateTime.Now; var user = _userManager.Account ?? "system"; if (input.RecID is null or 0) { // ── 新增:参照 SysJobService.AddJobDetail ── var nextPlanId = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) FROM ShippingPlan") + 1; var entity = new ShippingPlan { RecID = nextPlanId, Domain = "8010", Priority = 0, LotSerial = input.LotSerial, ShippingDate = string.IsNullOrWhiteSpace(input.ShippingDate) ? null : DateTime.Parse(input.ShippingDate), ShippingSite = input.ShippingSite, Consignee = input.Consignee, ShippingAddress = input.ShippingAddress, Telephone = input.Telephone, Remark = input.Remark, IsActive = 1, IsConfirm = 1, CreateUser = user, CreateTime = now, }; await _planRep.InsertAsync(entity); await SaveDetailsAsync(nextPlanId, input.Details, user, now, isNew: true); return new { id = nextPlanId, message = "新增成功" }; } else { // ── 编辑:参照 SysJobService.UpdateJobDetail ── var entity = await _planRep.GetFirstAsync(u => u.RecID == input.RecID.Value) ?? throw Oops.Oh("出货计划不存在"); entity.LotSerial = input.LotSerial; entity.ShippingDate = string.IsNullOrWhiteSpace(input.ShippingDate) ? null : DateTime.Parse(input.ShippingDate); entity.ShippingSite = input.ShippingSite; entity.Consignee = input.Consignee; entity.ShippingAddress = input.ShippingAddress; entity.Telephone = input.Telephone; entity.Remark = input.Remark; entity.UpdateUser = user; entity.UpdateTime = now; await _planRep.UpdateAsync(entity); await SaveDetailsAsync(input.RecID.Value, input.Details, user, now, isNew: false); return new { id = input.RecID, message = "编辑成功" }; } } /// /// 明细三路合并(参照 SeOrderService.SaveEntriesAsync): /// ① DB有且入参有(按 RecID 匹配)→ 更新 /// ② DB无但入参有 → 新增 /// ③ DB有但入参无 → 删除 /// private async Task SaveDetailsAsync(int planId, List details, string user, DateTime now, bool isNew) { var dbDetails = isNew ? new List() : await _detailRep.GetListAsync(u => u.PlanId == planId); var dbById = dbDetails.ToDictionary(u => u.RecID); var inputIds = new HashSet(details.Where(d => d.RecID is > 0).Select(d => d.RecID!.Value)); // RecID 列无 AUTO_INCREMENT,手动取当前最大值后顺序递增 var nextId = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) FROM ShippingPlanDetail") + 1; foreach (var d in details) { if (d.RecID > 0 && dbById.TryGetValue(d.RecID!.Value, out var existing)) { // ① 更新 existing.OrdNbr = d.OrdNbr; existing.BillNo = d.BillNo; existing.CustomNo = d.CustomNo; existing.CustomName = d.CustomName; existing.OrdDate = string.IsNullOrWhiteSpace(d.OrdDate) ? null : DateTime.Parse(d.OrdDate); existing.Country = d.Country; existing.ItemNum = d.ItemNum; existing.ItemName = d.ItemName; existing.Specification = d.Specification; existing.Qty = d.Qty; existing.Weight = d.Weight; existing.Volume = d.Volume; existing.Packaging = d.Packaging; existing.Remark = d.Remark; existing.UpdateUser = user; existing.UpdateTime = now; await _detailRep.UpdateAsync(existing); } else { // ② 新增 var detail = new ShippingPlanDetail { RecID = nextId++, PlanId = planId, Domain = "8010", SentryId = long.TryParse(d.SentryId, out var sid) ? sid : null, OrdNbr = d.OrdNbr, BillNo = d.BillNo, CustomNo = d.CustomNo, CustomName = d.CustomName, OrdDate = string.IsNullOrWhiteSpace(d.OrdDate) ? null : DateTime.Parse(d.OrdDate), Country = d.Country, ItemNum = d.ItemNum, ItemName = d.ItemName, Specification = d.Specification, Qty = d.Qty, Weight = d.Weight, Volume = d.Volume, Packaging = d.Packaging, Remark = d.Remark, IsActive = 1, IsConfirm = 1, CreateUser = user, CreateTime = now, }; await _detailRep.InsertAsync(detail); } } // ③ 删除:DB有、入参无 foreach (var toDelete in dbDetails.Where(u => !inputIds.Contains(u.RecID))) { await _detailRep.DeleteAsync(u => u.RecID == toDelete.RecID); } } // ══════════════════════════════════════════════════════════════ // 销售出库 POST /api/Order/shippingplan/ship // ══════════════════════════════════════════════════════════════ /// 销售出库(调用存储过程 pr_WMS_AutoCreateShipper)📦 [DisplayName("销售出库")] [ApiDescriptionSettings(Name = "ShipShippingPlan"), HttpPost("shippingplan/ship")] public async Task ShipShippingPlan([FromBody] ShippingPlanShipInput input) { var orgNo = ""; // 组织编号,可从配置或 UserManager 获取 var account = _userManager.Account ?? "system"; var ids = input.Ids; await _db.Ado.ExecuteCommandAsync( "CALL pr_WMS_AutoCreateShipper(@OrgNo, @Account, @Ids)", new SugarParameter("@OrgNo", orgNo), new SugarParameter("@Account", account), new SugarParameter("@Ids", ids)); return new { message = "销售出库执行成功" }; } // ──────────────── 内部查询结果映射类 ──────────────── private sealed class ShippingPlanListRow { public int Id { get; set; } public string? LotSerial { get; set; } public string? OrdNbr { get; set; } public string? BillNo { get; set; } public DateTime? OrdDate { get; set; } public string? Country { get; set; } public string? CustomNo { get; set; } public string? CustomName { get; set; } public string? ItemNum { get; set; } public string? ItemName { get; set; } public string? Specification { get; set; } public decimal? Qty { get; set; } public decimal? Weight { get; set; } public decimal? Volume { get; set; } public string? Packaging { get; set; } public string? ShippingSite { get; set; } public DateTime? ShippingDate { get; set; } public string? ShippingAddress { get; set; } public string? Consignee { get; set; } public string? Telephone { get; set; } public string? AsnRecID { get; set; } public string? Sid { get; set; } } }