ShippingPlanService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. namespace Admin.NET.Plugin.AiDOP.Order;
  2. /// <summary>
  3. /// 出货计划服务 📦
  4. /// 路由前缀:/api/Order/shippingplan/...
  5. /// </summary>
  6. [ApiDescriptionSettings(Order = 290, Description = "出货计划")]
  7. [Route("api/Order")]
  8. [AllowAnonymous]
  9. [NonUnify]
  10. public class ShippingPlanService : IDynamicApiController, ITransient
  11. {
  12. private readonly ISqlSugarClient _db;
  13. private readonly SqlSugarRepository<ShippingPlan> _planRep;
  14. private readonly SqlSugarRepository<ShippingPlanDetail> _detailRep;
  15. private readonly UserManager _userManager;
  16. public ShippingPlanService(
  17. ISqlSugarClient db,
  18. SqlSugarRepository<ShippingPlan> planRep,
  19. SqlSugarRepository<ShippingPlanDetail> detailRep,
  20. UserManager userManager)
  21. {
  22. _db = db;
  23. _planRep = planRep;
  24. _detailRep = detailRep;
  25. _userManager = userManager;
  26. }
  27. // ══════════════════════════════════════════════════════════════
  28. // 列表 GET /api/Order/shippingplan/list
  29. // ══════════════════════════════════════════════════════════════
  30. /// <summary>获取出货计划分页列表 📦</summary>
  31. [DisplayName("获取出货计划列表")]
  32. [HttpGet("shippingplan/list")]
  33. public async Task<object> GetShippingPlanList([FromQuery] ShippingPlanListInput input)
  34. {
  35. var pars = new List<SugarParameter>();
  36. var conditions = new List<string>();
  37. if (!string.IsNullOrWhiteSpace(input.OrdNbr))
  38. {
  39. conditions.Add("b.OrdNbr LIKE @OrdNbr");
  40. pars.Add(new SugarParameter("@OrdNbr", $"%{input.OrdNbr.Trim()}%"));
  41. }
  42. if (!string.IsNullOrWhiteSpace(input.BillNo))
  43. {
  44. conditions.Add("b.bill_no LIKE @BillNo");
  45. pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
  46. }
  47. if (!string.IsNullOrWhiteSpace(input.Country))
  48. {
  49. conditions.Add("b.Country = @Country");
  50. pars.Add(new SugarParameter("@Country", input.Country.Trim()));
  51. }
  52. if (!string.IsNullOrWhiteSpace(input.ShippingDateFrom))
  53. {
  54. conditions.Add("a.ShippingDate >= @ShippingDateFrom");
  55. pars.Add(new SugarParameter("@ShippingDateFrom", input.ShippingDateFrom.Trim()));
  56. }
  57. if (!string.IsNullOrWhiteSpace(input.CustomNo))
  58. {
  59. conditions.Add("b.CustomNo LIKE @CustomNo");
  60. pars.Add(new SugarParameter("@CustomNo", $"%{input.CustomNo.Trim()}%"));
  61. }
  62. if (!string.IsNullOrWhiteSpace(input.ItemNum))
  63. {
  64. conditions.Add("b.ItemNum LIKE @ItemNum");
  65. pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
  66. }
  67. var whereClause = conditions.Count > 0 ? "WHERE " + string.Join(" AND ", conditions) : "";
  68. var baseSql = $"""
  69. SELECT
  70. a.ShippingSite,
  71. a.LotSerial,
  72. a.ShippingDate,
  73. a.ShippingAddress,
  74. a.Consignee,
  75. a.Telephone,
  76. a.RecID AS Id,
  77. b.OrdNbr,
  78. b.bill_no AS BillNo,
  79. b.OrdDate,
  80. b.Country,
  81. b.CustomNo,
  82. b.CustomName,
  83. b.ItemNum,
  84. b.ItemName,
  85. b.Specification,
  86. b.Qty,
  87. b.Packaging,
  88. CAST(c.RecID AS CHAR) AS AsnRecID,
  89. CAST(b.RecID AS CHAR) AS Sid,
  90. b.Volume,
  91. b.Weight
  92. FROM ShippingPlan a
  93. LEFT JOIN ShippingPlanDetail b ON a.RecID = b.plan_id
  94. LEFT JOIN ASNBOLShipperDetail c ON b.ItemNum = c.ContainerItem
  95. AND b.bill_no = c.ordnbr
  96. AND c.shtype = 'SH'
  97. AND c.Typed <> 'S'
  98. AND c.IsActive = 1
  99. {whereClause}
  100. """;
  101. var offset = (input.Page - 1) * input.PageSize;
  102. var total = await _db.Ado.GetIntAsync(
  103. $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  104. var list = await _db.Ado.SqlQueryAsync<ShippingPlanListRow>(
  105. $"SELECT * FROM ({baseSql}) AS t ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}",
  106. pars);
  107. return new { total, page = input.Page, pageSize = input.PageSize, list };
  108. }
  109. // ══════════════════════════════════════════════════════════════
  110. // 详情 GET /api/Order/shippingplan/{id}
  111. // ══════════════════════════════════════════════════════════════
  112. /// <summary>获取出货计划详情(含明细)📦</summary>
  113. [DisplayName("获取出货计划详情")]
  114. [HttpGet("shippingplan/{id}")]
  115. public async Task<object> GetShippingPlanDetail(int id)
  116. {
  117. var plan = await _planRep.GetFirstAsync(u => u.RecID == id)
  118. ?? throw Oops.Oh("出货计划不存在");
  119. var details = await _detailRep.GetListAsync(u => u.PlanId == id);
  120. return new
  121. {
  122. recID = plan.RecID,
  123. lotSerial = plan.LotSerial,
  124. shippingDate = plan.ShippingDate?.ToString("yyyy-MM-dd"),
  125. shippingSite = plan.ShippingSite,
  126. consignee = plan.Consignee,
  127. shippingAddress = plan.ShippingAddress,
  128. telephone = plan.Telephone,
  129. remark = plan.Remark,
  130. details = details.Select(d => new
  131. {
  132. recID = d.RecID,
  133. ordNbr = d.OrdNbr,
  134. billNo = d.BillNo,
  135. customNo = d.CustomNo,
  136. customName = d.CustomName,
  137. ordDate = d.OrdDate?.ToString("yyyy-MM-dd"),
  138. country = d.Country,
  139. itemNum = d.ItemNum,
  140. itemName = d.ItemName,
  141. specification = d.Specification,
  142. qty = d.Qty,
  143. weight = d.Weight,
  144. volume = d.Volume,
  145. packaging = d.Packaging,
  146. remark = d.Remark,
  147. })
  148. };
  149. }
  150. // ══════════════════════════════════════════════════════════════
  151. // 保存(新增/编辑)POST /api/Order/shippingplan/save
  152. // ══════════════════════════════════════════════════════════════
  153. /// <summary>保存出货计划(新增或编辑)📦</summary>
  154. [DisplayName("保存出货计划")]
  155. [ApiDescriptionSettings(Name = "SaveShippingPlan"), HttpPost("shippingplan/save")]
  156. public async Task<object> SaveShippingPlan([FromBody] ShippingPlanSaveInput input)
  157. {
  158. var now = DateTime.Now;
  159. var user = _userManager.Account ?? "system";
  160. if (input.RecID is null or 0)
  161. {
  162. // ── 新增:参照 SysJobService.AddJobDetail ──
  163. var nextPlanId = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) FROM ShippingPlan") + 1;
  164. var entity = new ShippingPlan
  165. {
  166. RecID = nextPlanId,
  167. Domain = "8010",
  168. Priority = 0,
  169. LotSerial = input.LotSerial,
  170. ShippingDate = string.IsNullOrWhiteSpace(input.ShippingDate) ? null : DateTime.Parse(input.ShippingDate),
  171. ShippingSite = input.ShippingSite,
  172. Consignee = input.Consignee,
  173. ShippingAddress = input.ShippingAddress,
  174. Telephone = input.Telephone,
  175. Remark = input.Remark,
  176. IsActive = 1,
  177. IsConfirm = 1,
  178. CreateUser = user,
  179. CreateTime = now,
  180. };
  181. await _planRep.InsertAsync(entity);
  182. await SaveDetailsAsync(nextPlanId, input.Details, user, now, isNew: true);
  183. return new { id = nextPlanId, message = "新增成功" };
  184. }
  185. else
  186. {
  187. // ── 编辑:参照 SysJobService.UpdateJobDetail ──
  188. var entity = await _planRep.GetFirstAsync(u => u.RecID == input.RecID.Value)
  189. ?? throw Oops.Oh("出货计划不存在");
  190. entity.LotSerial = input.LotSerial;
  191. entity.ShippingDate = string.IsNullOrWhiteSpace(input.ShippingDate) ? null : DateTime.Parse(input.ShippingDate);
  192. entity.ShippingSite = input.ShippingSite;
  193. entity.Consignee = input.Consignee;
  194. entity.ShippingAddress = input.ShippingAddress;
  195. entity.Telephone = input.Telephone;
  196. entity.Remark = input.Remark;
  197. entity.UpdateUser = user;
  198. entity.UpdateTime = now;
  199. await _planRep.UpdateAsync(entity);
  200. await SaveDetailsAsync(input.RecID.Value, input.Details, user, now, isNew: false);
  201. return new { id = input.RecID, message = "编辑成功" };
  202. }
  203. }
  204. /// <summary>
  205. /// 明细三路合并(参照 SeOrderService.SaveEntriesAsync):
  206. /// ① DB有且入参有(按 RecID 匹配)→ 更新
  207. /// ② DB无但入参有 → 新增
  208. /// ③ DB有但入参无 → 删除
  209. /// </summary>
  210. private async Task SaveDetailsAsync(int planId, List<ShippingPlanDetailInput> details,
  211. string user, DateTime now, bool isNew)
  212. {
  213. var dbDetails = isNew ? new List<ShippingPlanDetail>()
  214. : await _detailRep.GetListAsync(u => u.PlanId == planId);
  215. var dbById = dbDetails.ToDictionary(u => u.RecID);
  216. var inputIds = new HashSet<int>(details.Where(d => d.RecID is > 0).Select(d => d.RecID!.Value));
  217. // RecID 列无 AUTO_INCREMENT,手动取当前最大值后顺序递增
  218. var nextId = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) FROM ShippingPlanDetail") + 1;
  219. foreach (var d in details)
  220. {
  221. if (d.RecID > 0 && dbById.TryGetValue(d.RecID!.Value, out var existing))
  222. {
  223. // ① 更新
  224. existing.OrdNbr = d.OrdNbr;
  225. existing.BillNo = d.BillNo;
  226. existing.CustomNo = d.CustomNo;
  227. existing.CustomName = d.CustomName;
  228. existing.OrdDate = string.IsNullOrWhiteSpace(d.OrdDate) ? null : DateTime.Parse(d.OrdDate);
  229. existing.Country = d.Country;
  230. existing.ItemNum = d.ItemNum;
  231. existing.ItemName = d.ItemName;
  232. existing.Specification = d.Specification;
  233. existing.Qty = d.Qty;
  234. existing.Weight = d.Weight;
  235. existing.Volume = d.Volume;
  236. existing.Packaging = d.Packaging;
  237. existing.Remark = d.Remark;
  238. existing.UpdateUser = user;
  239. existing.UpdateTime = now;
  240. await _detailRep.UpdateAsync(existing);
  241. }
  242. else
  243. {
  244. // ② 新增
  245. var detail = new ShippingPlanDetail
  246. {
  247. RecID = nextId++,
  248. PlanId = planId,
  249. Domain = "8010",
  250. SentryId = long.TryParse(d.SentryId, out var sid) ? sid : null,
  251. OrdNbr = d.OrdNbr,
  252. BillNo = d.BillNo,
  253. CustomNo = d.CustomNo,
  254. CustomName = d.CustomName,
  255. OrdDate = string.IsNullOrWhiteSpace(d.OrdDate) ? null : DateTime.Parse(d.OrdDate),
  256. Country = d.Country,
  257. ItemNum = d.ItemNum,
  258. ItemName = d.ItemName,
  259. Specification = d.Specification,
  260. Qty = d.Qty,
  261. Weight = d.Weight,
  262. Volume = d.Volume,
  263. Packaging = d.Packaging,
  264. Remark = d.Remark,
  265. IsActive = 1,
  266. IsConfirm = 1,
  267. CreateUser = user,
  268. CreateTime = now,
  269. };
  270. await _detailRep.InsertAsync(detail);
  271. }
  272. }
  273. // ③ 删除:DB有、入参无
  274. foreach (var toDelete in dbDetails.Where(u => !inputIds.Contains(u.RecID)))
  275. {
  276. await _detailRep.DeleteAsync(u => u.RecID == toDelete.RecID);
  277. }
  278. }
  279. // ══════════════════════════════════════════════════════════════
  280. // 销售出库 POST /api/Order/shippingplan/ship
  281. // ══════════════════════════════════════════════════════════════
  282. /// <summary>销售出库(调用存储过程 pr_WMS_AutoCreateShipper)📦</summary>
  283. [DisplayName("销售出库")]
  284. [ApiDescriptionSettings(Name = "ShipShippingPlan"), HttpPost("shippingplan/ship")]
  285. public async Task<object> ShipShippingPlan([FromBody] ShippingPlanShipInput input)
  286. {
  287. var orgNo = ""; // 组织编号,可从配置或 UserManager 获取
  288. var account = _userManager.Account ?? "system";
  289. var ids = input.Ids;
  290. await _db.Ado.ExecuteCommandAsync(
  291. "CALL pr_WMS_AutoCreateShipper(@OrgNo, @Account, @Ids)",
  292. new SugarParameter("@OrgNo", orgNo),
  293. new SugarParameter("@Account", account),
  294. new SugarParameter("@Ids", ids));
  295. return new { message = "销售出库执行成功" };
  296. }
  297. // ──────────────── 内部查询结果映射类 ────────────────
  298. private sealed class ShippingPlanListRow
  299. {
  300. public int Id { get; set; }
  301. public string? LotSerial { get; set; }
  302. public string? OrdNbr { get; set; }
  303. public string? BillNo { get; set; }
  304. public DateTime? OrdDate { get; set; }
  305. public string? Country { get; set; }
  306. public string? CustomNo { get; set; }
  307. public string? CustomName { get; set; }
  308. public string? ItemNum { get; set; }
  309. public string? ItemName { get; set; }
  310. public string? Specification { get; set; }
  311. public decimal? Qty { get; set; }
  312. public decimal? Weight { get; set; }
  313. public decimal? Volume { get; set; }
  314. public string? Packaging { get; set; }
  315. public string? ShippingSite { get; set; }
  316. public DateTime? ShippingDate { get; set; }
  317. public string? ShippingAddress { get; set; }
  318. public string? Consignee { get; set; }
  319. public string? Telephone { get; set; }
  320. public string? AsnRecID { get; set; }
  321. public string? Sid { get; set; }
  322. }
  323. }